码迷,mamicode.com
首页 > 其他好文 > 详细

回溯法

时间:2015-03-02 01:03:20      阅读:266      评论:0      收藏:0      [点我收藏+]

标签:回溯   八皇后   素数环   递归   剪枝   

只要能把待求解的问题分成不太多的步骤,每个步骤又只有不太多的选择,都可以考虑应用回溯法。想象一棵包含 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这个小循环,和这里类似,也可以用一个数组来替代掉。)
Code:

#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

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!