【C/C++】n皇后問題/全排列/遞迴/回溯/演算法筆記4.3
按常規,先說一下我自己的理解。
遞迴中的return常用來作為遞迴終止的條件,但是對於返回數值的情況,要搞明白它是怎麼返回的。遞迴的方式就是自己呼叫自己,而在有返回值的函式中,上一層的函式還沒執行完就呼叫下一層,因此,當達到遞迴終止條件時,首先return的是最底層呼叫的函式,return之後,繼續執行上一層呼叫該函式之後的程式碼,此時我們看到的是上一層的情況,當上一層剩餘的程式碼執行完之後,表示上一層的函式也結束,此時再返回上上一層,執行遞迴程式碼之後的程式碼,如此往復迴圈,直到返回到最上層,結束整個遞迴過程。需要注意的是,上一層執行遞迴之後的程式碼的時候,會呼叫下一層返回的值,也可以理解為在執行上一層程式碼的時候會呼叫下一層的實現過程,直到下一層執行完返回一個數值,然後再加上上一層的數值,就構成了上一層return的東西,如此往復。
注意這個return,return是返回上一層,而不是跳出回到主函式。
然後如果不是return,在當前層沒有可以執行的東西的時候,也跳回到上一層。
下面我們來看演算法筆記中的全排列和n皇后問題。
n皇后問題
n皇后問題是指在一個n*n的國際象棋棋盤上放置n個皇后,使得這n個皇后兩兩均不在同一行、同一列、同一對角線上,求合法的方案數。
(我第一反應這不是圖論裡的匹配嘛……也可以點著色(x)
因為如果列舉n*n
種情況的位置,選擇n個,計算量太大,所以我們只考慮全排列情況,然後剔掉不滿足不在同一對角線上的情況。
法1:列舉
這裡枚舉出n長數列的全排列,然後剔掉不滿足不在同一對角線上的情況。
全排列:給出1~n的數字,給出所有的排列方式(不重複)。(Ann)
遞迴思想:
- 遞迴邊界:
- 遞迴本體:假設已經填好了P[1]~P[index-1],正準備填P[index]. 列舉x從1到n,如果hashtable[x] == false,就把它填到index中。然後遞迴下一位。
遞迴完成後,把這一位的hashtable[x]釋放。
判斷:到達邊界(輸出)的時候,判斷是不是在同一對角線上。
#include <iostream> #include <algorithm> using namespace std; const int maxn = 11; int n; int P[maxn]; bool hashTable[maxn] = {false}; int cnt = 0; void show_hashTable() { for (int i = 1; i <= n; i++) { printf("%d ", hashTable[i]); } printf("\n"); } void generateP(int index) { if(index == n+1) //遞迴邊界,邊界先判斷,只算結果 { for(int i = 1; i <= n; i++) { printf("%d", P[i]); } printf("\n"); return; // bool flag = true; //flag為true表示當前排列合法 // for(int i = 1; i <= n; i++) //遍歷任意兩個皇后 // { // for(int j = i + 1; j <=n; j++) // { // if(abs(i - j) == abs(P[i] - P[j])) //如果在一條對角線上 // { // flag = false; //不合法 // } // } // } // if(flag) cnt++; // return; } for(int x = 1; x <= n; x++) { printf("x %d\n", x); if(hashTable[x] == false) { P[index] = x; //show_hashTable(); hashTable[x] = true; generateP(index + 1); hashTable[x] = false; //show_hashTable(); } } } int main() { scanf("%d", &n); generateP(1); printf("%d\n", cnt); system("pause"); }
這段程式碼是怎麼執行的?
我實在是好奇,然後就一步一步嘗試了一下。
為了方便理解起見,這裡選n=3.
結果是123 132 213 231 312 321
自帶字典序。
for(int x = 1; x <= n; x++)
{
printf("x %d\n", x);
if(hashTable[x] == false)
{
P[index] = x;
//show_hashTable();
hashTable[x] = true;
generateP(index + 1);
hashTable[x] = false;
//show_hashTable();
}
}
我做了一個這段程式碼的執行原理:
法2:回溯
定義:在到達遞迴前的某層,由於一些事實導致已經不需要往任何一個子問題遞迴,就可以直接返回上一層。
從程式碼編寫上來看,其實就是把判斷放到了遞迴的最開頭。
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 11;
int n;
int P[maxn];
bool hashTable[maxn] = {false};
int cnt = 0;
void show_hashTable()
{
for (int i = 1; i <= n; i++)
{
printf("%d ", hashTable[i]);
}
printf("\n");
}
void generateP(int index)
{
if(index == n+1) //遞迴邊界,邊界先判斷,只算結果
{
for(int i = 1; i <= n; i++)
{
printf("%d", P[i]);
}
printf("\n");
cnt++;
return;
// bool flag = true; //flag為true表示當前排列合法
// for(int i = 1; i <= n; i++) //遍歷任意兩個皇后
// {
// for(int j = i + 1; j <=n; j++)
// {
// if(abs(i - j) == abs(P[i] - P[j])) //如果在一條對角線上
// {
// flag = false; //不合法
// }
// }
// }
// if(flag) cnt++;
// return;
}
for(int x = 1; x <= n; x++)
{
//printf("x %d\n", x);
if(hashTable[x] == false)
{
bool flag = true; //表示可行
for (int pre = 1; pre < index; pre++) //考察index之前的是否會與index衝突
{
if (abs(index - pre) == abs(x - P[pre]))
{
flag = false;
break; //已經設定了flag,很保險;break只是為了節約不要後面無意義的for迴圈
}
}
if (flag)
{
P[index] = x;
hashTable[x] = true;
generateP(index + 1);
hashTable[x] = false;
}
}
}
}
int main()
{
scanf("%d", &n);
generateP(1);
printf("%d\n", cnt);
system("pause");
}