只要能把待求解的问题分成不太多的步骤,每个步骤又只有不太多的选择,都可以考虑应用回溯法。想象一棵包含 L 层,每层的分支因子均为b的解答树,它的结点数高达1+b+b^2+...+b^(L-1)=(b^L-1)/(b-1)。无论是b太大还是L太大,结点数都会是天文数字。
最简单的思路是,从8X8=64个格子中选一个子集,使得满足要求。正是子集枚举问题。然而,规模是2^64个。
第二个想法是,从64个格子中选8个格子。这是组合生成问题,有C(64,8)=4.426X10^9种方案。
进一步优化,因恰好每行每列只放置一个皇后,若用C[x]表示第x行皇后的列编号,则是全排列生成问题。有8!=40320个。
在编写递归枚举程序之前,需要深入分析问题,对模型优化。一般还应对解答树的结点数有一个粗略的估计。
事实上,四皇后问题的完整解答树个数只有17个结点,比4!=24小;因为有些结点无法继续扩展。(即为可行性约束)
当把问题分成若干步骤并递归求解时,若当前步骤没有合法选择,则递归函数不再递归调用自身,而是返回上一级递归调用,这种现象称为回溯。递归枚举算法常称为回溯法。
Code:
//八皇后问题求解,解的个数 #include<stdio.h> #include<stdlib.h> void search(int cur); int C[100]; int cnt; int n; int main() { scanf("%d",&n); cnt=0; search(0); printf("%d\n",cnt); system("pause"); return 0; } void search(int cur)//放置第cur行 { if(cur==n) cnt++;//递归边界 else for(int i=0;i<n;++i) { C[cur]=i;//尝试把第cur行的皇后放在第 i 列 int ok=1; for(int j=0;j<cur;++j)//检查是否和前面的皇后冲突 if(C[cur]==C[j]||C[cur]+cur==C[j]+j||C[cur]-cur==C[j]-j) { ok=0; break; } if(ok) search(cur+1); } }结点数似乎很难进一步减少,但效率可以继续提高:利用二维布尔值数组vis[3][n*2]直接判断当前尝试的皇后所在的列和两个对角线是否已有其他皇后。(主对角线标识y-x可能为负,所以加上n) (这个思想相当于用一个数组省掉了一个循环中的小循环,可以对比之前的枚举排列中第一个思路,那里else中的i循环里的j这个小循环,和这里类似,也可以用一个数组来替代掉。)
#include<stdio.h> #include<stdlib.h> #define MAXN 100 void search(int cur); int C[MAXN]; int vis[3][MAXN]; int n; int cnt; int main() { scanf("%d",&n); cnt=0; search(0); printf("%d\n",cnt); system("pause"); return 0; } void search(int cur) { if(cur==n) cnt++; else for(int i=0;i<n;++i) { if(!vis[0][i]&&!vis[1][cur+i]&&!vis[2][cur-i+n])//利用二维数组直接判断 { C[cur]=i;//如果不打印解,整个C数组可以省略 vis[0][i]=vis[1][cur+i]=vis[2][cur-i+n]=1;//修改全局辅助变量 search(cur+1); vis[0][i]=vis[1][cur+i]=vis[2][cur-i+n]=0;//切记!要改回来 } } }一般地,在回溯法中修改了辅助的全局变量,则一定要及时把它们恢复原状。(如这里的vis数组)例如,若函数有多个出口,则需在每个出口处恢复被修改的值。除非,你估计保留你的修改。(如这里的全局变量cnt。还是不太一样。。)
输入正整数n,把1到n组成一个环,使得相邻两个整数之和均为素数。输出时从整数1开始逆时针排列,同一个环输出一次。n<=16。
第一个思路是,生成-测试法。对每个排列进行测试,16!=2X10^13.
Code:
//素数环,生成-测试法 #include<cstdio> #include<algorithm> #define MAXN 1000 using namespace std; int isp[MAXN];//布尔数组,isp[i]表示整数i是否为素数 int A[MAXN]; int is_prime(int x) {//do NOT use this if x is very large if(x==1) return 0; for(int i=2;i*i<=x;++i) if(x%i==0) return 0; return 1; } int main() { int n; scanf("%d",&n); for(int i=2;i<=2*n;++i) isp[i]=is_prime(i); for(int i=0;i<n;++i) A[i]=i+1; do { int ok=1; for(int i=0;i<n;++i) if(isp[A[i]+A[(i+1)%n]]==0) { ok=0; break; } if(ok) { for(int i=0;i<n;++i) printf("%d ",A[i]); printf("\n"); } }while(next_permutation(A+1,A+n)); system("pause"); return 0; }发现,当n=12时就很慢。 用回溯法的话,和之前八皇后类似,dfs(cur)函数表示放置第cur位置上的数。第0位置肯定放1,从第1位置开始放,每个位置都是从2到n的枚举,并且测试是否满足。
Code:
//素数环,回溯法 #include<stdio.h> #include<stdlib.h> #include<string.h> #define MAXN 1000 void dfs(int cur,int n); int A[MAXN]; int vis[MAXN]; int isp[MAXN]; int is_prime(int x) { if(x==1) return 0; for(int i=2;i*i<=x;++i) if(x % i==0) return 0; return 1; } int main() { int n; scanf("%d",&n); for(int i=2;i<=2*n;++i) isp[i]=is_prime(i); memset(vis,0,sizeof(vis)); A[0]=1; dfs(1,n); system("pause"); return 0; } void dfs(int cur,int n)//放置第cur位置 { if(cur==n && isp[A[n-1]+A[0]])//递归边界。测试第一个数和最后一个数 { for(int i=0;i<n;++i) printf("%d ",A[i]); printf("\n"); } else for(int i=2;i<=n;++i)//尝试放2到n if(!vis[i]&&isp[i+A[cur-1]])//若i没放过,且与前一个数之和为素数 { A[cur]=i; vis[i]=1;//设置使用标志 dfs(cur+1,n); vis[i]=0;//清楚标志 } }经测试,回溯法比生成-测试法快很多。 另外,从解答树的角度,回溯法正是按照深度优先的顺序在遍历解答树。
若一个字符串包含两个相邻的重复子串,则是容易的。如BB、ABCDABCD等。而D、DC、CBABCBA则是困难的串。输入正整数n和L,输出由前L个字符组成的、字典序第n小的困难的串。
一种方法是检查所有长度为偶数的子串,判断前一半是否等于后一半。尽管正确,但做了很多无用功。
在八皇后问题中,我们只判断当前皇后是否和前面的皇后冲突,但并不判断以前的皇后是否相互冲突——那些皇后在之前已经判断过了。同样的道理,这里只需要判断当前串的后缀,而非所有子串。
这里还是递归的一种思想,就是只考虑当前位置。这样的话,即是假设前面的位置是已经符合要求的,现在考虑末位新加的当前位置,则只需要从后面考虑子串即可。从当前位置往前数长度为j的子串,和连续的再向前长度为j的子串,是否相同。
Code:
#include<stdio.h> #include<stdlib.h> int dfs(int cur); int cnt; int n,ll; int C[85]; int main() { scanf("%d%d",&n,&ll); cnt=0; dfs(0); system("pause"); return 0; } int dfs(int cur) { if(cnt++==n) { for(int i=0;i<cur;++i) printf("%c",C[i]+'A'); printf("\n"); return 0; } for(int i=0;i<ll;++i)//当前位置尝试各个字符 { C[cur]=i; int ok=1;//当前串是否是困难的 for(int j=1;j*2<=cur+1;++j)//从后往前判断2*j长度的各一半是否相同 { int equal=1;//当前长度为j的两个串是否相同 for(int k=0;k<j;++k)//遍历长度为j的前后部分的对应位置 if(C[cur-k]!=C[cur-j-k]) { equal=0; break;} if(equal) { ok=0; break;} } if(ok) { if(dfs(cur+1)==0) return 0;}//递归搜索。如果已找到解,则直接退出 } return 1; }此题一开始可以想到在第0位置放置A,然后放置第1位置,只有当后面的位置没有选择、即不能满足条件时,才回到前面位置继续换大一点的字符枚举,如第0位置可能要换成B。其中每个位置都要枚举,枚举到什么程序又不确定,没有选择时还需要回溯,这种情况就要递归。
这里的回溯,是按照深度优先遍历解答树,保证了字典序的第n小。
下面是左边是L=2时完整的解答树,右边是L=3时部分的解答树。事实上,L=3时,可以构造出无限长的困难的串。
(图用Visio画的,而且直接截图,不太好,有人推荐个常用画图的吗?而且感觉CSDN博客贴图好麻烦。)
一个有n个结点的图G和这些结点的排列,定义结点i的带宽b(i)为i和相邻结点在排列中的最远距离,而所有b(i)的最大值就是整个图的带宽。给定图G,求出让带宽最小的结点排列。
直接的思路是,递归枚举全排列,分别计算带宽,选取最小的方案。
如何优化呢?八皇后问题中,有很多可行性约束,可以在得到完整解之前避免扩展那些不可行的结点,但本题并没有可行性约束——任何排列都是合法的。
优化:一,记录目标已找到的最小带宽k。若发现两个结点的距离大于等于k,则再怎么扩展也不可能比当前解更优,可以剪枝。二,在搜索到结点u时,u结点还有m个相邻点没有确定位置,那么对结点u来说,最理想的情况是这m个结点紧跟在u后面,这样的结点带宽为m,而其他任何非理想情况的带宽至少为m+1。这样,若m>=k,可以剪枝。
这样的剪枝优化思路在很多论文中也很常见。此题相当于是最小化最大值。记录下已找到的最小值,检测当前情况时,发现当前情况的最优值比已找到的最优值差,则可以直接剪枝。
附,打印解法的八皇后Code:
//解数,及具体方法(每行皇后放置的列号) #include<stdio.h> #include<string.h> #define MAXN 50 void search(int cur); int n; int cnt=0; int c[MAXN]; int path[1000][MAXN]; //int k=0; int main() { while(scanf("%d",&n)==1) { cnt=0; //k=0; search(0); printf("%d\n",cnt); for(int i=0;i<cnt;++i) { for(int j=0;j<n;++j) printf("%d ",path[i][j]); printf("\n"); } } return 0; } void search(int cur) { if(cur==n) { memcpy(path[cnt++],c,sizeof(c)); } else for(int i=0;i<n;++i) { int ok=1; c[cur]=i; for(int j=0;j<cur;++j) if(c[cur]==c[j]||cur+c[cur]==j+c[j]||cur-c[cur]==j-c[j]) {ok=0; break;} if(ok) search(cur+1); } }
//解数,及具体放置。回溯法。 #include<stdio.h> #include<string.h> #define MAXN 50 void search(int cur); int c[MAXN]; int path[1000][MAXN];//path的行数不要开错了,和MAXN没关系 //int k=0; int cnt=0; int vis[3][2*MAXN]; int n; int main() { while(scanf("%d",&n)==1) { cnt=0; //k=0; memset(vis,0,sizeof(vis)); search(0); printf("%d\n",cnt); for(int i=0;i<cnt;++i) { for(int j=0;j<n;++j) printf("%d ",path[i][j]); printf("\n"); } } return 0; } void search(int cur) { if(cur==n) { memcpy(path[cnt++],c,sizeof(c));} else for(int i=0;i<n;++i) { if(!vis[0][i]&&!vis[1][cur+i]&&!vis[2][cur-i+n]) { c[cur]=i; vis[0][i]=vis[1][cur+i]=vis[2][cur-i+n]=1; search(cur+1); vis[0][i]=vis[1][cur+i]=vis[2][cur-i+n]=0; } } }
原文地址:http://blog.csdn.net/buxizhizhou530/article/details/44007879