【回溯】子集树和排列树(装载/最大团/n皇后/旅行商)
来源:互联网 发布:剑灵正太捏脸数据 编辑:程序博客网 时间:2024/06/05 18:07
之前一直分不清哪种递归属于回溯法,当然回溯法也不一定用递归来做,上了算法课有了一点感悟,记录一下。这四道题是算法的作业,在OJ上可以测试通过,感觉解空间这个概念真的是很帮助思考的一个东西。
解空间
解空间就是所有解的可能取值构成的空间, 一个解往往包含了得到这个解的每一步,往往就是对应解空间树中一条从根节点到叶节点的路径。
子集树和排列树都是一种解空间,它们不是真实存在的数据结构,也就是说并不是真的有这样一棵树,只是抽象出的解空间树。
约束条件和限界条件
约束条件Constraint是问题中就限定好的条件,比如在装载问题中装入第i个物体后不能超过背包总容量时才考虑装入它,即搜索左子树的情况。
限界条件Bound是需要自己挖掘的一个界,可能是上界或者下界,当问题中的某个值在走向某棵子树时会超出这个界限时,就可以放弃这棵子树了。
用这两个条件可以对解空间树进行剪枝,这样回溯法才有别于枚举。
子集树
当所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树称为子集树。子集树通常有2^n个叶结点(完全二叉树)。
回溯法搜索子集树的一般算法:
void backtrack(int i){ if(i>t) { //找到了一个解,记录一下 return ; } if(Constraint1() && Bound1())//左子树剪枝 { x[i]=1; //考虑搜索左子树 backtrack(i+1); //回退了!维护好 } if(Constraint0() && Bound0())//右子树剪枝 { x[i]=0; //考虑搜索右子树 backtrack(i+1); }return ;}
一般来说,子集树是一棵完全二叉树,这是因为子集往往只能取或者不取原集合中的元素,也就是取=1(左子树),不取=0(右子树)。
因为树的根节点只有一个,但从第一个物体开始就要考虑选择还是不选择,所以选择序列数组应该开t+1大小的,然后从1~t层(根节点算0层)即是考虑这个物体选择/不选择的层。
要注意搜索完左子树,往往要维护好这个i节点,因为相当于回退到了上一层,再去考虑是否要搜索右子树。
排列树
当所给的问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。排列树通常有n!个叶节点。也就是说n个元素的每一个排列都是解空间中的一个元素。
回溯法搜索排列树的一般算法:
void backtrack(int i){ if(i>t) { //找到了一个解,记录一下 return ; } else for(int j=i;j<=t;j++) { swap(x[j],x[i]); if(Constraint() && Bound())//剪枝 backtrack(i+1); swap(x[j],x[i]); }return ;}
对于i层及其后的所有层,都和第i层交换值然后再搜索交换后的下一层,并在回退后交换回来(维护好本层),这样能够搜索完全排列(可以想象一下2个节点的情况,3个节点的情况…)。
因为树的根节点只有一个,但从第一个位置开始就要考虑排这t个物品中的哪一个,所以选择序列数组应该开t+1大小的,然后从1~t层(根节点算0层)即是考虑这个位置放t个物品中的哪一个。
装载问题(子集树)
#include<iostream>using namespace std;int count=0;//全局计数int t;//箱子数int *w;//重量数组int c1,c2;//两艘船载重量int cw;//当前载重量int bestw;//当前找到过的最优成功载重量int *c;//当前的放入情况int *b;//当前找到过的最优的放入情况bool ok=false;void backtrack(int i)//回溯法:在一棵子集树解空间中深度搜索{ if(i>t)//超出子集树的叶节点了 { if(cw>bestw)//仅当这次搜索严格大于当前最优时 { bestw=cw;//才做更新(这样能保证一样的值取按字典序最大者) for(int j=1;j<=t;j++)//更新最优选择序列 b[j]=c[j]; ok=true;//记录找到过成功的解 } return ;//回退 } //考虑搜索左子树(即放入第i个的情况) if(cw+w[i]<=c1)//约束条件剪枝:不能超过c1载重量才可以放入i物品(走左子树) { c[i]=1;//记录当前第i个装进来了 cw+=w[i];//把当前载重量加进来 backtrack(i+1);//然后搜索下一层(左子树) cw-=w[i];//搜索完以后,要退回到本层,所以把刚才加上的左子树的重量减掉 //而w[i]是不需要动的,因为每次向下走一层时总是会按左走还是右走更新c[i] //也就是说目前c[i]里的值只是废的而已 } //考虑搜索右子树(即不放入第i个的情况) int sumend=0; for(int j=i+1;j<=t;j++) sumend+=w[j];//sumend里存的是从第i+1个到第t个全部放进来的重量 if(cw+sumend>bestw)//限界条件剪枝:如果剩下的全放进来都没法更重,就不用看右子树了 { c[i]=0;//记录第i个物体不放入(从第i-1层到第i层走了右子树) backtrack(i+1);//然后搜索下一层(右子树) }}int main(){ while(cin>>t) { w=new int[t+1];//分配空间,从1开始计数 c=new int[t+1]; b=new int[t+1]; int sum=0;//用于求总和 for(int i=1;i<=t;i++)//读入物体重量 { cin>>w[i]; sum+=w[i];//加到总和里 } cin>>c1>>c2;//读入两船载重量 //输入检查:如果c1+c2都没有所有物体加和大,不可能装得下 if(c1+c2<sum) { cout<<"Case "<<++count<<endl; cout<<"No"<<endl; //释放空间 delete[] w; delete[] c; delete[] b; continue;//直接进入下次循环 } //每次初始化 cw=bestw=0; ok=false; //从第0层向第1层开始搜索 backtrack(1); cout<<"Case "<<++count<<endl; if(true==ok && sum-bestw<=c2)//如果对于c1而言能找到ok的解,并且剩下的物体不超过c2容量 { cout<<bestw<<" "; for(int i=1;i<=t;i++) cout<<b[i];//把左1右0的子树选择序列输出,也就是c1的物品选择情况 cout<<endl; } else//否则,并不能放下 cout<<"No"<<endl; //释放空间 delete[] w; delete[] c; delete[] b; }return 0;}
最大团问题(子集树)
#include<iostream>using namespace std;int count=0;//全局计数char **v;//用来存二维邻接方阵int t;//方阵行列数int nown,maxn;//当前节点数和当前找到过的最大节点数bool *k;//选择序列:用来存搜索过程中的节点i是否放在了当前的图的节点集中void backtrack(int i)//回溯法:在一棵子集树解空间中深度搜索{ if(i>t)//超过叶节点层,说明找到了一个解 { if(nown>maxn)//看一下会不会更大 maxn=nown;//更大就更新一下 return ;//返回 } //考虑搜索左子树(即第i个节点放入的情况) //约束条件剪枝:放入第i个节点后所得的图仍然是个团(完全图),才搜索左子树 bool ok=true; for(int j=1;j<i;j++)//对i以前的每个节点j if(true==k[j] && '0'==v[i][j])//如果出现了i和在当前团中的节点j没有边的情况 { ok=false;//那么就不必考虑左子树了,因为放入i会破坏当前的'团'的性质 break; } if(true==ok)//如果可以搜索左子树,就搜索之 { k[i]=true;//记录第i个放入了 nown++;//团的规模+1 backtrack(i+1);//向下一层进发考虑 nown--;//当回退到本层时,因为后面还要考虑搜索右子树,团的规模要减回来(即此时还没放入第i个) } //考虑搜索右子树(即第i个节点不放入的情况) //边界条件剪枝:如果剩下的节点全都可以和当前构成团,都不会有更大的团,右子树就不必看了 if(t-i+nown>maxn)//仅当可能有更大的团时才做 { k[i]=false;//记录第i个不放入 backtrack(i+1);//向下一层进发考虑 }return ;//左右子树都考虑完,该回退到上一层了}int main(){ while(cin>>t) { //不妨从1开始计数,分配1~t的方阵空间 v=new char*[t+1]; for(int i=1;i<=t;i++) v[i]=new char[t+1]; //分配选择序列的空间 k=new bool[t+1]; //读入方阵 for(int i=1;i<=t;i++) for(int j=1;j<=t;j++) cin>>v[i][j]; /* for(int i=1;i<=t;i++) { for(int j=1;j<=t;j++) cout<<v[i][j]<<" "; cout<<endl; }*/ //初始化 nown=maxn=0; //从第0层向第1层开始搜索 backtrack(1); cout<<"Case "<<++count<<": "<<maxn<<endl; //释放空间 for(int i=1;i<=t;i++) delete[] v[i]; delete[] v; }return 0;}
n皇后问题(排列树)
//约束①行不相同:直接让i号皇后就保持在第i行//约束②列不相同:让t个x[i]的值都不相同,且都在1~t之间,因为只做swap()所以约束②也自然满足//约束③不在同一斜线:abs(x[i]-x[j])!=abs(i-j)即斜率不为1或-1,该约束需要手动剪枝#include<iostream>#include<algorithm>//含有swap()#include<cmath>//含有abs()using namespace std;int t;//每次输入的规模int *x;//x[i]表示i号皇后放在第i行的第x[i]列,这样自然地约束了①int total;//解的个数int c=0;bool Place(int i)//关于③的可行性约束:看从i以前的(算是对后续而言暂时摆好了的皇后)是否都符合③{ for(int k=1;k<i;k++)//对i以前的每个(它们之间肯定不用检查了,因为之前的层已经检查过了) if(abs(x[i]-x[k])==abs(i-k))//如果有和i号皇后在同一斜线的 return false;//说明i号皇后列在这里不可行,返回falsereturn true;//否则,i号皇后的列在这里是可行的}void backtrack(int i)//回溯法:在一棵排列树解空间中深度优先搜索{ if(i>t)//说明超过了叶节点,即找到了一个可行解 total++;//记录多了这个解 else//否则,还要继续往下搜索 for(int j=i;j<=t;j++)//对它和它后面行上的所有皇后 { swap(x[j],x[i]);//两个皇后列列交换,这样这个位置(这一层)上就有了前面层的基础上的所有可能列 if(true==Place(i))//手动剪枝,去掉不满足约束③的 backtrack(i+1);//向下一层进发考虑 swap(x[j],x[i]);//回退到这一层时,需要把列号交换回来 }return ;//返回上一层}int main(){ while(cin>>t) { x=new int[t+1];//分配t+1个空间,即从1开始标号 for(int i=1;i<=t;i++) x[i]=i;//初始化,第i个皇后放在第i行,这是一种符合①②的摆法 //初始化 total=0; backtrack(1);//从第1个皇后,即第1行开始搜索排列树解空间 cout<<"Case "<<++c<<": n="<<t<<endl; cout<<"Total: "<<total<<endl<<endl; delete[] x; }return 0;}
旅行商问题(排列树)
#include<iostream>#include<algorithm>using namespace std;int coun=0;//全局计数int t;//矩阵维度int **a;//邻接矩阵int *x;//x数组用来作排列树层次,存矩阵的游标int nowf;//当前费用int minf;//当前已经找到的最小费用bool Place(int i,int j)//可行性条件{ if(a[x[i-1]][x[j]]!=-1)//如果从i-1能进入j if(-1==minf || nowf+a[x[i-1]][x[j]]<minf)//限界条件:剪枝 return true;return false;}void backtrace(int i)//回溯法遍历排列树解空间{ if(i==t)//如果到了最后一层,那么要看看能不能回来(上一层并没有加上至第t层的值,更没有考虑回到第1层的事) { if(a[x[t-1]][x[t]]!=-1 && a[x[t]][x[1]]!=-1)//如果从第n-1层能到n层,并且从第n层能到第1层 if(-1==minf || nowf+a[x[t-1]][x[t]]+a[x[t]][x[1]]<minf)//如果还没找到过最小,或者当前比之前的还小 minf=nowf+a[x[t-1]][x[t]]+a[x[t]][x[1]];//更新最小 } else//如果没到最后一层 { for(int j=i;j<=t;j++)//对于i及其后的x里存的所有游标 if(true==Place(i,j))//如果满足可行性条件 { swap(x[j],x[i]);//交换,这样这个位置(这一层)上就有了前面层的基础上的所有可能 nowf+=a[x[i-1]][x[i]];//因为这次选择了x[i-1]->x[i]这条路,把它加进来 backtrace(i+1);//搜索下一层 nowf-=a[x[i-1]][x[i]];//回退以后减回来 swap(x[j],x[i]);//回退之后交换回来 } }return ;//返回上一层}int main(){ while(cin>>t) { //开空间:下标从1开始计数 a=new int*[t+1]; for(int i=1;i<=t;i++) a[i]=new int[t+1]; x=new int[t+1]; //初始化 nowf=0; minf=-1; for(int i=1;i<=t;i++) for(int j=1;j<=t;j++) cin>>a[i][j]; //就以第1个为起始点即可,毕竟要形成一个环,以谁开始都一样 for(int j=1;j<=t;j++) x[j]=j;//先1,2,3,4,..这样排列 //回溯法遍历排列树解空间 backtrace(2);//从1->2层开始考虑 //输出 cout<<"Case "<<++coun<<endl; if(-1==minf) cout<<"No loop"<<endl; else cout<<minf<<endl; //释放空间 delete[] x; for(int i=1;i<=t;i++) delete[] a[i]; delete[] a; }return 0;}
- 【回溯】子集树和排列树(装载/最大团/n皇后/旅行商)
- 回溯子集树与排列树——装载问题&旅行售货员问题(算法设计课题)
- 回溯算法:子集树和排列树
- 回溯之子集树和排列树
- 回溯法 求一个子集问题:装载问题、最大团问题
- 回溯问题+幂集、排列、子集和问题、八皇后问题
- 回溯法(子集树)----- 装载问题
- 回溯法——关于子集树和排列树
- n皇后,排列树
- 算法设计与分析实验四回溯法+子集树+最大团+0-1背包问题求解
- 回溯法:子集树与排列树
- 回溯法:子集树与排列树
- 递归回溯问题的四道经典题:N皇后,组合,全排列,二叉树路径和
- 回溯算法;双船装载问题;限界+约束;子集树;时间复杂度:O(2的n次方);
- c2java 回溯,下一个排列和子集和
- 子集树和排列树
- 子集树和排列树
- [算法之回溯法] 子集树与排列树
- 杂记
- 字符串的操作
- u3d_rpg游戏开发之物品管理(一)
- PAT 1038
- 深度学习网络调参技巧
- 【回溯】子集树和排列树(装载/最大团/n皇后/旅行商)
- Centos7.4安装Open-falcon v0.2
- 621. Task Scheduler
- 【python数据挖掘课程】十七.社交网络Networkx库分析人物关系(初识篇)
- VS KMP-串的模式匹配(五)
- 比较两个日期时间字符串
- 异常2
- 虚拟机局域网映射方法
- js模糊查找对象引用