回溯法

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了回溯法相关的知识,希望对你有一定的参考价值。

回溯法(backtracking):递归地构造和枚举可能的情况,同时排除不必要的枚举,检查所有可能的解,这就是回溯法的思路

如果让我来评价的话,这种思路真的是很简单很暴力。但是往往很有效。

一、经典模型:八皇后问题:

在棋盘上放置8个皇后,棋盘为8*8,使它们互不攻击,每个皇后的攻击范围为同行同列和同对角线,找出所有的解

思路:

  1. 我们不能直接枚举所有可能的情况,因为直接枚举的解答树会有C864=4.426*10^9个节点,这是一定会爆的。
  2. 既然同行同列都会攻击,那么我们可以用cur表示放置的行数,递归地放置0-7行,这样可以避免对行的枚举,如果放置成功,那么这一定是一个解
  3. 估算计算量:如果说我们在不重复的行数放置皇后,那么计算量为8!=40320。这是可接受的
  4. 实现:如果我们用二维数组C记录状态(condition),vis[][]数组记录被占用的列、主对角线(左上到右下)、副对角线(右上到左下),递归放置即可

实现如下:此代码找出并打印n皇后问题的解

 1 //找出并打印n皇后问题的解
 2 #include<cstdio>
 3 #include<cstring>
 4 int tot=0,n,vis[3][100],C[10];//tot为解得总数,n为皇后个数 ,C记录地图,下标为行数,数组内容为列数 
 5 
 6 void search(int cur){
 7     if(cur==n){
 8         tot++;
 9         printf("\\n");
10         for(int i=0;i<n;i++){
11             for(int j=0;j<n;j++)
12                 printf("%c ",j!=C[i]?*:Q);
13             printf("\\n");
14         }
15         printf("\\n");
16     }
17     else for(int i=0;i<n;i++){
18         if(!vis[0][i] && !vis[1][cur+i] && !vis[2][cur-i+n]){
19             C[cur]=i;
20             vis[0][i]=vis[1][cur+i]=vis[2][cur-i+n]=1;
21             search(cur+1);
22             C[cur]=i;
23             vis[0][i]=vis[1][cur+i]=vis[2][cur-i+n]=0;
24         }
25     }
26 }
27 int main(){
28     tot=0,scanf("%d",&n);
29     memset(vis,0,sizeof(vis));
30     memset(C,0,sizeof(C));
31     search(0);
32     printf("%d",tot);
33 }

如果只找出数目,不打印棋盘,实现如下

其中整个C数组都去掉了

 1 #include<cstdio>
 2 #include<cstring>
 3 int tot=0,n,vis[3][20];//tot为解得总数,n为皇后个数 ,C数组省去了 
 4 
 5 void search(int cur){
 6     if(cur==n)tot++;
 7     else for(int i=0;i<n;i++){
 8         if(!vis[0][i] && !vis[1][cur+i] && !vis[2][cur-i+n]){
 9             vis[0][i]=vis[1][cur+i]=vis[2][cur-i+n]=1;
10             search(cur+1);
11             vis[0][i]=vis[1][cur+i]=vis[2][cur-i+n]=0;
12         }
13     }
14 }
15 int main(){
16     tot=0,scanf("%d",&n);
17     memset(vis,0,sizeof(vis));
18     search(0);
19     printf("%d",tot);
20 }

 

二、素数环(UVa 524 PE了9次我去):

题目简述:输入一个数字n,把1-n的组成一个环,要求相邻两个数字之和为素数,其中n满足(0<n<=16), 输出从1开始,逆时针排列,每种环恰好输出一次

详细要求见传送门:UVa 524

思路:用回溯法,用数组a存储构造的字符串,vis数组记录构造的数字是否已经填入,用prime数组来打素数表,first用来判断输出格式,例如n==7时没有答案,Case语句不能输出

dfs实现如下:

  1. 从第二个位置填数字
  2. 如果cur==n,再判断最后一个数字和第一个数字的和能否构成素数,如果能,按指定格式输出。

实现如下:注意如何避免PE,下面的实现中有三处格式控制,防止PE

技术分享
 1 #include<cstdio>
 2 #include<cmath>
 3 int kase,n=20,prime[40],a[20],vis[20],first;//vis[i]记录 第i个数字是否被填在a数组中,first记录是否打印Case语句 
 4 bool isPrime(int x){
 5     if(x<2||(x%2==0)&&x!=2)return false;
 6     if(x==2||x==3||x==5)return true;
 7     for(int i=3,len=(int)sqrt(1.0*x)+1;i<=len;i++)
 8         if(x%i==0)return false;
 9     return true;
10 }
11 void dfs(int cur){
12     if(cur==n && prime[a[0]+a[n-1]]){//打印方案
13         if(first){
14             printf("Case %d:\\n",++kase);//这有格式 
15             first=0;//第一次打印 
16         } 
17         for(int i=0;i<n;i++){
18             printf("%d",a[i]);
19             printf("%c",i<n-1? :\\n);//这有格式 
20         }
21     }
22     else for(int i=2;i<=n;i++){//这里是因为第一个数字必须是1,所以 1 不变,填后面 
23         if(!vis[i] && prime[i+a[cur-1]]){
24             vis[i]=1;//设置标志
25             a[cur]=i;//把数字i填入到a[cur]中
26             dfs(cur+1);
27             vis[i]=0;//清除标志
28         }
29     }
30 }
31 int main(){
32     //初始化
33     for(int i=0;i<20;i++)a[i]=i+1;
34     for(int i=0;i<40;i++)prime[i]=isPrime(i);//打素数表
35     while(scanf("%d",&n)!=EOF){
36         first=1;
37         if(kase)puts("");//这里有格式 
38         dfs(1);//回溯法,这里不是0 
39     }
40 }
View Code

技术分享

 

以上是关于回溯法的主要内容,如果未能解决你的问题,请参考以下文章

用回溯法求01背包问题,怎样使用C++模板啊,迫切求指点!

用回溯法做0-1背包问题,这两行(程序中标注)是干嘛?为啥又要减?

回溯法八皇后问题(递归和非递归)

暴力穷举和回溯法(八皇后问题)

0-1背包问题如下,画用回溯法求解时的搜索情况,急用啊

[XJTUSE 算法设计与分析] 第五章 回溯法