# **回溯算法** ​ **搜索与回溯是计算机解题中常用的算法,很多问题无法根据某种确定的计算法则来求解,可以利用搜索与回溯的技术求解。回溯是搜索算法中的一种控制策略。它的基本思想是:为了求得问题的解,先选择某一种可能情况向前探索,在探索过程中,一旦发现原来的选择是错误的,就退回一步重新选择,继续向前探索,如此反复进行,直至得到解或证明无解。** ​ **如迷宫问题:进入迷宫后,先随意选择一个前进方向,一步步向前试探前进,如果碰到死胡同,说明前进方向已无路可走,这时,首先看其它方向是否还有路可走,如果有路可走,则沿该方向再向前试探;如果已无路可走,则返回一步,再看其它方向是否还有路可走;如果有路可走,则沿该方向再向前试探。按此原则不断搜索回溯再搜索,直到找到新的出路或从原路返回入口处无解为止。** ## 算法框架 **递归回溯法算法框架[一]** ``` c int Search(int k)  {  for (i=1;i<=算符种数;i++)   if (满足条件)    {     保存结果     if (到目的地) 输出解;     else Search(k+1);     恢复:保存结果之前的状态{回溯一步}     }  } ``` **递归回溯法算法框架[二]** ``` c int Search(int k)  {   if (到目的地) 输出解;    else     for (i=1;i<=算符种数;i++)      if (满足条件)        {         保存结果;     Search(k+1);         恢复:保存结果之前的状态{回溯一步}        }  } ``` ## 例题讲解 **【例1】素数环:从1到20这20个数摆成一个环,要求相邻的两个数的和是一个素数。** **【算法分析】** **非常明显,这是一道回溯的题目。从1开始,每个空位有20种可能,只要填进去的数合法:与前面的数不相同;与左边相邻的数的和是一个素数。第20个数还要判断和第1个数的和是否素数。** **【算法流程】** **1、数据初始化; 2、递归填数:判断第i个数填入是否合法;** **A、如果合法:填数;判断是否到达目标(20个已填完):是,打印结果;不是,递归填下一个;** **B、如果不合法:选择下一种可能;** ``` c #include #include #include #include using namespace std; bool b[21]={0}; int total=0,a[21]={0}; int search(int); //回溯过程 int print(); //输出方案 bool pd(int,int); //判断素数 int main() { search(1); cout<"; for (int j=1;j<=20;j++) cout<sqrt(i)) return 1; else return 0; } ``` **【例2】设有n个整数的集合{1,2,…,n},从中取出任意r个数进行排列(r #include #include using namespace std; int num=0,a[10001]={0},n,r; bool b[10001]={0}; int search(int); //回溯过程 int print(); //输出方案 int main() { cout<<"input n,r:"; cin>>n>>r; search(1); cout<<"number="< #include #include using namespace std; int a[10001]={1},n,total; int search(int,int); int print(int); int main() { cin>>n; search(n,1); //将要拆分的数n传递给s cout<<"total="<0时,继续递归 s+=i; //回溯:加上拆分的数,以便产分所有可能的拆分 } } int print(int t) { cout<A[J] AND ABS(I-J)<>ABS(A[I]-A[J]){I和J分别表示两个皇后的行号} ``` c #include #include #include #include using namespace std; bool d[100]={0},b[100]={0},c[100]={0}; int sum=0,a[100]; int search(int); int print(); int main() { search(1); //从第1个皇后开始放置 system("pause"); } int search(int i) { int j; for (j=1;j<=8;j++) //每个皇后都有8位置(列)可以试放   if ((!b[j])&&(!c[i+j])&&(!d[i-j+7])) //寻找放置皇后的位置  //由于C++不能操作负数组,因此考虑加7 { //放置皇后,建立相应标志值 a[i]=j; //摆放皇后 b[j]=1; //宣布占领第j列 c[i+j]=1; //占领两个对角线 d[i-j+7]=1; if (i==8) print(); //8个皇后都放置好,输出 else search(i+1); //继续递归放置下一个皇后 b[j]=0; //递归返回即为回溯一步,当前皇后退出 c[i+j]=0; d[i-j+7]=0; } } int print() { int i; sum++; //方案数累加1 cout<<"sum="<2,1->3,3->1,4->3,5->2,7->4,8… ![image-20201130205822919](https://picreso.oss-cn-beijing.aliyuncs.com/image-20201130205822919.png) **【算法分析】** ​ 如图4(b),马最多有四个方向,若原来的横坐标为j、纵坐标为i,则四个方向的移动可表示为: 1: (i,j)→(i+2,j+1); (i<3,j<8) 2: (i,j)→(i+1,j+2); (i<4,j<7) 3: (i,j)→(i-1,j+2); (i>0,j<7) 4: (i,j)→(i-2,j+1); (i>1,j<8) 搜索策略: ​ S1:A[1]:=(0,0); ​ S2:从A[1]出发,按移动规则依次选定某个方向,如果达到的是(4,8)则转向S3,否则继续搜索下一个到达的顶点; ​ S3:打印路径。 ``` c #include  #include  #include  using namespace std;  int a[100][100],t=0; //路径总数和路径  int x[4]={2,1,-1,-2}, //四种移动规则   y[4]={1,2,2,1};  int search(int); //搜索  int print(int); //打印  int main() //主程序  {   a[1][1]=0;a[1][2]=0; //从坐标(0,0)开始往右跳第二步   search(2);   system("pause");  };  int search(int i)  {   for (int j=0;j<=3;j++) //往4个方向跳   if (a[i-1][1]+x[j]>=0&&a[i-1][1]+x[j]<=4   &&a[i-1][2]+y[j]>=0&&a[i-1][2]+y[j]<=8) //判断马不越界   {   a[i][1]=a[i-1][1]+x[j]; //保存当前马的位置   a[i][2]=a[i-1][2]+y[j];   if (a[i][1]==4&&a[i][2]==8) print(i);   else search(i+1); //搜索下一步   }  }  int print(int ii)  {   {   t++;   cout<";   cout<<"4,8"< #include #include #include using namespace std; int data[6][6]={{0,0,0,0,0,0},{0,13,11,10,4,7},{0,13,10,10,8,5},{0,5,9,7,7,4},{0,15,12,10,11,5},{0,10,11,8,8,4}}; int max1=0,g[10],f[10]; bool p[6]={0}; int go(int step,int t) // step是第几个人,t是之前已得的效益 { for (int i=1;i<=5;i++) if (!p[i]) //判断第i项工作没人选择 { f[step]=i; //第step个人,就选第i项工作 p[i]=1; //标记第i项工作被人安排了 t+=data[step][i]; //计算效益值 if (step<5) go(step+1,t); else if (t>max1) //保存最佳效益值 { max1=t; for (int j=1;j<=5;j++) g[j]=f[j]; //保存最优效益下的工作选择方案 } t-=data[step][i]; //回溯 p[i]=0; } } int main() { go(1,0); //从第1个人,总效益为0开始 for (int i=1;i<=5;i++) cout< #include #include using namespace std; int book[6],c; bool flag[6],like[6][6]={{0,0,0,0,0,0},{0,0,0,1,1,0},{0,1,1,0,0,1}, {0,0,1,1,0,0},{0,0,0,0,1,0},{0,0,1,0,0,1}};; int search(int); int print(); int main() { for (int i=1;i<=5;i++) flag[i]=1; search(1); //从第1个开始选书,递归。 system("pause"); } int search(int i) //递归函数 { for (int j=1;j<=5; j++) //每个人都有5本书可选 if (flag[j]&&like[i][j]) //满足分书的条件 { flag[j]=0; //把被选中的书放入集合flag中,避免重复被选 book[i]=j; //保存第i个人选中的第j本书 if (i==5) print(); //i=5时,所有的人都分到书,输出结果 else search(i+1); //i<5时,继续递归分书 flag[j]=1; //回溯:把选中的书放回,产生其他分书的方案 book[i]=0; } } int print() { c++; //方案数累加1 cout <<"answer " < #include #include #include using namespace std; int u[8]={1,2,2,1,-1,-2,-2,-1}, //8个方向上的x,y增量 v[8]={-2,-1,1,2,2,1,-1,-2}; int a[100][100]={0},num=0; //记每一步走在棋盘的哪一格和棋盘的每一格有 //没有被走过 bool b[100][100]={0}; int search(int,int,int); //以每一格为阶段,在每一阶段中试遍8个方向 int print(); //打印方案 int main() { a[1][1]=1;b[1][1]=1; //从(1,1)第一步开始走 search(1,1,2); //从(1,1)开始搜第2步该怎样走 cout<25) {print();return 0;} //达到最大规模打印、统计方案 for (k=0;k<=7;k++) //试遍8个方向 { x=i+u[k];y=j+v[k]; //走此方向,得到的新坐标 if (x<=5&&x>=1&&y<=5&&y>=1&&(!b[x][y])) { //如果新坐标在棋盘上,并且这一格可以走 b[x][y]=1; a[x][y]=n; search(x,y,n+1); //从(x,y)去搜下一步该如何走 b[x][y]=0; a[x][y]=0; } } } int print() { num++; //统计总方案 if (num<=5) //打印出前5种方案 { for (int k=1;k<=5;k++) //打印本次方案 { for (int kk=1;kk<=5;kk++) cout< #include #include using namespace std; int n,i,j,k,rest,sum,total; int s[7]; int main() { cout << "Input n k"; cin >> n >> k; total = 0; s[1] = 0; i = 1; while (i) { s[i]++; if (s[i] > n) i--; else if (i == k) { sum = 0; for (j = 1; j <= k; j++) sum += s[j]; if (n == sum) total++; } else { rest -= s[i]; i++; s[i] = s[i-1] - 1; } } cout << total; system("pause"); return 0; } 方法2、递归,参考程序如下。 #include #include #include using namespace std; int n,k; int f(int a,int b,int c) { int g = 0,i; if (b == 1) g = 1; else for (i = c; i <= a/b; i++) g += f(a-i,b-1,i); return g; } int main() { cout << "Input n,k:"; cin >> n >> k; cout << f(n,k,1); system("pause"); return 0; } 方法3、用动态循环穷举所有不同的分解,要注意剪枝,参考程序如下。 #include #include #include using namespace std; int n,k,total; int min(int x,int y) { if (x < y) return x; else return y; } void select(int dep,int rest,int last) { int i; if (dep == 0) total++; else for (i = min(rest-dep+1,last); i >= rest/dep; i--) select(dep-1,rest-i,i); } int main() { cout << "Input n,k:"; cin >> n >> k; total = 0; select(k,n,n); cout << total; system("pause"); return 0; } 方法4、递推法 首先将正整数n分解成k个正整数之和的不同分解方案总数等于将正整数n-k分解成任意个不大于k的正整数之和的不同分解方案总数(可用ferror图证明之),后者的递推公式不难得到,参考程序如下。 #include #include #include #include using namespace std; int i,j,k,n,x; int p[201][7]; int main() { cin >> n >> k; memset(p,0,sizeof(p)); p[0][0] = 1; for (i = 1; i <= n; i++) p[i][1] = 1; for (i = 1; i <= n - k; i++) for (j = 2; j <= min(i,k); j++) for (x = 1; x <= min(i,j); x++) p[i][j] += p[i-x][min(i-x,x)]; cout << p[n-k][k]; system("pause"); return 0; } ```