第5章〓回溯法 5.1本章知识结构 本章主要讨论回溯法的概念、回溯算法搜索解的过程、利用剪支函数提高搜索效率、基于子集树和排列树的算法框架及其经典应用示例,其知识结构如图5.1所示。 图5.1本章知识结构图 5.2《教程》中的练习题及其参考答案 1. 简述回溯算法中主要的剪支策略。 答: 回溯算法中主要的剪支策略如下。 (1) 可行性剪支: 在扩展结点处剪去不满足约束条件的分支。例如,在0/1背包问题中,如果选择物品i会导致总重量超过背包容量,则终止选择物品i的分支的继续搜索。 (2) 最优性剪支: 用限界函数剪去得不到最优解的分支。例如,在0/1背包问题中,如果沿着某个分支走下去无论如何都不可能得到比当前解bestv更大的价值,则终止该分支的继续搜索。 2. 简述回溯法中常见的两种类型的解空间树。 答: 回溯法中常见的两种类型的解空间树是子集树和排列树。 当给定的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树称为子集树,这类子集树通常有2n个叶子结点,遍历子集树需要O(2n)的时间。 当给定的问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。这类排列树通常有n!个叶子结点,遍历排列树需要O(n!)的时间。 3. 鸡兔同笼问题是一个笼子里面有鸡和兔子若干只,数一数,共有a个头、b条腿,求鸡和兔子各有多少只?假设a=3,b=8,画出对应的解空间树。 答: 用x和y分别表示鸡和兔子的数目,显然鸡的数目最多为min(a,b/2),兔的数目最多为min(a,b/4),也就是说x的取值范围是0~3,y的取值范围是0~2。对应的解空间树如图5.2所示,共17个结点。 图5.2a=3,b=8的解空间树 4. 考虑子集和问题,n=3,a={1,3,2},t=3,回答以下问题: (1) 不考虑剪支,画出求解的搜索空间和解。 (2) 考虑左剪支(选择元素),画出求解的搜索空间和解。 (3) 考虑左剪支(选择元素)和右剪支(不选择元素),画出求解的搜索空间和解。 答: (1) 对应的搜索空间如图5.3所示,图中结点为“(cs,i)”,其中cs表示当前选择的元素的和,i为结点的层次。最后找到的两个解是{1,2}和{3}。 图5.3子集和问题的搜索空间(1) (2) 对应的搜索空间如图5.4所示,图中结点所包含数字的含义同(1),最后找到的两个解是{1,2}和{3}。 (3) 对应的搜索空间如图5.5所示,图中结点为“(cs,rs,i)”,其中cs表示当前选择的元素的和,rs为剩余元素的和(含当前元素),i为结点的层次。最后找到的两个解是{1,2}和{3}。 图5.4子集和问题的搜索空间(2) 5. 考虑n皇后问题,其解空间树由1、2、……、n构成的n!种排列组成,现用回溯法求解,要求: (1) 通过解搜索空间说明n=3时是无解的。 (2) 给出剪支操作。 (3) 最坏情况下在解空间树上会生成多少个结点?分析算法的时间复杂度。 答: (1) n=3时的解搜索空间如图5.6所示,不能得到任何叶子结点,所以无解。 图5.5子集和问题的搜索空间(3) 图5.63皇后问题的解搜索空间 (2) 剪支操作是任何两个皇后不能同行、同列和同对角线。 (3) 最坏情况下解空间树中根结点层(i=0)有一个结点,i=1层有n个结点,i=2层有n(n-1)个结点,i=3层有n(n-1)(n-2)个结点,以此类推,设结点总数为C(n): C(n)=1+n+n(n-1)+n(n-1)(n-2)+…+n! ≈n+n(n-1)+n(n-1)(n-2)+…+n! =n!1+11!+12!+…+1n-1! =n!e-1n!-1n+1!-…=n!e-1=O(n!) 每个结点进行冲突判断的时间为O(n),所以算法的时间复杂度为O(n×n!)。 图5.7一棵二叉树 6. 二叉树的所有路径(LintCode480★)。给定一棵二叉树,设计一个算法求出从根结点到叶子结点的所有路径。例如,对于如图5.7所示的二叉树,答案是{"1>2>5","1>3"}。要求设计如下成员函数: vector binaryTreePaths(TreeNode *root) { } 解: 将二叉树看成一棵解空间树,从根结点开始搜索所有的结点,用解向量x表示从根结点到当前结点的路径串,当每次到达一个叶子结点时将x添加到ans中,最后返回ans即可。对应的回溯算法如下: class Solution { vector ans; string x; public: vector binaryTreePaths(TreeNode *root) { if(root==NULL) return {}; x=to_string(root->val); dfs(root); return ans; } void dfs(TreeNode *root) {   //回溯算法 if(root->left==NULL && root->right==NULL) ans.push_back(x); else { if(root->left!=NULL) { string tmp=x; x+="->"+to_string(root->left->val); dfs(root->left); x=tmp;   //回溯 } if(root->right!=NULL) { string tmp=x; x+="->"+to_string(root->right->val); dfs(root->right); x=tmp;   //回溯 } } } }; 上述程序提交后通过,执行用时为42ms,内存消耗为5.36MB。 7. 二叉树的最大路径和Ⅱ(LintCode475★★)。给定一棵二叉树,设计一个算法找到二叉树的最大路径和,路径必须从根结点出发,路径可在任意结点结束,但至少包含一个结点(也就是根结点)。要求设计如下成员函数: int maxPathSum2(TreeNode *root) {} 解: 将二叉树看成一棵解空间树,用ans表示答案,从根结点开始搜索,用cursum记录路径上的结点值之和,当到达任意一个结点时,如果cursum>ans,则置ans=cursum,最后返回ans。对应的回溯算法如下: class Solution { int ans; //存放答案 public: int maxPathSum2(TreeNode *root) { if(root==NULL) return 0; ans=root->val; int cursum=root->val; dfs(cursum,root); return ans; } void dfs(int cursum,TreeNode* root) {   //回溯算法 if(root==NULL) return; if(cursum>ans) ans=cursum; if(root->left!=NULL) { cursum+=root->left->val; dfs(cursum,root->left); cursum-=root->left->val; } if(root->right!=NULL) { cursum+=root->right->val; dfs(cursum,root->right); cursum-=root->right->val; } } }; 8. 求组合(LintCode152★★)。给定两个整数 n 和 k,设计一个算法求从1~n中选出k个数的所有可能的组合,对返回组合的顺序没有要求,但一个组合内的所有数字需要是升序排列的。例如,n=4,k=2,答案是{{1,2},{1,3},{1,4},{2,3},{2,4},{3,4}}。要求设计如下成员函数: vector> combine(int n,int k) { } 解: 采用《教程》5.2节中求幂集的解法2的思路,设置解向量x,x[i]表示一个组合中位置为i的整数,按从i到n的顺序试探,所以得到的解中数字是按升序排列的。用cnt表示选择的整数的个数,当cnt=k时对应一个解,将所有解添加到ans中,最后返回ans即可。对应的回溯程序如下: class Solution { vector> ans; vector x; public: vector> combine(int n,int k) { dfs(0,n,k,1); return ans; } void dfs(int cnt,int n,int k,int i) {   //回溯算法 if(cnt==k) { ans.push_back(x); } else { for(int j=i;j<=n;j++) { x.push_back(j); dfs(cnt+1,n,k,j+1); x.pop_back(); } } } }; 上述程序提交后通过,执行用时为61ms,内存消耗为5.46MB。 9. 最小路径和Ⅱ(LintCode1582★★)。给出一个m×n的矩阵,每个点有一个正整数的权值,设计一个算法从(m-1,0)位置走到(0,n-1)位置(可以走上、下、左、右4个方向),找到一条路径使得该路径所经过的权值和最小,返回最小权值和。要求设计如下成员函数: int minPathSumII(vector> &matrix) {} 解法1: 采用《教程》5.3节中图路径搜索的思路,用cursum累计路径的长度(路径上所经过点的权值和),为了避免路径上的点重复,设置visited数组(初始元素均为0),一旦走到一个顶点便将对应位置的visited置为1,每一步只能走visited为0的点。从起始点(m-1,0)出发进行搜索,当搜索到终点(0,n-1)时将较小的cursum存放到ans中。最后返回ans即可。对应的回溯算法如下: class Solution { const int INF=0x3f3f3f3f; int dx[4]={0,0,1,-1};   //水平方向的偏移量 int dy[4]={1,-1,0,0};   //垂直方向的偏移量 int ans;   //存放答案 vector> visited; vector> A; public: int minPathSumII(vector> &matrix) { A=matrix; int m=A.size(); int n=A[0].size(); visited=vector>(m,vector(n,0)); int x=m-1,y=0; visited[x][y]=1; int cursum=A[x][y]; ans=INF; dfs(m,n,cursum,x,y); return ans; } void dfs(int m,int n,int cursum,int x,int y) { //回溯算法 if (x==0 && y==n-1) {   //找到终点 ans=min(ans,cursum); } else { for(int di=0;di<4;di++) { int nx=x+dx[di]; int ny=y+dy[di]; if(nx<0 || nx>=m || ny<0 || ny>=n) continue; if(visited[nx][ny]==1) continue; visited[nx][ny]=1; cursum+=A[nx][ny]; if(cursum> visited; vector> A; public: int minPathSumII(vector> &matrix) { A=matrix; int m=A.size(); int n=A[0].size(); visited=vector>(m,vector(n,INF)); int x=m-1,y=0; visited[x][y]=A[x][y]; int cursum=A[x][y]; dfs(m,n,cursum,x,y); return visited[0][n-1]; } void dfs(int m,int n,int cursum,int x,int y) { //回溯算法 visited[x][y]=cursum; for(int di=0;di<4;di++) { int nx=x+dx[di]; int ny=y+dy[di]; if(nx<0 || nx>=m || ny<0 || ny>=n) continue; if(cursum+A[nx][ny]>visited[nx][ny]) continue; cursum+=A[nx][ny]; dfs(m,n,cursum,nx,ny); cursum-=A[nx][ny]; } } }; 上述程序提交后通过,执行用时为41ms,内存消耗为5.45MB。 10. 递增子序列(LeetCode491★★)。给定一个含n(1≤n≤15)个整数的数组 nums(100≤nums[i]≤100),找出并返回所有该数组中不同的递增子序列,递增子序列中至少有两个元素,可以按任意顺序返回答案。数组中可能含有重复元素,如果出现两个整数相等,也可以视作递增序列的一种特殊情况。例如,nums={4,6,7,7},答案是{{4,6},{4,6,7},{4,6,7,7},{4,7},{4,7,7},{6,7},{6,7,7},{7,7}}。要求设计如下成员函数: vector> findSubsequences(vector& nums) {} 解: 用x表示解向量(一个满足题目要求的递增子序列),ans存放最后答案。用i遍历nums,分为以下两类情况: (1) 当x为空时,有将nums[i]添加到x中和不将nums[i]添加到x中两种选择。 (2) 当x非空时,若nums[i]≥x.back(),将nums[i]添加到x中,在nums[i]≠x.back()时不将nums[i]添加到x中,因为nums[i]>x.back()时只能将nums[i]添加到x中,否则会得不到一些递增子序列。 对应的代码如下: class Solution { public: vector> ans; vector x; vector> findSubsequences(vector& nums) { dfs(nums,0); return ans; } void dfs(vector& nums,int i) {   //回溯算法 if (i==nums.size()) { if (x.size()>=2) ans.push_back(x); } else { if (x.size()==0) { x.push_back(nums[i]); dfs(nums,i+1); x.pop_back(); dfs(nums,i+1); } else { if (nums[i]>=x.back()) { x.push_back(nums[i]); dfs(nums,i+1); x.pop_back(); } if (nums[i]!=x.back()) { dfs(nums,i+1); } } } } }; 上述程序提交后通过,执行用时为20ms,内存消耗为19.3MB。如果改为当x非空时,若nums[i]≥x.back(),做将nums[i]添加到x中和不将nums[i]添加到x中两种选择,也就是将上述dfs中最下面的两个if语句改为如下: if (nums[i]>=x.back()) { x.push_back(nums[i]);   //将nums[i]添加到x中 dfs(nums,i+1); x.pop_back(); dfs(nums,i+1);   //不将nums[i]添加到x中 } 则会出现重复的递增子序列,例如nums={4,6,7,7}时,输出结果是{{4,6,7,7},{4,6,7},{4,6,7},{4,6},{4,7,7},{4,7},{4,7},{6,7,7},{6,7},{6,7},{7,7}},从中看出{4,6}等重复项。 另外也可增加一个last参数表示子序列x中最后的元素,当x为空时last为INT_MIN。对应的代码如下: class Solution { public: vector> ans; vector x; vector> findSubsequences(vector& nums) { int last=INT_MIN; dfs(nums,last,0); return ans; } void dfs(vector& nums,int last,int i) {   //回溯算法 if (i==nums.size()) { if (x.size()>=2) ans.push_back(x); } else { if (nums[i]>=last) { x.push_back(nums[i]); dfs(nums, nums[i], i+1); x.pop_back(); } if (nums[i]!=last) { dfs(nums,last,i+1); } } } }; 上述程序提交后通过,执行用时为32ms,内存消耗为19.4MB。 11. 给表达式添加运算符(LeetCode282★★★)。给定一个长度为n(1≤n≤10)的仅包含数字0~9的字符串num和一个目标值整数target(-231≤target≤231-1),设计一个算法在num的数字之间添加二元运算符(不是一元)+、-或*,返回所有能够得到 target 的表达式。注意所返回表达式中的运算数不应该包含前导零。例如,num="105",target=5,答案是{"1*0+5","10-5"}。要求设计如下成员函数: vector addOperators(string num,int target) { } 解: 采用回溯法求解,用x表示值为target 的表达式,用ans存放这样的全部表达式。在算法设计中需要注意以下两个方面: (1) 产生的表达式中运算数可以是连续的一个或者多个数字,当分隔点为num[i]时,通过num.substr(i,j-i+1)取出后面的数字子串curs,对应的值为curd,由于运算数不应该包含前导零,所以当j!=i && num[i]=='0'成立时返回。 (2) 当分隔点num[i]后面的运算数产生后,分隔点处可以取'+'、'-'或者'*',即三选一。若当前求出的表达式的值为cursum,分隔点前面的一个运算数是pred: ① 若取'+',执行x+='+'+curs,cursum+=curd,prev=curd,递归处理对应的子问题,回溯时恢复修改的参数。 ② 若取'-',执行x+='-'+curs,cursum-=curd,prev=curd,递归处理对应的子问题,回溯时恢复修改的参数。 ③ 若取'*',执行x+='*'+curs,cursum=cursum-pred+pred*curd,prev=pred*curd,例如1+2×3×4,假设当前在3和4之间取'*',则cursum应减去2×3=6,然后加上2×3×4=24。再递归处理对应的子问题,回溯时恢复修改的参数。 例如,num="105",target=5,采用回溯法的过程如图5.8所示,图中用'.'表示分隔点,虚框表示为前导零的结点,带阴影结点对应一个解。 图5.8num="105",target=5的求解过程 对应的程序如下: class Solution { public: string num; int target; vector ans; string x; vector addOperators(string num, int target) { this->num=num; this->target=target; dfs(0,0,0); return ans; } void dfs(int i,long long cursum,long long pred) {  //回溯算法 if (i==num.size()) { if (cursum==target) ans.push_back(x); } else { int oldlen=x.size(); for (int j=i;j> visited; void dfs(int m,int n,int x,int y) {   //回溯算法 if (x==m && y==n) {   //找到目标位置 ans++;   //路径的条数增加1 } else { for (int di=0;di<4;di++) {   //试探所有可走路径 int nx=x+dx[di];   //求出从(x,y)走到的位置(x1,y1) int ny=y+dy[di]; if (nx<1 || nx>m || ny<1 || ny>n)   //跳过越界位置 continue; if (visited[nx][ny]==1)   //仅考虑没有访问的位置 continue; visited[nx][ny]=1; dfs(m,n,nx,ny); visited[nx][ny]=0;   //回溯 } } } void solve(int m,int n) {   //求解算法 ans=0; visited=vector>(m+1,vector(n+1,0)); visited[1][1]=1; dfs(m,n,1,1); printf("%d*%d象棋中马的路径条数=%d\n",m,n,ans); } 13. 设计一个回溯算法求迷宫中从入口s到出口t的最短路径及其长度。 解: 用bestx和bestlen保存最短路径及其长度。从curp方格(初始为s)出发进行搜索,当前路径及其长度分别保存在x和len中: (1) 若curp=t,说明找到一条迷宫路径,通过比较路径长度将最优解保存在bestx和bestlen中。 (2) 否则试探每个方位di(取值为0~3),求出相邻方格(nx,ny),跳过超界、已经访问或者为障碍物的位置,走到(nx,ny)位置并继续搜索,再从(nx,ny)位置回溯。最后输出bestx和bestlen。 对应的回溯算法如下: int dx[4]={-1, 1, 0, 0}; int dy[4]={0, 0, -1, 1}; struct Box {   //方格类型 int x,y; Box() {} Box(int x,int y):x(x),y(y) {} }; int visited[MAXM][MAXN]; vector> A; int m,n; Box t; int bestlen;   //最短路径长度 vector bestx;   //最短路径 void dfs(Box&curp,vector&x,int len) {   //回溯算法 if(curp.x==t.x && curp.y==t.y) { if(len=m || ny<0 || ny>=n || visited[nx][ny]==1) continue; if(A[nx][ny]==1) continue; if(len> mg,Box&start,Box&goal) {   //求解算法 A=mg; m=A.size(); n=A[0].size(); Box curp=start; visited[start.x][start.y]=1; t=goal; memset(visited,0,sizeof(visited)); vector x; x.push_back(curp); bestlen=INF; dfs(curp,x,0); printf("最短路径: "); for(int i=0;i=1){   //重复试探 q[i]++; while (q[i]<=n && !place(i))   //试探一个位置(i,q[i]) q[i]++; if (q[i]<=n) {   //为第i个皇后找到了一个合适的位置(i,q[i]) if (i==n)   //若放置了所有皇后,输出一个解 dispasolution(n); else {   //皇后没有放置完 i++;   //转向下一行,即开始下一个皇后的放置 q[i]=0;   //每次放一个新皇后都从该行的首列进行试探 } } else i--;   //若第i个皇后找不到合适的位置,则回溯到上一个皇后 } } 15. 题目描述见《教程》第3.11节的第12题,这里要求采用回溯法求解。 解: 本问题与旅行商问题类似,不同之处是不必回到起点。先将边数组tuple转换为邻接矩阵A,为了简单,将城市编号由1~n改为0~n-1,这样起点变成了顶点0,并且不必考虑路径中最后一个顶点到顶点0的道路。然后采用《教程》第5章中5.13节求TSP问题的回溯法求出最短路径长度bestd并返回。对应的回溯法算法如下: const int INF=0x3f3f3f3f;   //表示∞ vector x;   //解向量(路径) int d;   //x路径的长度 int bestd=INF;   //保存最短路径长度 void dfs(vector> &A,int s,int i) {   //回溯算法 int n=A.size(); if(i>=n) {   //到达一个叶子结点 bestd=min(bestd,d);   //通过比较求最优解 } else { for(int j=i;j> &tuple) {   //求解算法 if(n==1) return 0; vector> A(n,vector(n,INF));   //邻接矩阵 for(int i=0;i=n) disp(n);   //所有皇后放置结束,输出一个解 else { for (int j=i;j> binaryTreePathSum(TreeNode *root, int target) {} 解: 将二叉树看成一棵解空间树,从根结点开始搜索,解向量x记录路径上的结点,cursum记录路径上的结点值之和,当到达叶子结点时,如果cursum=target,则将x添加到答案ans中,最后返回ans。对应的回溯算法如下: class Solution { vector> ans;   //存放答案 vector x;   //解向量 public: vector> binaryTreePathSum(TreeNode *root, int target) { if(root==NULL) return ans; x.push_back(root->val); int cursum=root->val; dfs(cursum,target,root); return ans; } void dfs(int cursum,int target,TreeNode *root) {   //回溯算法 if (root->left==NULL && root->right==NULL) { if(cursum==target) ans.push_back(x); } else { if(root->left!=NULL) { x.push_back(root->left->val); cursum+=root->left->val; dfs(cursum,target,root->left); cursum-=root->left->val; x.pop_back(); } if(root->right!=NULL) { x.push_back(root->right->val); cursum+=root->right->val; dfs(cursum,target,root->right); cursum-=root->right->val; x.pop_back(); } } } }; 2. 第k个排列(LintCode88★★)。给定 n 和 k,设计一个算法求1~n的全排列中字典序的第k个排列。要求设计如下成员函数: string getPermutation(int n,int k) {} 解: 利用《教程》5.10.1节的perm1算法的思路,增加cnt计数,x作为解向量存放一个排列,每次找到一个排列时执行cnt++,当cnt=k时将x存放到ans中,并置flag为true,终止对其他路径的搜索。最后返回ans。对应的回溯算法如下: class Solution { string ans;   //存放第k个排列 int cnt;   //计数 bool flag;   //是否找到 string x;   //解向量 vector used;   //used[i]表示a[i]是否使用过 public: string getPermutation(int n,int k) { used=vector(n+1,false); x=string(n,'0'); flag=false; cnt=1; dfs(n,k,0); return ans; } void dfs(int n,int k,int i) {   //回溯算法 if (i>=n) { if(cnt==k) { flag=true; ans=x; } cnt++; } else if(!flag) { for(int j=1;j<=n;j++) { if(used[j]) continue; x[i]='0'+j; used[j]=true; dfs(n,k,i+1); used[j]=false; x[i]='0'; } } } }; 3. 设计一个算法求解这样的子集和问题: 给定含n个正整数的数组a,从中选出若干整数,使它们的和恰好为t,如果找到任意一种解,返回true,否则返回false。 解法1: 利用《教程》5.6节的求子集和问题的思路,用flag表示子集和问题是否有解,初始设置为false,一旦找到一个解置flag为true。含左、右剪支的回溯算法如下: bool flag;   //子集和问题是否有解 void dfs(vector &a,int t,int cs,int rs,int i) {   //回溯算法 if (i>=a.size()) {   //到达一个叶子结点 if (cs==t) flag=true;   //找到一个满足条件的解 } else if(!flag) {   //没有到达叶子结点 rs-=a[i];   //求剩余整数的和 if (cs+a[i]<=t) {   //左孩子结点剪支 dfs(a,t,cs+a[i],rs,i+1); } if (cs+rs>=t) {   //右孩子结点剪支 dfs(a,t,cs,rs,i+1); } rs+=a[i];   //恢复剩余整数和(回溯) } } bool solve(vector &a,int t) {   //判断子集和问题是否有解 int rw=0; for (int j=0;j &a,int t,int cs,int i) {   //回溯算法 if(cs==t) { flag=true; } if(!flag) { for(int j=i;j &a,int t) {   //判断子集和问题是否有解 flag=false; dfs(a,t,0,0);   //i从0开始 return flag; } 4. 设计一个算法求解这样的子集和问题: 给定含n个正整数的数组a,从中选出若干整数,使它们的和恰好为t,假设该问题至少有一个解,当有多个解时求选出的整数个数最少的一个解。 解: 利用《教程》5.6节的求子集和问题的思路,用x存放当前解,bestx存放最优解(初始时置其长度为n),当找到一个解x时,将较小长度的x存放到bestx中,最后返回bestx。含左、右剪支的回溯算法如下: vector x; vector bestx; void dfs(vector &a,int t,int cs,int rs,int i) {   //回溯算法 if (i>=a.size()) {   //到达一个叶子结点 if (cs==t && x.size()=t) {   //右孩子结点剪支 dfs(a,t,cs,rs,i+1); } rs+=a[i];   //恢复剩余整数和(回溯) } } vector solve(vector &a,int t) {   //判断子集和问题是否有解 int rw=0; for (int j=0;j(a.size());   //初始化bestx的长度为n dfs(a,t,0,rw,0);   //i从0开始 return bestx; } 5. 求有重复元素的排列问题(LintCode16★★)。设有一个含n个整数的数组a,其中可能含有重复的元素,求这些元素的所有不同排列。例如a={1,1,2},输出结果是{1,1,2},{1,2,1},{2,1,1}。 解: 在用回溯法求全排列的基础上增加对元素的重复性判断。例如,对于a={1,1,2},不判断重复性时输出结果是{1,1,2},{1,2,1},{1,1,2},{1,2,1},{2,1,1},{2,1,1},共6个排列,其中有3个是重复的。重复性判断是这样的: 当扩展a[i]时仅选取在a[i..j-1]中没有出现的元素a[j],将其与a[i]交换,如果a[j]出现在a[i..j-1]中,则对应的排列已经在前面求出了,如果再这样做会产生重复的排列。基于排列树的回溯算法如下: class Solution { vector> ans;   //存放答案 public: vector> permuteUnique(vector &nums) { dfs(nums,0); return ans; } bool ok(vector &a,int i,int j) { //ok()用于判断重复元素 if (j>i) { for(int k=i;k &a,int i) {   //求有重复元素的排列问题 int n=a.size(); if (i==n) { ans.push_back(a); } else { for (int j=i;jm时到达一个叶子结点,输出一个排列。采用used数组避免重复,used[j]=false表示没有选择整数j,used[j]=true表示已经选择整数j。对应的回溯算法如下: vector x;   //x[1..m]中存放一个排列 vector used; void dfs(int n,int m,int i) {   //回溯算法 if (i>m) { for (int j=1;j<=m;j++) printf(" %d",x[j]);   //输出一个排列 printf("\n"); } else { for (int j=1;j<=n;j++) { if (!used[j]) { used[j]=true;   //修改used[i] x[i]=j;   //x[i]选择j dfs(n,m,i+1); x[j]=0; used[j]=false;   //回溯:恢复used[i] } } } } void solve(int n,int m) {   //求1~n中m个元素的全排列 x=vector(m+1); used=vector(n,false); dfs(n,m,1);   //i从1开始 } 7. 对于n皇后问题,有人认为当n为偶数时,其解具有对称性,即n皇后问题的解个数恰好为n/2皇后问题的解个数的两倍,这个结论正确吗?请编写回溯法程序对n=4、6、8、10的情况进行验证。 解: 这个结论不正确。验证程序如下: int q[MAXN];   //存放n皇后问题的解(解向量) int cnt;   //累计解个数 bool valid(int i,int j) {   //测试(i,j)位置能否放置皇后 if (i==0) return true;   //第一个皇后总是可以放置 int k=0; while (k=n) cnt++;   //所有皇后放置结束 else { for (int j=i;j x;   //解向量 vector used;   //顶点访问标记 int cnt;   //累计哈密顿回路的数目 void disp(int n,int s) {   //输出一个解路径 printf(" (%d) ",cnt++); for (int i=0;i",x[i]); printf("%d\n",s); } void dfs(vector> &A,int s,int curlen,int i) {  //回溯算法 int n=A.size(); if(curlen==n-1) { if(A[x.back()][s]==1) disp(n,s); } else { for(int j=0;j> &A,int s) {   //求从顶点s出发的哈密顿回路 int n=A.size(); x.clear(); x.push_back(s); used=vector(n,0); used[s]=1; printf("从顶点%d出发的哈密顿回路:\n",s); dfs(A,s,0,s); } 解法2: 采用《教程》5.10.1节中解法2的思路。对应的回溯算法如下: vector x;   //解向量 int cnt;   //累计哈密顿回路的数目 void disp(int n,int s) {   //输出一个解路径 printf("(%d) ",cnt++); for (int i=0;i",x[i]); printf("%d\n",s); } void dfs(vector> &A,int s,int i) {   //回溯算法 int n=A.size(); if (i>=n) { if(A[x[n-1]][s]==1) disp(n,s); } else { for (int j=i;j> &A,int s) {   //求从顶点s出发的哈密顿回路 int n=A.size(); x.clear(); x.push_back(s); for(int i=0;i x; vector bestx; int bestw; void dfs(int cw,int cc,int i) {   //回溯算法 if(i>=n) {   //搜索到叶子结点 if (cwcost) continue;   //剪支 if(cw+w[i][j]>bestw) continue;   //剪支 x[i]=j;   //部件i选择供应商j cc+=c[i][j]; cw+=w[i][j]; dfs(cw,cc,i+1); cc-=c[i][j];   //cc回溯 cw-=w[i][j];   //cw回溯 x[i]=-1; } } } void solve() {   //求解算法 int cw=0,cc=0; x=vector(n,-1); printf("求解结果\n"); bestw=INF; dfs(cw,cc,0); for(int i=0;i x; vector bestx; int bestw; vector used;   //供应商是否已经使用 void dfs(int cw,int cc,int i) {   //回溯算法 if(i>=n) {   //搜索到叶子结点 if (cwcost)   //剪支 continue; if(cw+w[i][j]>bestw)   //剪支 continue; x[i]=j;   //部件i选择供应商j used[j]=true; cc+=c[i][j]; cw+=w[i][j]; dfs(cw,cc,i+1); cc-=c[i][j];   //cc回溯 cw-=w[i][j];   //cw回溯 used[j]=false; x[i]=-1; } } } void solve() {   //求解算法 int cw=0,cc=0; x=vector(n,-1); used=vector(m,false); printf("求解结果\n"); bestw=INF; dfs(cw,cc,0); for(int i=0;i> A;   //邻接矩阵 int n; vector x;   //解向量 int cn;   //当前解的顶点数 int bestn;   //最大团的顶点数 void dfs(int cn,int i) {   //回溯算法 if (i>=n) {   //到达叶子结点 bestn=max(bestn,cn); } else { bool complete=true;   //检查顶点i与当前团的相连关系 for (int j=0;j=bestn) {   //剪支(右子树) x[i]=0;   //不选中顶点i dfs(cn,i+1); } } } void solve(vector> &a) {   //求最大团问题 A=a; n=A.size(); x=vector(n,0); cn=0;   //当前团中的顶点数 dfs(cn,0); printf("最大团中的顶点数=%d\n",bestn); }