第5章〓回溯法 5.1单项选择题及其参考答案 1. 回溯法是在问题的解空间中按策略从根结点出发搜索的。 A. 广度优先 B. 活结点优先 C. 扩展结点优先 D. 深度优先 答: 回溯法采用深度优先搜索在解空间中搜索问题的解。答案为D。 2. 下列算法中通常以深度优先方式搜索问题的解。 A. 回溯法 B. 动态规划 C. 贪心法 D. 分支限界法 答: 回溯法采用深度优先搜索在解空间中搜索问题的解,分支限界法采用广度优先搜索在解空间中搜索问题的解。答案为A。 3. 关于回溯法以下叙述中不正确的是。 A. 回溯法有通用解题法之称,可以系统地搜索一个问题的所有解或任意解 B. 回溯法是一种既带系统性又带跳跃性的搜索算法 C. 回溯法算法需要借助队列来保存从根结点到当前扩展结点的路径 D. 回溯法算法在生成解空间的任一结点时,先判断该结点是否可能包含问题的解,如果肯定不包含,则跳过对以该结点为根的子树的搜索,逐层向祖先结点回溯 答: 回溯算法是采用深度优先遍历的,需要借助栈保存从根结点到当前扩展结点的路径。答案为C。 4. 回溯法的效率不依赖于下列因素。 A. 确定解空间的时间 B. 满足显式约束的值的个数 C. 计算约束函数的时间 D. 计算限界函数的时间 答: 回溯法解空间是虚拟的,不必事先确定整个解空间。答案为A。 5. 下面是回溯法中为避免无效搜索采取的策略。 A. 递归函数 B. 剪支函数 C. 随机数函数 D. 搜索函数 答: 剪支函数包括约束函数(在扩展结点处剪除不满足约束条件的路径)和限界函数(剪去得不到问题解或最优解的路径)。答案为B。 6. 对于含n个元素的子集树问题(每个元素二选一),最坏情况下解空间树的叶子结点个数是。 A. n! B. 2n C. 2n+1-1 D. 2n-1 答: 这样的解空间树是一棵高度为n+1的满二叉树,叶子结点恰好有2n个。答案为B。 7. 用回溯法求解0/1背包问题时的解空间是。 A. 子集树 B. 排列树 C. 深度优先生成树 D. 广度优先生成树 答: 在0/1背包问题中每个物品是二选一(要么选中要么不选中),与物品的顺序无关,对应的解空间为子集树类型。答案为A。 8. 用回溯法求解0/1背包问题时最坏时间复杂度是。 A. O(n) B. O(nlog2n) C. O(n×2n) D. O(n2) 答: 0/1背包问题的解空间为一棵高度为n+1的满二叉树,结点个数为2n+1-1,最坏情况是搜索全部结点。答案为C。 9. 用回溯法求解TSP问题时的解空间是。 A. 子集树 B. 排列树 C. 深度优先生成树 D. 广度优先生成树 答: TSP问题的解空间属于典型的排列树,因为路径与顶点顺序有关。答案为B。 10. n个学生每个人有一个分数,求最高分的学生的姓名,最简单的方法是。 A. 回溯法 B. 归纳法 C. 迭代法 D. 以上都不对 答: 最简单的方法是依次迭代比较求最高分数。答案为C。 11. 求中国象棋中马从一个位置到另外一个位置的所有走法,采用回溯法求解时对应的解空间是。 A. 子集树 B. 排列树 C. 深度优先生成树 D. 广度优先生成树 答: 每一步马从相邻可走的位置中选择一个位置走下去。答案为A。 12. n个人排队在一台机器上做某个任务,每个人的等待时间不同,完成他的任务的时间是不同的,求完成这n个任务的最小时间,采用回溯法求解时对应的解空间是。 A. 子集树 B. 排列树 C. 深度优先生成树 D. 广度优先生成树 答: 该问题是求1~n的某个排列对应的n个任务完成的最小时间。答案为B。 5.2问答题及其参考答案 1. 回溯法的搜索特点是什么? 答: 回溯法的搜索特点是深度优先搜索+剪支。深度优先搜索可以尽快地找到一个解,剪支函数可以终止一些路径的搜索,提高搜索性能。 2. 有这样一个数学问题,x和y是两个正实数,求x+y=3的所有解,请问能否采用回溯法求解,如果改为x和y是两个均小于或等于10的正整数,又能否采用回溯法求解,如果能,请采用解空间画出求解结果。 答: 当x和y是两个正实数时,理论上讲两个实数之间有无穷个实数,所以无法枚举x和y的取值,不能采用回溯法求x+y=3的所有解。 当x和y是两个均小于或等于10的正整数时,它们的枚举范围是有限的,可以采用回溯法求x+y=3的所有解,采用剪支仅扩展x,y∈[1,2]的结点。解向量是(x,y),对应的解空间如图5.1所示,找到的两个解是(1,2)和(2,1)。 图5.1求x+y=3的解空间 3. 对于n=4,a=(11,13,24,7),t=31的子集和问题,利用左、右剪支的回溯法算法求解,给出求出的所有解,并且画出在解空间中的搜索过程。 答: 利用左、右剪支的回溯法算法求出两个解如下。 第1个解: 选取的数为11 13 7 第2个解: 选取的数为24 7 在解空间中的搜索过程如图5.2所示,图中每个结点为(cs,rs),其中cs为考虑第i个整数时选取的整数和,rs为剩余整数和。题中实例搜索的结点个数是11,如果不剪支,需要搜索31个结点。 图5.2子集和问题的搜索过程 4. 对于n皇后问题,通过解空间说明n=3时是无解的。 答: n=3时的解向量为(x1,x2,x3),xi表示第i个皇后的列号,对应的解空间如图5.3所示,所有的叶子结点均不满足约束条件,所以无解。 图5.33皇后问题的解空间 5. 对于n皇后问题,有人认为当n为偶数时其解具有对称性,即n皇后问题的解个数恰好为n/2皇后问题的解个数的两倍,这个结论正确吗? 答: 这个结论错误。因为两个n/2皇后问题的解合并起来不是n皇后问题的解。 6. 《教程》5.2.4节采用解空间为子集树求解n皇后问题,请问能否采用解空间为排列树的回溯框架求解?如果能,请给出剪支操作,说明最坏情况下的时间复杂度,按照最坏情况下的时间复杂度比较哪个算法更好? 答: 设n皇后问题的解向量为(x1,x2,…,xn),xi表示第i个皇后的列号,显然每个解一定是1~n的某个排列,所以可以采用解空间为排列树的回溯框架求解n皇后问题。其剪支操作是任何两个皇后不能同行、同列和同两条对角线。最坏情况下该算法的时间复杂度为O(n×n!),由于O(n!)好于O(nn),所以按照最坏情况下的时间复杂度比较,解空间为排列树的回溯算法好于解空间为子集树的回溯算法。实际上可以通过进一步剪支使得《教程》5.2.4节的算法的最坏时间复杂度达到O(n×n!)。 7. 对应如图5.4所示的无向连通图,假设颜色数m=2,给出m着色的所有着色方案,并且画出对应的解空间。 答: 这里n=4,顶点编号为0~3,m=2,颜色编号为0和1,解向量为(x0,x1,x2,x3),xi表示顶点i的着色,对应的解空间如图5.5所示,着色方案有两种,分别是(0,1,1,1)和(1,0,0,0)。 图5.4一个无向连通图 图5.5m着色问题的解空间 8. 有一个0/1背包问题,物品个数n=4,物品编号为0~3,它们的重量分别是3、1、2和2,价值分别是9、2、8和6,背包容量W=3。利用左、右剪支的回溯法算法求解,并且画出在解空间中的搜索过程。 答: 求解过程如下。 ① 4个物品按v/w递减排序后的结果如表5.1所示。 ② 从i=0开始搜索对应的解空间如图5.6所示。 图5.60/1背包问题的解空间 最后得到的最优解是选择编号为1和2的物品,总重量为3,总价值是10。 表5.14个物品按v/w递减排序后的结果 序号i物品编号no重量w价值vv/w 02284 10393 23263 31122 9. 以下算法用于求n个不同元素a的全排序,当a=(1,2,3)时,请给出算法输出的全排序的顺序。 int cnt=0;//累计排列的个数 void disp(vector&a)  //输出一个解 {printf(" 排列%2d: (",++cnt); for (int i=0;i& a,int i)  //递归算法 {int n=a.size(); if (i>=n-1)  //递归出口 disp(a); else {for (int j=n-1;j>=i;j--) {swap(a[i],a[j]);  //交换a[i]与a[j] dfs(a,i+1); swap(a[i],a[j]);  //交换a[i]与a[j]:恢复 } } } void perm(vector& a)  //求a的全排列 { dfs(a,0); } 答: 当a=(1,2,3)时调用perm(a)的输出结果及其顺序如下。 排列1: (1,3,2) 排列2: (1,2,3) 排列3: (2,1,3) 排列4: (2,3,1) 排列5: (3,1,2) 排列6: (3,2,1) 10. 假设问题的解空间为(x0,x1,…,xn-1),每个xi有m种不同的取值,所有xi取不同的值,该问题既可以采用子集树递归回溯框架求解,也可以采用排列树递归回溯框架求解,考虑最坏时间性能应该选择哪种方法? 答: 一般情况下,这样的问题采用子集树递归回溯框架求解时最坏时间复杂度为O(mn),采用排列树递归回溯框架求解时最坏时间复杂度为O(n!),如果m=2,由于O(2n)O(n!),采用后者较好。 11. 以下两个算法都是采用排列树递归回溯框架求解任务分配问题,判断其正确性,如果不正确,请指出其中的错误。 (1) 算法1: void dfs(vector&x,int cost,int i)//递归算法 {if (i>n)  //到达叶子结点 {if (cost&x,int cost,int i)  //递归算法 {if (i>n)  //到达叶子结点 {if (cost a={2,3,4,1,-1}; //存放所有整数 int n=a.size(),t=5; vector x;  //解向量,全局变量 int cs;  //累计选择的元素和 int sum;  //累计组合个数 void dfs(int cs,int i)  //递归回溯算法 {if (i>=n)   //到达一个叶子结点 {if(cs==t)  //找到一个解 {printf(" (%d): i=%d ",++sum,i); for (int j=0;j a={4,2,3,1,5};//存放所有整数 int n=5,t=9; vector bestx;  //最优解向量 int bestm=n;  //最多选择n个元素 void dfs(int cs,int rs,vector&x,int m,int i)   //递归算法 {if (i>=n)  //到达一个叶子结点 {if (cs==t)  //找到一个解 {if (m=t)  //右孩子结点剪支 {x[i]=0;  //不选取整数a[i] dfs(cs,rs,x,m,i+1); } rs+=a[i];  //恢复剩余整数和 } } void subs5()  //求解子集和问题 {bestx.resize(n); vector x(n);  //解向量 int rs=0;  //表示所有整数和 for (int j=0;j x;//解向量,全局变量 int k;  //累计选择的元素个数 int sum;  //累计组合个数 void dfs(vector&a,int m,int i)  //递归回溯算法 {if (i>=n)   //到达一个叶子结点 {if(k==m)  //找到一个解 {printf(" (%d): ",++sum); for (int j=0;j&a,int m)  //求a中m个元素的组合 {n=a.size(); x.resize(n);  //指定x的长度为n sum=0; k=0; dfs(a,m,0); } 4. 设计一个算法求1~n中m(m≤n)个元素的排列,要求每个元素最多只能取一次。例如,n=3,m=2的输出结果是(1,2),(1,3),(2,1),(2,3),(3,1),(3,2)。 解: 用解向量x=(x0,x1,…,xm-1)表示m个整数的排列,每个xi是1~n中的一个整数,并且所有xi不相同,i从0开始搜索,当i≥m时到达一个叶子结点,输出一个解,即一个排列。为了避免元素重复,设计used数组,其中used[j]=0表示没有选择整数j,used[j]=1表示已经选择整数i。对应的算法如下: int m,n; int x[MAXM];//解向量x[0..m-1]存放一个排列 bool used[MAXN]; int sum;  //累计解个数 void dfs(int i)  //递归算法 {if (i>=m) {printf(" (%d): ",++sum); for (int j=0;jn) disp(n);  //所有皇后放置结束时输出一个解 else {for (int j=1;j<=n;j++)  //在第i行上试探每一个列j {if(used[j]) continue;  //跳过前面已经放置皇后的列号 cnt++; if (place(i,j))  //在第i行上找到一个合适的位置(i,j) {q[i]=j; used[j]=true;  //列号j已经放置皇后 queen31(n,i+1); used[j]=false;  //回溯 q[j]=0; } } } } void queen3(int n)  //递归求解n皇后问题 {memset(used,false,sizeof(used)); queen31(n,1); } 例如,n=4时上述算法共调用place(i,j)算法32次,如果不改进则调用60次,n=6时从894次降低为356次。 6. 请采用基于排列树的回溯框架设计求解n皇后问题的算法。 解: 用q数组表示n皇后问题的一个解中所有皇后的列号,显然q一定是1~n的某个排列,所以可以采用基于排列树的回溯框架设计。对应的算法如下: int q[MAXN];//存放各皇后所在的列号,为全局变量 int cnt=0;  //累计解个数 void disp(int n)   //输出一个解 {printf("第%d个解:",++cnt); for (int i=1;i<=n;i++) printf("(%d,%d) ",i,q[i]); printf("\n"); } bool place(int i,int j)  //测试(i,j)位置能否摆放皇后 {if (i==1) return true;  //第一个皇后总是可以放置 int k=1; while (kn) disp(n);  //所有皇后放置结束 else {for (int j=i;j<=n;j++)  //在第i行上试探每一个列j {swap(q[i],q[j]);  //第i个皇后放置在q[j]列 if(place(i,q[i]))  //剪支操作 queen41(n,i+1); swap(q[i],q[j]);  //回溯 } } } void queen4(int n)  //用递归法求解n皇后问题 {for(int i=1;i<=n;i++) q[i]=i; queen41(n,1); } 7. 一棵整数二叉树采用二叉链b存储,设计一个算法求根结点到每个叶子结点的路径。 解: 这里的二叉树b看成解空间,所谓结点p的扩展就是搜索它的左、右孩子结点。解向量x存放根结点到一个叶子结点的路径(由于路径的长度可能不同,所以不同解的解向量的长度可能不同)。从根结点出发搜索到叶子结点,每次遇到一个叶子结点就输出x。对应的算法如下: vector x;//解向量 int sum;  //累计解个数 void dfs(TreeNode* b) {if (b->left==NULL && b->right==NULL)  //到达一个叶子结点 {printf(" (%d): ",++sum); for(int j=0;jleft!=NULL)  //结点b有左孩子 {x.push_back(b->left->val);  //在x的末尾添加b的左孩子结点值 dfs(b->left); x.pop_back();  //回溯 } if(b->right!=NULL)  //结点b有右孩子 {x.push_back(b->right->val);  //在x的末尾添加b的右孩子结点值 dfs(b->right); x.pop_back();  //回溯 } } } void allpath(TreeNode* b)  //求解算法 {if(b==NULL) return; x.push_back(b->val); dfs(b); } 8. 一棵整数二叉树采用二叉链b存储,设计一个算法求根结点到叶子结点的路径中路径和最小的一条路径,如果这样的路径有多条,求其中的任意一条。 解: 与第7题的解题思路类似,这里是求最短路径(最优解),增加sum表示当前路径x的路径和,bestx和bestsum用于存放最优解,分别是最优路径与最优路径和。在找到一个解时通过比较路径长度求最短路径。对应的算法如下: vector x;//解向量 int sum;  //路径和 vector bestx;  //最优解向量 int bestsum=INF;  //最短路径和 void dfs(TreeNode* b) {if (b->left==NULL && b->right==NULL)  //到达一个叶子结点 {if(sumleft!=NULL)  //结点b有左孩子 {x.push_back(b->left->val);  //在x的末尾添加b的左孩子结点值 sum+=b->left->val; dfs(b->left); sum-=b->left->val; x.pop_back();  //回溯 } if(b->right!=NULL)  //结点b有右孩子 {x.push_back(b->right->val);  //在x的末尾添加b的右孩子结点值 sum+=b->right->val; dfs(b->right); sum-=b->right->val; x.pop_back();  //回溯 } } } void minpath(TreeNode* b)  //求解算法 {if(b==NULL) return; x.push_back(b->val); sum=b->val; dfs(b); printf(" 最短路径: "); for(int j=0;j x;//解向量 void dfs(TreeNode* b) {if (b->left==NULL && b->right==NULL)//到达一个叶子结点 {printf(" %d的编码: ",b->val); for(int j=0;jleft!=NULL)  //结点b有左孩子结点 {x.push_back(0);  //在x的末尾添加0 dfs(b->left); x.pop_back();  //回溯 } if(b->right!=NULL)  //结点b有右孩子结点 {x.push_back(1);  //在x的末尾添加1 dfs(b->right); x.pop_back();  //回溯 } } } void leafcode(TreeNode* b)  //求解算法 {if(b==NULL) return; dfs(b); } 10. 假设一个含n个顶点(顶点编号为0~n-1)的不带权图采用邻接矩阵A存储,设计一个算法判断其中顶点u到顶点v是否有路径。 解: 将从图中顶点u出发的全部搜索看成解空间,所谓顶点u的扩展就是搜索它相邻的尚未访问的顶点。用flag变量表示u到v是否有路径(看成解向量,初始设置为false),解空间中的起始点u对应根结点,叶子结点对应顶点v。从顶点u出发搜索,为了避免在一条路径中重复访问顶点,设置visited数组(初始时所有元素置为0),visited[j]=0表示顶点j未访问,visited[j]=1表示顶点j已访问。由于是从根结点开始搜索其相邻顶点的,所以必须先置visited[u]=1,当访问到顶点v时说明u到v有路径,置flag为true,一旦flag为true,便终止后面的结点扩展。采用深度优先搜索的回溯算法如下: vector> A={{0,1,1,0,0},{1,0,1,1,0},{1,1,0,1,0},{0,1,1,0,1},{0,0,0,1,0}}; int n=5; vector visited(n,0);//访问标记 bool flag;  //表示u到v是否有路径 void dfs(int u,int v) {if (u==v)  //到达顶点v flag=true; else if(!flag)  //没有到达顶点v且flag为假 {for(int j=0;j> A={{0,1,1,0,0},{1,0,1,1,0},{1,1,0,1,0},{0,1,1,0,1},{0,0,0,1,0}}; int n=5; vector x;//解向量(路径) int sum=0;  //路径数 vector visited(n,0); void dfs(int u,int v) {if (u==v)  //到达顶点v {printf(" (%d): ",++sum); for(int j=0;j> A={{0,1,5,0,0},{1,0,2,4,0},{5,2,0,1,0},{0,4,1,0,2},{0,0,0,2,0}}; int n=5; vector x;//解向量 int len=0;  //路径长度 vector bestx;  //最优解向量 int bestlen=INF;  //最优路径长度 vector visited(n,0); void dfs(int u,int v) {if (u==v)  //到达顶点v {if(len #include using namespace std; vector used(10,0);//used[j]表示数字j是否已使用 int sum;  //累计解个数 void dfs(vector x,int n,int i)  //递归回溯算法 {if(i>=n) {int m=x[0]*1000+x[1]*100+x[2]*10+x[3]; int n=x[0]*1000+x[1]*100+x[4]*10+x[3]; int s=x[4]*10000+x[3]*1000+x[2]*100+x[0]*10+x[3]; if (m+n==s)  //找到一个可行解 {printf(" 第%d个解 ",++sum); printf("兵:%d 炮:%d 马:%d 卒:%d 车:%d\n",x[0],x[1],x[2],x[3],x[4]); } return; } else {for(int j=0;j<=9;j++) {if(used[j]==0) {x[i]=j; used[j]=1; dfs(x,n,i+1); x[i]=-1; used[j]=0; } } } } void piece()  //求解算法 {int n=5; vector x(n);  //定义解向量 sum=0; dfs(x,n,0); } int main() {printf("实验结果\n "); piece(); printf("共%d个解\n",sum); return 0; } 上述程序的执行结果如图5.8所示。 图5.8exp51实验程序的执行结果 5.4.2子集和 编写一个实验程序exp52,给定含n个正整数的数组a和一个整数t,如果a中存在若干个整数(至少包含一个整数)的和恰好等于t,说明有解,否则说明无解。要求采用相关数据进行测试。 解: 相关原理见《教程》5.2.1节中求解子集和问题的算法。这里仅判断是否有解,用flag变量表示是否有解(初始设置为false),cnt变量表示解中选取的整数个数(初始为0),cs为当前选取的整数和,当到达一个叶子结点(i≥n)时,若cs==t && cnt>=1成立说明有解,置flag为true,一旦flag为true便终止结点的扩展,全部搜索完毕flag仍然为false说明无解。对应的实验程序如下: #include #include using namespace std; int n=4,t; vector a={11,13,24,7};//存放所有整数 int cnt;  //解中选取的整数个数 bool flag;  //是否存在解 void dfs(int cs,int rs,int i)   //递归回溯算法 {if (i>=n)  //找到一个叶子结点 {if (cs==t && cnt>=1)  //找到一个满足条件的解,置flag为true flag=true; } else if(!flag)  //没有到达叶子结点并且flag为假 {rs-=a[i];  //求剩余的整数和 if (cs+a[i]<=t)  //左孩子结点剪支 {cnt++; dfs(cs+a[i],rs,i+1); cnt--;  //回溯 } if (cs+rs>=t)  //右孩子结点剪支 dfs(cs,rs,i+1); rs+=a[i];  //恢复剩余整数和 } } bool subs()  //求解子集和问题 {int rs=0;  //表示所有整数和 for (int j=0;j #include using namespace std; #define INF 0x3f3f3f3f int n=4; int m=4; vector> A={{0,0,0,1},{0,1,0,0},{0,0,0,1},{1,0,0,0}}; int dx[]={0,0,1,-1};//水平方向的偏移量 int dy[]={1,-1,0,0}; //垂直方向的偏移量 vector> visited(m,vector(n,0)); struct Box  //方块类型 {int x; int y; Box(int x1,int y1): x(x1),y(y1) {}  //构造函数 }; vector x;  //解向量 int len;  //解向量表示的路径长度 vector bestx;  //最优解向量 int bestlen=INF;  //最优解向量表示的路径长度 int sum;  //表示路径数 void disp()  //输出一条迷宫路径 {printf(" 路径%d: ",++sum); for (int j=0;j10) return; if(s.x==t.x && s.y==t.y) {disp(); if(len=m || ny<0 || ny>=n) continue;  //跳过超界的方块 if(A[nx][ny]==1) continue;  //跳过障碍物 if(visited[nx][ny]==1) continue;   //跳过已经访问的方块 visited[nx][ny]=1;  //访问(nx,ny) len++; Box b(nx,ny); x.push_back(b);  //(nx,ny)添加到路径中 dfs(b,t); x.pop_back();  //回溯 len--; visited[nx][ny]=0; } } } void mgallpath(Box& s,Box& t)  //求解算法 {x.push_back(s);  //入口s添加到路径中 visited[s.x][s.y]=1;  //标记入口s已访问 len=0; dfs(s,t); } int main() {Box s(0,0); Box t(3,3); printf("实验结果\n"); printf(" 从[%d,%d]到[%d,%d]的全部路径\n",s.x,s.y,t.x,t.y); mgallpath(s,t); printf("一条最短路径: "); for (int j=0;j #include using namespace std; int n=5;//图中顶点个数 vector> A={{0,1,1,1,0},{1,0,0,1,1},{1,0,0,0,1},{1,1,0,0,1},{0,1,1,1,0}}; int cnt=0;  //累计路径条数 void disp(vector&x,int d,int s)  //输出一个解 {printf("第%d条回路: ",++cnt); for (int j=0;j",x[j]); printf("%d\n",s);  //末尾加上起点s } void dfs(vector&x,int d,int s,int i)  //回溯法 {if(i>=n)  //到达一个叶子结点 {if(A[x[n-1]][s]==1)  //若x[n-1]到s有边 disp(x,d,s); } else  //没有到达叶子结点 {for(int j=i;j x;  //定义解向量 x.push_back(s); for(int i=0;i> combinationSum3(int k,int n) {} }; 解: 用类变量ans存放结果,相关剪支原理见《教程》5.2.1节中求解子集和问题的算法,这里不再讨论,改为用解向量x存放选取的所有整数,每个分量的取值范围是1~9,cnt表示选取的整数个数,i从1开始(对应解空间的根结点),每个整数i只有选取和不选取两种可能,当到达一个叶子结点(i≥10)时,如果cs=N并且cnt=K表示找到一个解,将x添加到ans中。对应的程序如下: class Solution { vector> ans; int N,K; public: vector> combinationSum3(int k,int n) {N=n; K=k; int rs=0;//rs表示所有整数和 for (int j=1;j<=9;j++)  //求rs rs+=j; int cnt=0; vector x; dfs(0,rs,x,cnt,1);  //i从1开始 return ans; } void dfs(int cs,int rs,vector& x,int cnt,int i) //递归回溯算法 {if (i>=10)  //找到一个叶子结点 {if (cs==N && cnt==K)  //找到一个满足条件的解 ans.push_back(x); } else  //没有到达叶子结点 {rs-=i;  //求剩余的整数和 if (cs+i<=N)  //左剪支 {x.push_back(i);  //选取整数i cnt++; dfs(cs+i,rs,x,cnt,i+1); cnt--;  //回溯 x.pop_back(); } if (cs+rs>=N)  //右剪支 dfs(cs,rs,x,cnt,i+1); rs+=i;  //恢复剩余整数和 } } }; 上述程序的提交结果为通过,执行时间为0ms,内存消耗为6.5MB。实际上由于测试数据比较少,即使不采用任何剪支,执行时间也只有4ms。没有任何剪支的程序如下: class Solution { vector> ans; int N,K; public: vector> combinationSum3(int k,int n) {N=n; K=k; int cnt=0; vector x; dfs(0,x,cnt,1);  //i从1开始 return ans; } void dfs(int cs,vector& x,int cnt,int i) //递归回溯算法 {if (i>=10)  //找到一个叶子结点 {if (cs==N && cnt==K)  //找到一个满足条件的解 ans.push_back(x); } else  //没有到达叶子结点 {x.push_back(i);  //选取整数i cnt++; dfs(cs+i,x,cnt,i+1); cnt--;  //回溯 x.pop_back(); dfs(cs,x,cnt,i+1);  //不选取整数i } } }; 5.5.2LeetCode39——组合总和 问题描述: 给定一个无重复元素的数组a和一个目标数 t,找出a中所有可以使数字和为t的组合。a 中的数字可以无限制被重复选取,其中所有数字(包括t)都是正整数。例如,a={2,3,6,7},t=7的结果为{{7},{2,2,3}},而a={2,3,5},t=8的结果为{{2,2,2,2},{2,3,3},{3,5}}。要求设计如下函数: class Solution { public: vector> combinationSum(vector& a,int t) { } }; 解: 与LeetCode216题目类似,但这里a中每个元素可以重复选取,同样用解向量x存放选取的所有整数,用i从0开始遍历a,增加一个剩余数的参数rt(rt为t与当前选取的整数和的差,初始值为t)。当搜索第i层的一个结点时,求出cnt=rt/a[i](表示最多可以选取cnt个a[i]),这样a[i]的选取分为cnt+1种情况: 不选取a[i],选取一个a[i],选取两个a[i],…,选取cnt个a[i],如图5.14所示。当i≥N(对应解空间的一个叶子结点)并且rt=0时对应一个解x,将x添加到ans中。 图5.14a[i]元素是在cnt+1种情况中选一 另外也可以将满足rt=0的结点作为一个解,剪去i≥N或者rt<0的分支,或者说仅扩展满足i0的结点。对应的程序如下: class Solution { vector> ans; int N; public: vector> combinationSum(vector& a,int t) {N=a.size(); vector x; dfs(a,t,x,0);//i从0开始 return ans; } void dfs(vector&a,int rt,vector& x,int i)   //递归回溯算法 {if (rt==0)  //找到一个叶子结点 ans.push_back(x); else if(i0) {dfs(a,rt,x,i+1);  //不选择a[i] int cnt=0; for(int j=1;a[i]*j<=rt;j++)  //枚举a[i]可以选取的次数 {cnt++; x.push_back(a[i]);  //包含a[i]选取1,2,…,cnt次 dfs(a,rt-a[i]*j,x,i+1); } for(int j=0;j> partition(string s) {} }; 解: 用ans存放所有分割方案。i从0开始,找到s[i..j]的每一个回文的终止位置j,所以该问题类似子集和问题,假设有k个这样的j,扩展就是在k个情况中选取一个,再从j+1位置开始继续搜索。解向量x存放一个分割方案,当i≥n时表示找到了s的一个解,将x添加到ans中,最后返回ans。对应的程序如下: class Solution { vector> ans;//存放所有方案 public: vector> partition(string s) {vector x; dfs(s,x,0); return ans; } void dfs(string s,vector& x,int i)  //从i位置开始分割回文 {int n=s.size(); if(i>=n)  //找到一个解 ans.push_back(x); else {for(int j=i;j #include using namespace std; #define INF 0x3f3f3f3f #define MAXN 10005 int x[MAXN];//存放一个排列 bool used[MAXN];  //used[j]表示j是否使用过 int cnt; bool flag; bool permutation(int i,int n,int k)  //回溯算法 {if(i==n+1)  //到达一个叶子结点 {cnt++; if(cnt==k)  //找到第k个解时返回true return true; } else {for(int j=1;j<=n;j++) {if(!used[j])  //整数j没有使用过 {x[i]=j; used[j]=true;  //标记j已经使用 if (permutation(i+1,n,k)) return true; used[j]=false;  //回溯 } } } return false; } int main() {int n,k; while(scanf("%d%d",&n,&k)!=EOF) {flag=false; memset(used,false,sizeof(used)); cnt=0; for(int i=1;i<=n;i++)  //初始化 x[i]=i; permutation(1,n,k); for(int i=1;i<=n;i++)  //输出结果 {if(i==1) printf("%d",x[i]); else printf(" %d",x[i]); } printf("\n"); } return 0; } 上述程序的提交结果为通过,执行时间为452ms,内存消耗为1776KB。 5.5.5HDU2553——n皇后问题 问题描述: 在n×n个方格的棋盘上放置了n个皇后,使得它们不相互攻击(即任意两个皇后不允许处在同一排,同一列,也不允许处在与棋盘边框成45°角的斜线上)。对于给定的n,请求出有多少种合法的放置方法。 输入格式: 共有若干行,每行一个正整数n(n≤10),表示棋盘和皇后的数量,如果n=0表示结束。 输出格式: 共有若干行,每行一个正整数,表示对应输入行的皇后的不同放置数量。 输入样例: 1 8 5 0 输出样例: 1 92 10 解: 采用《教程》5.2.4节中求n皇后问题的思路,仅将输出全部解改为输出解的个数。另外,这里n最大为10,但测试用例个数可能超过10,所以先求出1~10皇后问题,将解个数存放在ans数组中,当输入的皇后个数为n时直接输出ans[n]即可。采用基于子集树递归函数框架的程序如下: #include #include using namespace std; #define MAXN 12 int q[MAXN];//存放各皇后所在的列号,为全局变量 int cnt;  //累计解个数 bool place(int i,int j)  //测试(i,j)位置能否摆放皇后 {if (i==1) return true;  //第一个皇后总是可以放置 int k=1; while (kn) cnt++;  //解个数增加1 else {for (int j=1;j<=n;j++)  //在第i行上试探每一个列j {if (place(i,j))  //在第i行上找到一个合适的位置(i,j) {q[i]=j; queen(n,i+1); q[i]=0;  //回溯 } } } } int main() {int ans[MAXN];  //ans[n]存放n皇后问题的解个数 for(int n=1;n<=10;n++) {cnt=0; queen(n,1); ans[n]=cnt; } int n; while(~scanf("%d",&n) && n) printf("%d\n",ans[n]); return 0; } 上述程序的提交结果为通过,执行时间为31ms,内存消耗为1372KB。当然也可以采用基于排列树的递归回溯框架求解,对应的程序如下: #include #include using namespace std; #define MAXN 12//最多皇后个数 int q[MAXN];  //存放各皇后所在的列号,为全局变量 int cnt;  //累计解个数 bool place(int i,int j)  //测试(i,j)位置能否摆放皇后 {if (i==1) return true;  //第一个皇后总是可以放置 int k=1; while (kn) cnt++;  //解个数增加1 else {for (int j=i;j<=n;j++)  //在第i行上试探每一个列j {swap(q[i],q[j]);  //第i个皇后放置在q[j]列 if(place(i,q[i]))  //剪支操作 queen(n,i+1); swap(q[i],q[j]);  //回溯 } } } int main() {int ans[MAXN]; for(int n=1;n<=10;n++) {cnt=0; for(int i=1;i<=n;i++) q[i]=i; queen(n,1); ans[n]=cnt; } int n; while(~scanf("%d",&n) && n) printf("%d\n",ans[n]); return 0; } 上述程序的提交结果为通过,执行时间为15ms,内存消耗为1740KB。 5.5.6HDU2616——杀死怪物 问题描述: YF的家乡附近有一座山,山上住着一个大怪物,YF想要消灭它。YF有n个技能,怪物的血量为m,当血量小于或等于0时怪物被消灭。YF的技能在不同的时间使用时有不同的效果。现在告诉你每个技能的效果,用(A,M)描述,A为该技能在普通时间内使用时消耗怪物的血量,M表示当怪物的血量小于或等于M时使用该技能可以获得双倍效果。每种技能最多只能使用一次。 输入格式: 输入包含多个测试用例。每个测试用例的第一行是两个整数n和m(2 #include using namespace std; #define MAXN 15 #define INF 0x3f3f3f3f int n; int bestx;//bestx表示最优解 struct Spell  //技能的类型 {int a,b; int used;  //标记该技能是否已经使用过 } s[MAXN]; void dfs(int rm,int i) {if(rm<=0)   //怪物被消灭对应叶子结点 bestx=min(bestx,i);  //比较求最小值 else {for(int j=0;j> n >> m) {for(int i=0;i> s[i].a >> s[i].b; s[i].used=0; } bestx=INF; dfs(m,0); if(bestx==INF) cout << -1 << endl; else cout << bestx << endl; } return 0; } 上述程序的提交结果为通过,执行时间为124ms,内存消耗为1800KB。 5.5.7POJ3187——向后数字和 问题描述: 给定一个1~n的某个排列,将相邻数字相加以生成一个少一个数字的新列表,重复这个操作,直到只剩下一个数字x为止。例如,若给定一个n=4的排列为3,1,2,4,第一次相邻数字相加的结果是4,3,6,第2次相邻数字相加的结果是7,9,第3次相邻数字相加的结果是16,那么该排列产生的向后数字和x=16。不同排列的向后数字和可能不同。 输入格式: 输入有多个测试用例,每个测试用例是两个由空格分隔的整数n和sum。 输出格式: 对于每个测试用例,求出其向后数字和等于sum的某个1~n的排列,如果有多个这样的排列,请选择按字典顺序排列最小的一个。 输入样例: 4 16 输出样例: 3 1 2 4 提示: 另外一个满足要求的答案是3,2,1,4,但3,1,2,4是最小的。 解: 题目就是枚举1~n的排列,求出其向后数字和x,若x=sum,则输出该排列。求1~n的全排列可以采用基于排列树的递归算法框架,对应的程序如下: #include #include using namespace std; #define MAXN 12 int n,sum; vector a; vector ans; bool judge(vector b)//判断b序列是否满足条件 {for (int i=n-1;i>=1;i--) {for (int j=0;j<=i-1;j++) b[j]=b[j]+b[j+1]; } if (b[0]==sum) return true; else return false; } bool flag; void dfs(vector &a,int i)  //递归算法 {if (i>=n)  //到达一个叶子结点 {if(judge(a)) {ans=a; flag=true; } } else if(!flag)  //没有到达叶子结点且flag为false {for (int j=i;j> n >> sum) {a.resize(n); for (int j=0;j #include #include using namespace std; int n,sum; bool judge(vector b)//判断b序列是否满足条件 {for (int i=n-1;i>=1;i--) {for (int j=0;j<=i-1;j++) b[j]=b[j]+b[j+1]; } if (b[0]==sum) return true; else return false; } int main() {while (cin >> n >> sum) {vector a(n); for (int j=0;j #include using namespace std; #define MAXN 12 char map[MAXN][MAXN]; int n,k; int cnt;//方案个数 int used[MAXN];  //used[j]表示第j列是否放了棋子 void queen(int tot,int i)  //回溯算法 {if(i>=n) {if (tot==k)   //棋子的个数恰好为k cnt++;  //解的个数增加1 } else {queen(tot,i+1);  //第i行不放棋子 for (int j=0;j #include using namespace std; #define MAXN 33 int dx[]={-1,1,-2,2,-2,2,-1,1};//x方向的偏移量 int dy[]={-2,-2,-1,-1,1,1,2,2};  //y方向的偏移量 int n,m; struct Box  //方块类型 {int x,y; Box() {} Box(int x,int y):x(x),y(y) {}  //构造函数 } ans[MAXN*MAXN]; int visited[MAXN][MAXN]; bool flag; void dfs(int x,int y,int cnt)  //从(x,y)出发搜索路径 {if(flag) return;  //找到一条路径后返回 if(cnt==n*m)  //找到一个解后输出 {flag=true; for(int i=1;i<=cnt;i++) printf("%c%d",ans[i].y+'A',ans[i].x+1); printf("\n"); } else {for(int di=0;di<8;di++)  //试探8个方位 {int nx=x+dx[di]; int ny=y+dy[di]; if(nx<0 || nx>=n || ny<0 || ny>=m)  //(nx,ny)超界时跳过 continue; if(visited[nx][ny])  //(nx,ny)已访问时跳过 continue; visited[nx][ny]=1; ans[cnt+1]=Box(nx,ny); dfs(nx,ny,cnt+1); visited[nx][ny]=0;  //回溯,为什么ans不必回退 } } } int main() {int t; scanf("%d",&t); for(int no=1;no<=t;no++) {if(no!=1) printf("\n"); scanf("%d%d",&n,&m); memset(visited,0,sizeof(visited)); flag=false; printf("Scenario #%d:\n",no); for(int i=0;is,则说明不满足约束条件,不能执行订单i。 使用besttot表示最大总收入(初始为0),当前总收入用tot表示。当i≥n时表示当前方案的总收入为tot,比较将最大值存放到besttot,最后输出besttot即可。对应的程序如下: #include #include #include using namespace std; #define MAXN 25 int s,m,n; struct Orders//车站订单类型 {int start;  //起点站 int dest;  //终点站 int peop;  //乘客人数 int mon;  //该订单的价格 } order[MAXN]; int cnt[MAXN];  //cnt[i]表示到达车站i的总乘客人数 int besttot;  //最优解 void dfs(int tot,int i) {if(i>=n) besttot=max(besttot,tot); else {dfs(tot,i+1);  //不考虑订单i bool flag=true;  //考虑订单i for(int j=order[i].start+1;j<=order[i].dest;j++)//处理订单i {cnt[j]+=order[i].peop; if(cnt[j]>s)  //不能执行该订单 flag=false; } if(flag)  //如果能够执行该订单 dfs(tot+order[i].mon,i+1);  //执行订单i for(int j=order[i].start+1;j<=order[i].dest;j++)//回溯 cnt[j]-=order[i].peop; } } int main() {while(cin >> s >> m >> n) {if(s==0 && m==0 && n==0) break; memset(cnt,0,sizeof(cnt)); besttot=0; for(int i=0;i>order[i].start>>order[i].dest>>order[i].peop; order[i].mon=(order[i].dest-order[i].start)*order[i].peop;  //求订单的价格 } dfs(0,0); cout << besttot << endl; } } 上述程序提交时通过,执行时间为625ms,消耗的空间为212KB,满足时空要求。 5.5.11POJ1129——最少频道数 问题描述: 现在需要在一个非常大的区域建立一个广播电台,想让该区域都能够接收到信号,必须建立一些用于转播信号的中继器,但是相邻的两个中继器的频道必须不同,否则会相互干扰,不相邻的中继器可以使用相同的频道。给定一个中继网络,要求求出所需要的最少不同频道数。 输入格式: 输入包含多个测试用例,每个测试用例的第一行是中继器的个数n,接下来共n行,每一行表示一个中继器的邻接关系,例如A:BCDH表示中继器A与中继器B、C、D和H相邻。注意相邻是对称关系,如果A与B相邻,则B必然与A相邻。中继器采用大写字母表示,邻接关系按字母顺序列出,中继器的个数n最多为26个。以输入n=0表示结束。 另外,由于中继器位于一个平面内,所以连接相邻中继器形成的图形没有任何交叉的线段。 输出格式: 对于每个测试用例,输出一行表示所需的最少频道数。示例输出显示了该行的格式。当只需要一个频道时,注意频道是单数形式。 输入样例: 2 A: B: 4 A:BC B:ACD C:ABD D:BC 4 A:BCD B:ACD C:ABD D:ABC 0 输出样例: 1 channel needed. 3 channels needed. 4 channels needed. 解: 由输入建立一个无向图的邻接表,每个顶点表示一个中继器,采用《教程》5.2.7节中图的m着色算法的思路,每种不同的频道就是一种不同的颜色,题目就是求使得图中所有相邻顶点着上不同颜色所需要的最少颜色数。但这里没有给出颜色数m,并且要求最小的m(最优解是最小值),属于问题Ⅱ类型。 相关变量设计与m着色算法的相同,仅增加bestm表示最小的m,根据四色定理,任意这样的图最多4种颜色即可着色。让m从1到4循环,从顶点0开始执行dfs回溯算法,若cnt>0说明找到了最小的m,置bestm,最后输出bestm。对应的程序如下: #include #include #include using namespace std; #define INF 0x3f3f3f3f//表示∞ #define MAXN 30  //最多顶点个数 int n; int x[MAXN]; int bestm; int cnt;  //全局变量,累计解个数 vector A[MAXN];  //邻接表 bool judge(int i,int j)  //判断顶点i是否可以着上颜色j {for(int k=0;k=n)  //到达一个叶子结点 cnt++; else if(cnt==0)  //如果cnt>0就没有必要继续了 {for(int j=0;j0) {bestm=m; break;  //一旦找到就结束循环 } } if(bestm==1)  //按题目要求输出结果 printf("1 channel needed.\n"); else printf("%d channels needed.\n",bestm); } return 0; } 上述算法需要调用bestm次dfs算法,也就是说若bestm=4,需要依次独立地调用dfs(1,0),dfs(2,0),dfs(3,0),dfs(4,0),这样性能比较低,可以改为从m=1开始,当没有找到合适的着色时置m++继续搜索,这样调用一次dfs即可。对应的改进算法如下: #include #include #include using namespace std; #define INF 0x3f3f3f3f//定义为∞ #define MAXN 30  //最多顶点个数 int n; int x[MAXN]; int bestm; vector A[MAXN];  //邻接表 bool judge(int i,int j)  //判断顶点i是否可以着颜色j {for(int k=0;k=n)  //到达一个叶子结点 bestm=min(bestm,m); else {for(int j=0;j