談談遞歸和回溯算法的運用


遞歸和回溯算法的運用

 

題目描述

有n個士兵站成一列,從第1個士兵前面向后望去,剛好能看到m個士兵,如果站在后面的士兵身高小於或者等於前面某個士兵的身高,那么后面的這個士兵就不能被看到,問這n個士兵有多少種排列方式,剛好在觀測位能看到m個士兵?

 

第一行輸入 n 個士兵和 m 個可以看到的士兵(n >= m),第二行輸入 n 個士兵的身高,輸出為排列方式的種數。


輸入:   

4 3

1 1 2 3

輸出:

6


 

也就是說,輸入數 n, m (n < m),然后輸入 n 個正整數到一個數組 a 中,a 數組中下標小的值如果小於后面某個數的話,后面這個數才可見。

我的思路是,

  1. 把數組 a 中的序列所有可能的排列情況列出來
  2. 然后對每一個可能的情況分析,如果某種排列能夠恰好使其中可見的數為 m,說明這種情況是符合要求的。

 

排列組合

那么先來考慮一個子問題:

Q:如何輸出一個數組的所有可能排列方式。

首先回顧一下我們是怎么進行全排列的。

一個大小為 n 的數組的全排列有 n! 種,


假設有 3 個各不相同的球,

          

   1         2         3 

要將他們放到 3 個不同的盒子中


就有 3! = 3x2x1 種方式,數學解題的思路如下:

首先把第一個球放到第 1 個盒子中,再考慮填入第 2 個盒子,把第 2 個球放入第 2 個盒子,剩下最后一個球只能放進最后一個盒子了,這是第一種情況;

然后回到放第 2 個盒子這一步,同樣的這個盒子可以先放第 3 個球,這樣第 2 個球就只能放入第 3 個盒子了,這就是第 2 種情況;

然后再回到填第 1 個盒子的地方,放入第 2 個球……

這樣總共還有 4 種可能的情況,那么總共的排列方式就是 6 種,分別的情形是(按球的序號進行排序):

  • 123
  • 132
  • 213
  • 231
  • 312
  • 321

這樣所有可能的排列方式就全部列出來了。可以看到,實際上將 1 2 3 這個序列中的數交換位置,可以得到后面的幾種排列。

嘗試交換位置

假設輸入的數組是a,a[0]=1, a[1]=2, a[2]=3

先嘗試如果單純的用循環加上交換數組數值的方法能否遍歷所有情況:

 1 #include <iostream>
 2 using namespace std;
 3 int n = 3;
 4 
 5 void print_arr(const int * a) {
 6     cout << "array: ";
 7     for(int i = 0; i < n; i++) {
 8         cout << a[i] << " ";
 9     }
10     cout << endl;
11 }
12 
13 void swap(int &a, int &b) {
14     int t = a;
15     a = b;
16     b = t;
17 }
18 
19 int main() {
20     int a[n] = {1, 2, 3};
21     
22     // 該數組本身的順序排列就是全排列中的一種 
23     print_arr(a);
24     
25     // 交換位置 
26     for(int i = 0; i < n; i++) {           // outer for 
27         for(int j = n-1; j > 0; j--) {    // inner for
28             swap(a[j], a[j-1]);
29             print_arr(a); 
30         }
31         swap(a[0], a[i]);
32     }
33     
34     return 0;
35 }

得到的結果為:

出現了 7 個序列,其中只有 5 個是不重復的,也就是說還少一種情況。

分析其原因:

在 inner for 循環結束的時候,將a[0]與a[i]進行交換,

這樣做實際上是類似於把第2個球放入第1個盒子的步驟,但是這沒有考慮到此時的數組已經不是最開始的 1, 2, 3 這樣的序列了

那么如果每次找到一個序列后,將數組重新設為 1, 2, 3 這樣的序列行不行呢?

仔細想一想,其實也不行,因為 inner for 里面的代碼僅僅交換了相鄰兩個數,這樣就遺漏了很多種情況。

遞歸和回溯

需要遍歷所有情況的話,最容易想到的應該就是遞歸了。

而且在思考排列球的方法中,很重要的一點就是

當所有球都放到了盒子中,要回到前一個盒子的那一步,選擇另一種方式放入小球。

這種方法就是回溯

很容易想到如果是 1 個球放 1 個盒子,只有 1 種情況,這是遞歸的終點(也就是把所有球都放到盒子中,這一次遞歸就結束了)。

那么,當序號最后的球(比如序號為 3,3 個球放入 3 個盒子)放到了第一個盒子中,而這趟遞歸也結束了。就認為所有可能的情況都已經遍歷過了,回溯遞歸也就結束了。

加入一個 bool 型數組,用於保存球的使用狀態(true 表示球已經放入盒子里)。

 1 #include <iostream>
 2 #include <string.h>
 3 using namespace std;
 4 int n = 3, m;
 5 int res = 0;
 6 
 7 void print_arr(const int * a) {
 8     cout << "array: ";
 9     for(int i = 0; i < n; i++) {
10         cout << a[i] << " ";
11     }
12     cout << endl;
13 }
14 
15 /**
16 * b  需要被分配數值的數組
17 * i  b 數組中需要被設置的序號 
18 * used 用來進行回溯的數組標志位,true 表示 a 數組中該序號的元素已經被使用 
19 * in 現在使用的 a 數組中的序號 
20 * 需要用到遞歸和回溯, 
21 * 若 i == n ,意味着 b 數組所有的元素都被分配了,此時可以嘗試打印數組
22 * 設置一個標志位 in,意味着 b[i] 空格將要被 a[in] 球占據。
23 * 每一個 b[i] 空格都要循環整個 a 中的球。
24 * 但是在 inner 的循環過程中  in 的值有可能超過 n,這個時候就需要直接退出循環了。 
25 */
26 int order(const int * a, int * b, bool used[], int i) {
27     /* all used */
28     if(i == n) {
29         print_arr(b);
30         /* 如果滿足條件,進行自定義的處理 */
31 //        if( getCoverd(b) == n - m) {
32 //            res++;
33 //        }
34         return 1;
35     }
36     int in = 0;
37     
38     while (in < n) {           // outter
39         while(used[in]) {    // inner
40             in++;
41         }
42         /*
43          * 如果在 inner 循環里 in 就已經達到 n 了
44          * 直接退出 outter 循環 
45         */
46         if(in >= n) {
47             break;
48         }
49         b[i] = a[in];
50         used[in] = true;
51         if( order(a, b, used, i+1) == 1 ) 
52         {
53             used[in] = false;
54             in++;
55         }
56     }    
57     return 1;
58 }
59 
60 int main() {
61     int a[n] = {1, 2, 3};
62     int b[n] = {};
63     bool used[n] = {false};
64     order(a, b, used, 0);        
65     cout <<    res ;
66 }

試着運行一下:

果然所有的可能情況都遍歷到了,並且沒有重復,沒有遺漏。然后把代碼中的 n 改為其它數,給數組 a 添加相應的元素,也能夠遍歷所有情況。

到這一步,全排列就已經實現了。

 


不過其實這里的代碼還有改進的地方,仔細觀察 order(const int*, int *, bool[], int) 這個函數,能夠發現它的返回值其實並沒有什么作用,可以考慮去掉返回值,將函數類型改為 void,這樣能夠減少堆棧的內存使用。

完成算法題

現在還需要的一步就是要算出每種可能的排列中,可見士兵的數量。用一個 getUncovered(int *a); 函數算出可見的士兵數,然后比較是否等於 m 就可以了。

完整的程序:

 1 #include <iostream>
 2 #include <string.h>
 3 using namespace std;
 4 int n , m;
 5 int res = 0;
 6     
 7 void print_arr(const int * a);
 8 int getCoverd(const int * a);
 9 
10 void print_arr(const int * a) {
11     cout << "array: ";
12     for(int i = 0; i < n; i++) {
13         cout << a[i] << " ";
14     }
15     cout << endl;
16 }
17 
18 /* 一個序列中出現的沒有被擋住的人 */
19 int getUncovered(const int * a) {
20     int uncovered = 1;
21     // 指向當前能看到的最高的人 
22     int point = 0; 
23     for(int i = 1; i < n; i++) {
24         if(a[i] > a[point]) {
25             uncovered++;
26             point = i;
27         }
28     }
29     return uncovered;
30 }
31 
32 /**
33 * b  需要被分配數值的數組
34 * i  b 數組中需要被設置的序號 
35 * used 用來進行回溯的數組標志位,true 表示 a 數組中該序號的元素已經被使用 
36 * in 現在使用的 a 數組中的序號 
37 * 需要用到遞歸和回溯, 
38 * 若 i == n ,意味着 b 數組所有的元素都被分配了,此時可以嘗試打印數組
39 * 設置一個標志位 in,意味着 b[i] 空格將要被 a[in] 球占據。
40 * 每一個 b[i] 空格都要循環整個 a 中的球。
41 * 但是在 inner 的循環過程中  in 的值有可能超過 n,這個時候就需要直接退出循環了。 
42 */
43 void order(const int * a, int * b, bool used[], int i) {
44     /* all used */
45     if(i == n) {
46         print_arr(b);
47         /* 如果滿足條件,進行自定義的處理 */
48         if( getUncovered(b) == m) {
49             res++;
50         }
51 //        return 1;
52     }
53     int in = 0;
54     
55     while (in < n) {           // outter
56         while(used[in]) {    // inner
57             in++;
58         }
59         /*
60          * 如果在 inner 循環里 in 就已經達到 n 了
61          * 直接退出 outter 循環 
62         */
63         if(in >= n) {
64             break;
65         }
66         b[i] = a[in];
67         used[in] = true;
68         order(a, b, used, i+1);
69         used[in] = false;
70         in++;
71     }        
72 }
73 
74 int main() {
75     cin >> n >> m;
76     
77     int a[n] = {};
78     int b[n] = {};
79     bool used[n] = {false};
80 
81     for(int i = 0; i < n; i++) {
82         cin >> a[i];
83     }
84 
85     order(a, b, used, 0);        
86     cout <<    res ;
87 }

最后進行一個簡單的測試,結果非常棒!

總結

  1. 把數學問題轉化成程序問題,要多做嘗試,盡可能的用自己熟悉的方法去理解問題,用易於實現的方法一步步地來解決問題
  2. 實際上最開始我的思路很亂,又想着將數組 a 中的數賦值到 b 中,又想着 b 中的數應該存放 a 中的哪個數,這樣導致想得太復雜,遞歸算法的實現也很亂。后來用兩個數 1,2 作為數組的兩個元素進行調試,在幾個斷點之間觀察變量的值,根據變量的值不斷地對遞歸函數實現進行修改,最終實現了正確的遞歸。
  3. 通過對函數的觀察,改進了函數,去掉了無用的返回值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM