算法(三)回溯法
一、概述
1、概念
回溯法有“通用解题法”之称。用它可以系统地搜索问题的所有解。回溯法是一个既带有系统性又带有跳跃性的搜索算法。
在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。
当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯 。(其实回溯法就是对隐式图的深度优先搜索算法)。若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。 而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
2、注意
(1)为什么有的回溯是0到N-1,有的是1到N
第一种情况数组下标一般从0开始,第二种一般从1开始(推荐)
(2)对于层数不确定或条件复杂的,最好用非回溯做
如不连续的和条件
(3)为什么不用广度优先
https://mp.weixin.qq.com/s/ClKJXdyFeSSW0A8FLFksnA
3、解题思路
(1)针对所给问题,定义易于搜索的解空间;
(2)以深度优先方式搜索解空间
(3)在搜索过程中对每一步用**剪枝函数(判断条件:可以放吗?值符合条件吗?)**避免无效搜索。
4、技巧
(1)当情况比较复杂时,要学会自己处理:如使用数组等
(2)当起点确定时,从第二层开始,但要注意数组起点的初始化
二、子集树问题
1、概念
当所给问题是从n个元素的集合S中找出S满足某种性质的子集时,相应的解空间树称为子集树。
这类子集树通常有2n个叶结点,其结点总个数为2(n+1)-1。遍历子集树的算法需Ω(2^n)计算时间。总解空间层为1到n+1。如下,3个元素,层数为1到4。
2、解题思路
/*** 回溯法搜索子集树的算法范式* output(x) 记录或输出得到的可行解x* constraint(t) 当前结点的约束函数* bount(t) 当前结点的限界函数* @param t t为当前解空间的层数,从第0层开始。n为总解空间的层数*/
void backtrace(int t)
{//这里可以有剪枝//....//一次搜索完成if (t > n){//进行当前求最值、是否符合题意等情况的判断,符合则记录或输出!!!Output(x);}else{//相当于暴力枚举,列举全部情况//1、处理第t个个体,第t个个体有n种选择for (int i = 0; i <= n; i++){//2、处理x[t] = i;//3、判断(最优性剪枝和可行性剪枝、是否已访问等)if (constrain(t) && bound(t)){backtrace(t + 1);}//4、恢复(简单的赋值后续可以重新覆盖,可以不进行恢复)...}}
}
3、例题
(1)0-1背包问题
两种以上选择,处理完要恢复。
#include #define N 3 //物品的数量
#define C 16 //背包的容量int w[4]={0,10,8,5}; //每个物品的重量
int v[4]={0,5,4,1}; //每个物品的价值
int x[4]={0,0,0,0}; //x[i]=1代表物品i放入背包,0代表不放入int CurWeight = 0; //当前放入背包的物品总重量
int CurValue = 0; //当前放入背包的物品总价值int BestValue = 0; //最优值;当前的最大价值,初始化为0
int BestX[N]; //最优解;BestX[i]=1代表物品i放入背包,0代表不放入void backtrack(int t)
{//一次搜索完毕if(t>N){//如果找到了一个更优的解if(CurValue>BestValue){//保存更优的值和解BestValue = CurValue;for(int i=1;i<=N;++i) BestX[i] = x[i];}}else{//遍历当前节点的子节点:0 不放入背包,1放入背包for(int i=0;i<=1;++i){x[t]=i;if(i==0) //不放入背包{backtrack(t+1);}else //放入背包{//约束条件:放的下if((CurWeight+w[t])<=C){CurWeight += w[t];CurValue += v[t];backtrack(t+1);CurWeight -= w[t]; //恢复CurValue -= v[t];}}}}
}int main(int argc, char* argv[])
{backtrack(1);printf("最优值:%d\n",BestValue);for(int i=1;i<=N;i++){printf("最优解:%-3d",BestX[i]);}return 0;
}
(2)N皇后问题
一个一个放。
第一个皇后放到第N个结束。充分利用问题隐藏的约束条件:每个皇后必然在不同的行(列),每个行(列)必然也只有一个皇后。这样我们就**可以把第N个皇后放到第N个行中,使用Pos[i]表示皇后i在i行中的位置(也就是列号)(i = 0 to N-1)。**这样代码会大大的简洁,因为节点的子节点数目会减少,判断冲突也更简单。而且避免了重复的计算。
#include
using namespace std;
int n;
int x[100]; //第t个放在第x[]列
int v[100]; //时间优化:标注第v[]列是否已被占用
int sum=0;void output(){cout << "第" <n){ //摆完了,如果t>n说明已经放了n个,已经完成一次放置sum++;//output();}else{//处理第t个,有n种选择,尝试将第t个皇后放在第t行第x[t](i)列for(int i=1;i<=n;i++){//时间优化:第i列没有个体放置if(v[i] == 0){x[t]=i;if(place(t)){ //可以放在i位置处,则继续搜索v[i] = 1;BackTrace(t+1);v[i] = 0;}//这里不可以省略!!x[t]=0; }}}
}
//非递归
void backTrace1(){int k;x[1]=0;k=1;while(k>=1){x[k]+=1;先放在第一个位置while((x[k]<=n && !(place(k)))){//如果不能放x[k]++;// //放在下一个位置}if(x[k]<=n){if(k==n){// //如果已经放完了n个皇后sum++;//output();}else{// //没有处理完,让k自加,处理下一个皇后k++;x[k]=0;}}else{// 当前无法完成放置,则进行剪枝,回溯回去,回到第k-1步k--;}}
}
int main()
{memset(x,0,sizeof(x));cin >> n;backTrace(1);//backTrace1();cout << sum;return 0;
}
(3)图的着色问题
https://www.cnblogs.com/ECJTUACM-873284962/p/8447050.html#_label3_1
(4)骑士游历问题
当情况比较复杂时,要学会自己处理:如使用数组等
https://www.cnblogs.com/cs-whut/p/11153529.html
(5)集合划分问题
https://www.cnblogs.com/ECJTUACM-873284962/p/8447050.html#_label3_1
(6)连续邮资问题???
**问题:**假设国家发行了k种不同面值的邮票,并且规定每张信封上最多只允许贴h张邮票。连续邮资问题要求对于给定的k和h的值,给出邮票面值的最佳设计,在1张信封上可贴出从邮资1开始,增量为1的最大连续邮资区间。
思路:
(1)用stampval来保存各个面值,用maxval来保存当前所有面值能组成的最大连续面值。
(2)stampval[0] 一定等于1,因为1是最小的正整数。相应的,maxval[0]=1*h。
(3)stampval[0] 一定等于1,因为1是最小的正整数。相应的,maxval[0]=1*h。接下去就是确定第二个,第三个…第k个邮票的面值了。
(4)对于stampval[i+1],它的取值范围是stampval[i]+1~maxval[i]+1。stampval[i]+1是因为这一次取的面值肯定要比上一次的面值大,而这次取的面值的上限是上次能达到的最大连续面值+1, 是因为如果比这个更大的话, 那么就会出现断层, 即无法组成上次最大面值+1这个数了。
https://www.cnblogs.com/ECJTUACM-873284962/p/8447050.html#_label3_1
//标记每种取到的钱数
//m使用了第m张邮票贴到邮件上进行凑总数,不能多于h张
void mark(int n,int m,int sum)
{ if(m>h) return; vis[sum]=true;//现有n种数值不同的邮票可以凑数for(int i=1;i<=n;++i) mark(n,m+1,sum+stampval[i]);
}
(7)符号三角形问题?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kPQq2SYz-1595651301331)(C:\Users\ddjj6\AppData\Roaming\Typora\typora-user-images\1583325911845.png)]
**问题:**第一行有n个元素,对于给定的n,计算有多少个不同的符号三角形,使其所含的“+”和“-”的个数相同。
思路:
(1)三角形所含符号总个数为n*(n+1)/2,当其为奇数时,显然不存在包含的"+“号个数与”-"号个数相同的符号三角形。此条件可用于剪枝。
(2)要会使用数组对三角形进行模拟。
(3)由题意知,只要第一行n个确定,整个三角型就确定了。
(4)第一行有n个符号,则此时三角形有n行。
#include
using namespace std;
int n,half,counts,p[100][100],sum;//(2)void backtrack(int t)
{if(t>n) sum++;else{for(int i=0;i<2;i++){p[1][t]=i;//第一行符号counts+=i;//当前"+"号个数//由当前第一行符号情况得出整个三角形,遍历第二行到第t行 (3)(4)for(int j=2;j<=t;j++){//条件p[j][t-j+1]=p[j-1][t-j+1]^p[j-1][t-j+2];counts+=p[j][t-j+1];}if(!((counts>half)||(t*(t+1)/2-counts>half))){backtrack(t+1);}for(int j=2;j<=t;j++){counts-=p[j][t-j+1];}counts-=i;}}
}//第一行的符号个数,n*(n+1)/4,当前"+"号个数,符号三角矩阵,已找到的符号三角形数
int main()
{cin>>n;half=n*(n+1)/2;//(1)if(half%2==1){cout<<"共有0个不同的符号三角形。"<
https://www.cnblogs.com/ECJTUACM-873284962/p/8447050.html#_label3_1
(8)健康的荷斯坦奶牛?
字典序最小:注意这里从1往0开始枚举,因为题目希望字典序最小
import java.util.Scanner;
public class Main {static int v;static int[] need;static int g;static int[][] have;static int[] select;static int min = Integer.MAX_VALUE;static int[] best;static int flag = 0;public static void main(String[] args) { Scanner in = new Scanner(System.in);v = in.nextInt();need = new int[v + 1];for(int i = 1;i <= v;i++) {need[i] = in.nextInt();}g = in.nextInt();select = new int[g + 1];best = new int[g + 1];for(int i = 1;i <= g;i++) {select[i] = 0;best[i] = 0;}have = new int[g + 1][v + 1]; for(int i = 1;i <= g;i++) {for(int j = 1;j <= v;j++) {have[i][j] = in.nextInt();}}dfs(1,0);System.out.print(min + " ");for(int i = 1;i <= g;i++) {if(best[i] == 1) {System.out.print(i);}if(i < g && best[i] == 1) {System.out.print(" ");}}}public static void dfs(int t,int num) {if(num >= min) return;if(t > g) {if(judge(select)) {if(num < min) {min = num;for(int i = 1;i <= g;i++) {best[i] = select[i];}}}}else {//1、多种情况,注意这里从1往0开始枚举(因为题目希望字典序最小,故前面的要先考虑先选)for(int i = 1;i >= 0;i--) {//2、处理select[t] = i;//3、判断if(i == 0) {dfs(t + 1,num);}else if(i == 1) {dfs(t + 1,num + 1);}//4、回复select[t] = 0;}}}public static boolean judge(int[] select) {for(int j = 1;j <= v;j++) {int t = 0;for(int i = 1;i <= g;i++) {if(select[i] == 1) {t += have[i][j];}}if(t < need[j]) {return false;}}return true;}}
(9)变换????
https://www.luogu.com.cn/problem/P1037
import java.util.Scanner;
public class Main {static int[][] rule = new int[10][10];static int result = 0;static int num;public static void main(String[] args) { Scanner in = new Scanner(System.in);int n = in.nextInt();int k = in.nextInt();for(int a = 0;a < k;a++) {int i = in.nextInt();int j = in.nextInt();rule[i][j] = 1;}num = get(n);}public static void dfs(int t) {if(t > num) {}else {}}public static boolean judge(int[] select) {for(int j = 1;j <= v;j++) {int t = 0;for(int i = 1;i <= g;i++) {if(select[i] == 1) {t += have[i][j];}}if(t < need[j]) {return false;}}return true;}public static int get(int n) {int o = 0;while(n != 0) {num++;n /= 10;}return o;}}
(10)盾神与砝码称重??
思路:
(1)每个砝码都可以选择放砝码盘、物品盘、不放(易错!)
(2)类似于0-1背包问题
https://www.dotcpp.com/oj/problem1548.html
回溯
import java.util.*;public class Main
{static int m = 0,n = 0;static int[] fa = new int[25];static boolean flag = false;static int w;static int sum = 0;public static void main(String args[]) {Scanner in = new Scanner(System.in);n = in.nextInt();m = in.nextInt();for(int i = 1;i <= n;i++) {fa[i] = in.nextInt();}for(int i = 0;i < m;i++) {w = in.nextInt();flag = false;dfs(1);if(flag) {System.out.println("YES");}else {System.out.println("NO");}}}public static void dfs(int t) {if(t > n) {if(sum == w) {flag = true;}}else {for(int i = 0;i < 3;i++) {if(i == 0) {dfs(t + 1);}if(i == 1) {sum += fa[t];dfs(t + 1);sum -= fa[t];}if(i == 2) {sum -= fa[t];dfs(t + 1);sum += fa[t];}}}}}
非回溯
import java.util.*;public class Main
{static int m = 0,n = 0;static int[] fa = new int[25];static boolean flag = false;static int w;public static void main(String args[]) {Scanner in = new Scanner(System.in);n = in.nextInt();m = in.nextInt();for(int i = 1;i <= n;i++) {fa[i] = in.nextInt();}for(int i = 0;i < m;i++) {w = in.nextInt();flag = false;dfs(0,1);if(flag) {System.out.println("YES");}else {System.out.println("NO");}}}public static void dfs(int now,int index) {if(now == w) {flag = true;return ;}if(index > n) {return ;}dfs(now + fa[index],index + 1);dfs(now - fa[index],index + 1);dfs(now,index + 1);}}
三、排序树问题
1、概念
当所给问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。例如旅行售货员问题(如下图)的解空间树是一棵排列树,这类排列树通常有n!个叶结点。遍历子集树的算法需Ω(n!)计算时间。
根为第一层,n个数,n+1 层,从第一到第n个进行
特别适用于排序 序列中
2、解题思路
for if swap 处理 递归 恢复
/*** * output(x) 树一次遍历结束,记录或输出得到的可行解x* constraint(t) 当前结点的题目约束函数* bount(t) 当前结点的数据限界函数* @param t t为当前解空间的层数* @param n n为条件给的个数*/void backtrack(int t){if(t > n){//进行当前求最值、是否符合题意等情况的判断,符合则记录或输出!!!output(x);}else{//第t层有(n-t+1)种交换方式for (int i = t; i <= n; i++) {//1、(判断准备工作)先判断再交换(最后计算)//判断(最优性剪枝和可行性剪枝、是否已访问等)if(constraint(t) && bount(t)){//交换swap(x[t], x[i]);//(最后计算)...backtrack(t+1);swap(x[t], x[i]);}//2、(判断准备工作)先交换再判断(最后计算)//swap(x[t], x[i]); //if (constraint(t)&&bound(t)) backtrack(t+1); //swap(x[t], x[i]); }}}backtrack(1) //第一层开始从头到尾
backtrack(2) //第一层固定(起点固定),从第二层开始
java swap()函数
public static void swap(int i,int j) {int t = x[i];x[i] = x[j];x[j] = t;}
3、技巧
(1)注意初始化
初始化序列干脆为1 2 3 4 …… n
(2)求最优记录输出要判断
(3)推荐使用顺序:判断-交换-计算
(4)为什么要swap呢
这个swap其实是“挪”向了同层的其他节点,但这个地方我们没有建树,直接通过在这条路径上进行swap得到。
譬如,一条12345路径,我们从1出发1->2->3->4->5
那我们backtrace(2)的时候会把2和345交换位置,就是为了获得1->3,1->4,1->5开头的路径。
(5)一般数组下标都是从1开始,便于根据题意操作
(6)由backpack(t+1)知:若当前为第t层,则上一节点在第(t-1)层为x[t-1],下一节点为x[i]
4、例题
(1)全排列问题
https://blog.csdn.net/long_long_int/article/details/79930888
非字典序 :swap
#include
using namespace std;
int n,a[100];
void dfs(int t)
{if(t>n){for(int i=1;i<=n;i++){cout<>n){for(int i=1;i<=n;i++){a[i]=i;//初始化a数组,第一次干脆定从小到大}dfs(1);}
}
字典序:子集树
#include
using namespace std;
int n,a[100],v[100];
//a数组用于保存每一次的排列,v数组用于判断数字是不是已经被选过
void dfs(int dp)//dp从1到n,执行n次,每次选择一个数
{if(dp>n)//dp为n+1的时候,就说明已经选了n个,可以输出了; {for(int i=1;i<=n;i++){cout<>n){dfs(1);}
}
(2)旅行商问题
起点已定,故从第2层开始即可
https://blog.csdn.net/lfb637/article/details/80635047
/**@回溯-旅行商(TSP)问题
*/
#include
#include
#define MAX 100
using namespace std;
int n; //城市个数
int a[MAX][MAX]; //城市间距离
int x[MAX]; //记录路径
int bestx[MAX] = {0}; //记录最优路径
int bestp = 63355; //最短路径长
int cp = 0; //当前路径长
void backpack(int t){if(t>n){//要能回去且距离仍最小if((a[x[n]][1])&&(a[x[n]][1]+cp>n; //顶点数//初始化序列for(int i = 1;i<=n;i++){x[i] = i;}cout<<"输入城市之间的距离(0表示城市间不通):"<>a[i][j];}}//起点已定,故从第2层开始即可backpack(2);cout<<"最少旅行费用为: "<
import java.util.Scanner;
public class Main {static int n;static int[][] a;static int[] x;static int now;static int[] r;static int min = Integer.MAX_VALUE;public static void main(String[] args) { Scanner in = new Scanner(System.in);n = in.nextInt();a = new int[n + 1][n + 1];r = new int[n + 1];x = new int[n + 1];for(int i = 1;i <= n;i++) {x[i] = i;}for(int i = 1;i <= n;i++) {for(int j = 1;j <= n;j++) {a[i][j] = in.nextInt();}}dfs(2);System.out.println(min);for(int i = 1;i <= n;i++) {System.out.print(r[i]);}System.out.print(r[1]);}public static void dfs(int t) {if(t > n) {if(a[x[n]][x[1]] != 0 && now + a[x[n]][x[1]] < min) {min = now + a[x[n]][x[1]];for(int i = 1;i <= n;i++) {r[i] = x[i];}}}else {for(int i = t;i <= n;i++) {if(a[x[t - 1]][x[i]] != 0 && now + a[x[t - 1]][x[i]] < min) {now += a[x[t - 1]][x[i]];swap(i,t);dfs(t + 1);swap(i,t);now -= a[x[t - 1]][x[i]];}}}}public static void swap(int i,int j) {int t = x[i];x[i] = x[j];x[j] = t;}}
(3)批处理作业调度问题
https://www.cnblogs.com/ECJTUACM-873284962/p/8447050.html#_label3_3
#include
using namespace std;
#define MAX 200
int* x1;//作业Ji在机器1上的工作时间
int* x2;//作业Ji在机器2上的工作时间
int number=0;//作业的数目
int* xorder;//作业顺序
int* bestorder;//最优的作业顺序
int bestvalue=MAX;//最优的时间
int xvalue=0;//当前完成用的时间
int f1=0;//机器1完成的时间
int* f2;//机器2完成的时间void backtrack(int t)
{if(t>number){if(xvalue < bestvalue){for(int i=1;i<=number;i++) bestorder[i]=xorder[i];bestvalue=xvalue;}}else{for(int i=t;i<=number;i++){//1、判断f1+=x1[xorder[i]];//计算该作业总时间f2[t]=(f2[t-1]>f1?f2[t-1]:f1)+x2[xorder[i]];//算入总时间xvalue+=f2[t];if(xvalue>number;x1=new int[number+1];x2=new int[number+1];xorder=new int[number+1];bestorder=new int[number+1];f2=new int[number+1];x1[0]=0;x2[0]=0;xorder[0]=0;bestorder[0]=0;f2[0]=0;cout<<"请输入每个作业在机器1上所用的时间:"<>x1[i];}cout<<"请输入每个作业在机器2上所用的时间:"<>x2[i];}for(i=1;i<=number;i++) xorder[i]=i;backtrack(1);cout<<"最节省的时间为:"<
(4)圆排列问题???
https://www.cnblogs.com/ECJTUACM-873284962/p/8447050.html#_label3_6
代码中圆排列的圆心横坐标以第一个圆的圆心为原点。所以,总长度为第一个圆的半径+最后一个圆的半径+最后一个圆的横坐标。
#include
#include
using namespace std;
#define MAX 200float minlen=10000,x[4],r[4];//当前最优值,当前圆排列圆心横坐标,当前圆排列
int n;//圆排列中圆的个数
//计算当前所选择圆的圆心横坐标
float center(int t)
{float temp=0;for(int j=1;jtemp) temp=valuex;}return temp;
}
//计算当前圆排列的长度
void compute()
{float low=0,high=0;for(int i=1;i<=n;i++){if(x[i]-r[i]high) high=x[i]+r[i];}if(high-lown) compute();else{for(int j=t;j<=n;j++){//为什么这样不可以??把计算放在前面了!//float centerx=center(t);//if(centerx+r[t]+r[1]
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
