1算法思想
回溯
1.1含义
以深度优先方式搜索问题解的算法称为回溯法。
1.2思想
按照深度优先搜索策略,从根节点出发搜索解空间树,如果某结点不包含问题解,则逐层向祖先结点回溯;否则进入子树。
1.3特点
深度优先搜索+递归前设置变量,递归后清除变量设置
1.4适用
适合求取问题所有解类型的题目。例如求出问题的多少种组合。
1.5通用解法
回溯算法:
如果当前是问题的解,打印解;
设置变量;
递归;
清除变量设置。
1.6经典例题讲解
n皇后问题
在n×n格的国际象棋上摆放n个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
代码如下:
void place(int pos , vector<int>& result , vector< vector<int> >& results , int n) { if(pos == n) //如果当前是问题的解,打印解 { results.push_back(result); return; } //从中选出任意一个i值作为result[pos]摆放,作为第pos行的列 for(int i = 0 ; i < n ; i++) { bool isOk = true; for(int j = 0 ; j < pos ; j++) { //如果在同一列,说明不符合, if(result.at(j) == i) { isOk = false; break; } //如果在同一对角线 if( abs( result.at(j) - i ) == abs(j - pos) ) { isOk = false; break; } } if(isOk) {
result.push_back(i); //设置变量 place(pos + 1 , result , results , n); //递归 result.pop_back(); //清除变量设置 } } } |
1.7回溯与递归的区别
回溯通常也用到递归,递归往往求取的是一个解,回溯通常求取的是多个解。回溯的过程中需要对标记量进行设置,递归完成后,需要对标记量清除设置。
2回溯系列
类别-编号 |
题目 |
遁去的1 |
1 |
Prime ring problem 在给定的1到n的数字中,将数字填入环中,使得环中任意2个相邻的数字和为素数。按字典序输出所有符合条件的解 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47186279 关键: 1 对于其余情况,首先要判断当前的数字是否在环中,若不在环中,先标记为已用,并尝试设置第num+1个数字在ans数组中,然后继续尝试放入下一个数字,最后再将该数字 重新标记为未使用 2 注意剪枝的基本过程:初始化剪枝标记,初始化节点,放入循环中
//用于判定两个数的和是否为素数。 int prime[13] = {2,3,5,7,11,13,17,19,23,29,31,37,41}; //用于存放所有数的和 int ans[N]; bool mark[N];//剪枝标记
//判断一个数是否为素数 bool judge(int x) { //判断一个数是否为素数 for(int i = 0 ; i < 13 ; i++) { if(x==prime[i]) { return true; } } return false; } //判断最后一个数和第一个数是之和是否为质数,若是,输出答案 bool check(int n) { if(judge(ans[n] + ans[1])==false) { return false; } else { for(int i = 1 ; i <= n ; i++) { if(i!=1) { printf(" %d",ans[i]); } else { printf("%d",ans[i]); } } printf("\n"); return true; } }
//num:ans数组中已经存入的数字,n表示总共输入的数字个数 void DFS(int num,int n) { //如果已经存在大于一个数字,就开始检查 if(num > 1) { if(judge(ans[num] + ans[num-1])==false) { return; } } //如果所有数字全部确定完毕,需要检查第一个数和第n个数的和是否为质数 if(num==n) { check(n); //检查结束之后就返回 return; } //对第2个数到第n个数进行判定,递归判定 for(int i = 2 ; i <= n ; i++) { //如果当前数未被尝试 if(false==mark[i]) { mark[i] = true;//标记该数已经被尝试 ans[num+1] = i;//将新的数字加入到数组中 DFS(num+1,n);//尝试新的数字 mark[i] = false;//如果尝试失败,将当前数字重新设置为未被访问 } } } |
2 |
货郎担问题 四个顶点的货郎担问题。求从顶点1出发,最后回到顶点1的最短路线。 输入: 4 0 0 1 7 8 0 5 1 7 2 0 1 2 5 3 0 输出: 1 3 2 4 1 6
|
算法设计与分析 https://blog.csdn.net/qingyuanluofeng/article/details/47189381 v1 v2 v3 v4 v1 无穷 无穷 1 7 v2 8 无穷 5 1 v3 7 2 无穷 1 v4 2 5 3 无穷 算法分析: 因为是采用回溯法来做,肯定是递归,然后还需要现场清理。 要设置一个二维数组来标识矩阵内容,然后回溯还需要设计 一个二维标记数组来剪枝,设定一个目标变量,初始为无穷大, 后续如果有比目标变量值小的就更新。剪枝的条件就是如果走到当前节点的耗费值>=目标变量,就直接不再往下面走, 向上走。 深度优先 = 递归 递归基:如果到达叶子节点的上一个节点,那么就进行是否更新的判断 递归步:如果没有到达叶子节点,就进行剪枝操作,判断能否进入下一个节点,如果能,更新最优值
关键: 1 //递归基:如果已经遍历到叶子节点的上一层节点,i标识递归深度 if(i == g_n) { //判断累加和是否超过最大值,如果有0,应该排除;满足这个条件,才打印 if((g_iArr[pArr[i-1]][pArr[i]] != 0) && (g_iArr[pArr[g_n]][1] != 0) && (g_iCurResult + g_iArr[pArr[i-1]][pArr[i]] + g_iArr[pArr[g_n]][1] < g_iResult )) { g_iResult = g_iCurResult + g_iArr[pArr[i-1]][pArr[i]] + g_iArr[pArr[g_n]][1]; //用当前最优路径去更新最优路径,防止下一次没有 for(int k = 1 ; k <= g_n ; k++) { g_iBestPath[k] = pArr[k]; 2 //递归步:判断能否进入子树,需要尝试每一个节点 else { //尝试不同的组合 for(int j = i ; j <= g_n ; j++) { //判断能否进入子树:如果当前值+下一个连线值的和 < 最优值,就进入,0要pass if( (g_iArr[pArr[i-1]][pArr[j]] != 0) && (g_iCurResult + g_iArr[ pArr[i-1] ][ pArr[j] ] < g_iResult) ) 3 //交换i与j,则i为当前可以尝试的范围 //为完成后面k个元素的排列,逐一对数组第n-k~n个元素互换。数组第一个元素为1,生成后面n-1个元素的排列 //数组第一个元素与第二个元素互换,第一个元素为2,第2个元素为1,生成后面的n-1个元素的排列... swap(&pArr[i],&pArr[j]); //更新当前累加值,是i-1与i的 g_iCurResult += g_iArr[ pArr[i-1] ][ pArr[i] ]; //递归 backTrace(i+1,pArr); //回溯,清空累加值;能够走到这里,说明上述结果不是最优解,需要向求解树上一层回退 g_iCurResult -= g_iArr[pArr[i-1]][ pArr[i] ]; swap(&pArr[i],&pArr[j]);
代码: const int MAXSIZE = 100; const int MAX = 1000000000; int g_iArr[MAXSIZE][MAXSIZE];//邻接矩阵 int g_iResult;//存放最优解 int g_iPath[MAXSIZE];//存放最优路径上 int g_n;//元素个数 int g_iCurResult;//当前累加路径和 int g_iBestPath[MAXSIZE];//还需要设置一个数组,用来保存最优解
//可以做成字符串全排列的性质track(int i,int* pArr,int* pResult),其中pArr是用于存放最优解的路径 void backTrace(int i,int* pArr) { //递归基:如果已经遍历到叶子节点的上一层节点 if(i == g_n) { //判断累加和是否超过最大值,如果有0,应该排除;满足这个条件,才打印 if((g_iArr[pArr[i-1]][pArr[i]] != 0) && (g_iArr[pArr[g_n]][1] != 0) && (g_iCurResult + g_iArr[pArr[i-1]][pArr[i]] + g_iArr[pArr[g_n]][1] < g_iResult )) { g_iResult = g_iCurResult + g_iArr[pArr[i-1]][pArr[i]] + g_iArr[pArr[g_n]][1]; //用当前最优路径去更新最优路径,防止下一次没有 for(int k = 1 ; k <= g_n ; k++) { g_iBestPath[k] = pArr[k]; } } } //递归步:判断能否进入子树,需要尝试每一个节点 else { //尝试不同的组合 for(int j = i ; j <= g_n ; j++) { //判断能否进入子树:如果当前值+下一个连线值的和 < 最优值,就进入,0要pass if( (g_iArr[pArr[i-1]][pArr[j]] != 0) && (g_iCurResult + g_iArr[ pArr[i-1] ][ pArr[j] ] < g_iResult) ) { //交换i与j,则i为当前可以尝试的范围 //为完成后面k个元素的排列,逐一对数组第n-k~n个元素互换。数组第一个元素为1,生成后面n-1个元素的排列 //数组第一个元素与第二个元素互换,第一个元素为2,第2个元素为1,生成后面的n-1个元素的排列... swap(&pArr[i],&pArr[j]); //更新当前累加值,是i-1与i的 g_iCurResult += g_iArr[ pArr[i-1] ][ pArr[i] ]; //递归 backTrace(i+1,pArr); //回溯,清空累加值;能够走到这里,说明上述结果不是最优解,需要向求解树上一层回退 g_iCurResult -= g_iArr[pArr[i-1]][ pArr[i] ]; swap(&pArr[i],&pArr[j]); } } } } |
3 |
n皇后问题 在n×n格的国际象棋上摆放n个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 |
算法设计与分析 https://blog.csdn.net/qingyuanluofeng/article/details/47189389 //判断当前列能否摆放成功。判断存不存在相同列或者在同一个斜线上 bool place(int x[] , int k) { for(int i = 1 ; i < k ; i++) { if(x[i] == x[k] || abs(x[i] - x[k]) == abs(i - k)) { return false; |