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

遞歸和回溯算法的運用

 

題目描述

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

 

第一行輸入 n 個士兵和 m 個能夠看到的士兵(n >= m),第二行輸入 n 個士兵的身高,輸出爲排列方式的種數。算法


輸入:   數組

4 3函數

1 1 2 3測試

輸出:spa

6調試


 

也就是說,輸入數 n, m (n < m),而後輸入 n 個正整數到一個數組 a 中,a 數組中下標小的值若是小於後面某個數的話,後面這個數纔可見。code

個人思路是,blog

  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. 經過對函數的觀察,改進了函數,去掉了無用的返回值。
相關文章
相關標籤/搜索