书写结构:解空间和解结构,剪枝策略,示例,代码,时间复杂度分析
5-1子集和问题
1. 解空间和解结构 类似于0-1背包问题,对于S大小为n的子集和问题,解空间是由长度为n的0-1向量组成,解的结构为子集树,如对于上述示例的解,解空间即为(1,1,1,0,0,0)
2. 剪枝策略 注意到题目说S是正整数的集合,c是正整数,故深搜的过程一定使得子集和增加或不变。
考虑在某节点处向下搜索的过程,设当前层数为k(即当前考虑是否加入数S[k]到子集中)
约束函数:维护变量cw表示当前子集和,若cw+S[k]>c
,则剪去左子树
限界函数:计算变量rw表示剩余的整数之和(S[k+1:n-1]之和),若cw+rw<c
,即将之后所有整数加入子集也无法得到解,故减去右子树
对于本题而言,如果只要一个可行解的话,可以在搜索每个节点的时候,在考虑完是否加入数S[k]之后,判断S[k]是否等于c,若等于,则无需再搜索了
3. 示例 太多了不太好画,减少点数据量:
4. 代码 这里就没弄文件读写了(可以直接粘贴下面的输入测试啦)
输入1:
输入2:
得到一个解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <iostream> #include <windows.h> using namespace std;const int N=100010 ;int S[N],res[N];int n,c;int num=0 ;void dispsolution (int res[]) { cout<<"第" <<num++<<"个解" <<endl; for (int i=1 ;i<=n;i++) { if (res[i]) cout<<S[i]<<" " ; } cout<<endl; } void dfs (int cw,int rw,int res[],int level) { if (level>n) { if (cw==c) { for (int i=1 ;i<=n;i++) { if (res[i]) cout<<S[i]<<" " ; } exit (0 ); } }else { if (cw+S[level]<=c) { res[level]=1 ; dfs (cw+S[level],rw-S[level],res,level+1 ); } rw=rw-S[level]; if (cw+rw>=c) { res[level]=0 ; dfs (cw,rw,res,level+1 ); } } } int main () { cin>>n>>c; int rw=0 ; for (int i=1 ;i<=n;i++) { cin>>S[i]; rw=rw+S[i]; } dfs (0 ,rw,res,1 ); if (num==0 ) cout<<"No Solution!" <<endl; return 0 ; }
得到所有解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <iostream> using namespace std;const int N=100010 ;int S[N],res[N];int n,c;int num=0 ;void dispsolution (int res[]) { cout<<"第" <<num++<<"个解" <<endl; for (int i=1 ;i<=n;i++) { if (res[i]) cout<<S[i]<<" " ; } cout<<endl; } void dfs (int cw,int rw,int res[],int level) { if (level>n) { if (cw==c) dispsolution (res); }else { if (cw+S[level]<=c) { res[level]=1 ; dfs (cw+S[level],rw-S[level],res,level+1 ); } rw=rw-S[level]; if (cw+rw>=c) { res[level]=0 ; dfs (cw,rw,res,level+1 ); } } } int main () { cin>>n>>c; int rw=0 ; for (int i=1 ;i<=n;i++) { cin>>S[i]; rw=rw+S[i]; } dfs (0 ,rw,res,1 ); if (num==0 ) cout<<"No Solution!" <<endl; return 0 ; }
5. 时间复杂度分析 考虑最坏情况下时间复杂度,每个节点都进行搜索,处理每个节点所需的时间均为$O(1)$,一共有1+2+4+8+…+2^n^
=$O(2^{n+1})$个节点,故时间复杂度为$O(2^{n+1})$
参考:https://blog.csdn.net/gl620321/article/details/108801724
5-2 最小长度电路板排列问题
1. 解空间和解结构 类似于旅行商问题,对于n个电路板的最小电路板排列问题,其解空间为n个数的全排列,解结构为排列树
2. 剪枝策略 维护变量bestd表示当前的最小长度,假设此时选择第i个位置上的电路板,考虑选择第j个电路板(j>=i,因为i前面的已经选择好了)作为该位置上的排列,此时利用已经选择好的第1~第i个位置上的电路板(第i个位置刚刚选择好的)来计算连接块的最大长度,若该长度小于当前最优解,则继续进行下面位置的选择,否则第i个位置上不能选择第j个电路板,剪去该子树
3. 示例 示例数据量太大,减少点
1 2 3 4 5 4 4 1 1 1 1 1 1 0 1 0 0 0 1 0 1 1 0
4个电路板,L1={1,2},L2={1,2,4},L3={1,4},L4={1,2,3}
类似于下面的画法(排列树太多了,只画了一个子树):
最后的结果:
4. 代码 输入文件 input.txt:
1 2 3 4 5 6 7 8 9 8 5 1 1 1 1 1 0 1 0 1 0 0 1 1 1 0 1 0 1 1 0 1 0 1 0 0 1 1 0 1 0 0 0 0 0 1 0 1 0 0 1
输入含义
L1={1,4,5,6},L2={1,2,3,6,8},L3={1,3,4,5},L4={1,2,3,4,6},L5={1,7,8}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #include <iostream> #include <fstream> #include <vector> using namespace std;int n, m;int bestx[10 ];int B[10 ][10 ];int x[10 ], low[10 ], high[10 ];int bestd=0 ;int len (int ii) { for (int i = 1 ; i <= m; i++) { high[i] = 0 ; low[i] = n + 1 ; } for (int i = 1 ; i <= ii; i++) for (int k = 1 ; k <= m; k++) if (B[x[i]][k] > 0 ) { if (i < low[k]) low[k] = i; if (i > high[k]) high[k] = i; } int tmp = 0 ; for (int k = 1 ; k <= m; k++) if (low[k] <= n && high[k] > 0 && tmp < high[k] - low[k]) tmp = high[k] - low[k]; return tmp; } void swap (int * x, int i, int j) { int tmp; tmp = x[i]; x[i] = x[j]; x[j] = tmp; } void backtrack (int i) { if (i == n) { int tmp = len (i); if (tmp < bestd) { bestd = tmp; for (int j = 1 ; j <= n; j++) bestx[j] = x[j]; } } else { for (int j = i; j <= n; j++) { swap (x, i, j); int ld = len (i); if (ld < bestd) backtrack (i + 1 ); swap (x, i, j); } } } int arrangeBoards () { bestd = n + 1 ; for (int i = 1 ; i <= n; i++) x[i] = i; backtrack (1 ); return bestd; } int main (void ) { ifstream ifs ("input.txt" ) ; ifs>>n; ifs>>m; vector<int > temp (m) ; for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= m; j++) { ifs>>B[i][j] ; } } int minLen = arrangeBoards (); cout<<minLen<<endl; for (int i = 1 ; i <= n; i++) cout<<bestx[i]<<" " ; ifs.close (); return 0 ; }
5. 时间复杂度分析 对于最坏情况,每个节点都需要计算一次len,即序号运行len函数,而第i层节点运行len函数时间复杂度为$O(im)$,故最坏情况下所需时间为$T=O(m)(n-1)+O(2m) (n-1)(n-2)+…+O(n m)(n-1)!$,故最坏情况下时间复杂度为$O(m n!)$
5-3 最小重量机器设计问题
题目两个要求,总价格不超过d,并且部件重量之和要最小
1. 解空间和解结构 对于n个部件,m个供应商的最小重量机器设计问题,其解空间为长度为n的向量,向量的每一项为1~m的整数,对应的解结构为排列树(也不是那种TSP的排列树,不过也是排列啦,姑且称之为排列树)
2. 剪枝策略 考虑第i个部件供应商的选择
约束函数:维护变量cp表示当前已经购得的部件的价格之和,假设考虑选择第j个供应商,若cp+c[i][j]>d
,则剪去以该节点为根节点的子树,无需再进行搜索
限界函数:维护变量cw表示当前已经购得的部件的重量之和,变量bestw表示当前最优解的重量之和,假设考虑选择第j个供应商,若cw+w[i][j]>=bestw
,则剪去该节点为根节点的子树,无需进行搜索
3. 示例 对于题中示例画树:
4. 代码 输入:
1 2 3 4 5 6 7 3 3 4 1 2 3 3 2 1 2 2 2 1 2 3 3 2 1 2 2 2
没实现文件读写了~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <bits/stdc++.h> using namespace std;int n,m,d;int c[999 ][999 ];int w[999 ][999 ];int cw=0 ,cp=0 ;int bestw=999 ,bestp=999 ;int x[999 ];int bestx[999 ];void backtrack (int i) { if (i>=n){ if (cp<=d&&cw<bestw){ bestw=cw; bestp=cp; for (int k=0 ;k<n;k++){ bestx[k]=x[k]; } } } else { for (int j=0 ;j<m;j++){ x[i]=j; cw=cw+w[i][j]; cp=cp+c[i][j]; if (cw<bestw&&cp<=d) backtrack (i+1 ); cw-=w[i][j]; cp-=c[i][j]; } } } int main () { cin>>n>>m>>d; int i=0 ,j=0 ; for (i=0 ;i<n;i++){ for (j=0 ;j<m;j++){ cin>>c[i][j]; } } for (i=0 ;i<n;i++){ for (j=0 ;j<m;j++){ cin>>w[i][j]; } } backtrack (0 ); cout<<bestw<<endl; for (int k=0 ;k<n;k++) cout<<bestx[k]+1 <<" " ; }
5. 时间复杂度分析 考虑最坏情况,对于每个非叶子节点,搜索所需时间为$O(m)$,对于每个叶子节点,搜索所需的时间为$O(1)$,故最坏情况下所需时间为$T=m*(m+m^2+…+m^{n-1})+m^n=O(m^n)$
5-4 运动员最佳配对问题
输入:
1 2 3 4 5 6 7 3 10 2 3 2 3 4 3 4 5 2 2 2 3 5 3 4 5 1
输出:
1. 解空间和解结构 将问题转换为男运动员选女运动员的问题,故n个男、女运动员的运动员最佳匹配问题的解空间为n个数的全排列,对应的解结构为排列树
2. 剪枝策略 考虑对第i个男运动员匹配女运动员的情况,其中变量Max存储当前最优解的竞赛优势,变量sum存储第1~i-1个已经匹配完成的男运动员的竞赛优势,计算第i~n个男运动员的最大可能的竞赛优势ctn,若cnt+sum<Max
,则剪去该节点及其子树
其中第j个男运动员的最大可能的竞赛优势为该男运动员与所有女运动员进行匹配所得竞赛优势中最大者,存储到maxsum数组中
3.示例
4.代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include <bits/stdc++.h> using namespace std;int n;int boy[21 ][21 ],girl[21 ][21 ]; int Max=INT_MIN; int sum=0 ; int data[21 ][21 ]; int maxSum[21 ]; int book[21 ]; void dfs (int t) { if (t>=n) { Max=max (Max,sum); return ; } int ctn=0 ; for (int i=t;i<n;i++) ctn+=maxSum[i]; if (sum+ctn<Max) return ; for (int i=0 ;i<n;i++) { if (!book[i]) { book[i]=1 ; sum+=data[t][i]; dfs (t+1 ); book[i]=0 ; sum-=data[t][i]; } } } int main () { cin>>n; for (int i=0 ;i<n;i++) { for (int j=0 ;j<n;j++) cin>>boy[i][j]; } for (int i=0 ;i<n;i++) { for (int j=0 ;j<n;j++) cin>>girl[i][j]; } for (int i=0 ;i<n;i++) { for (int j=0 ;j<n;j++) { data[i][j]=boy[i][j]*girl[j][i]; maxSum[i]=max (maxSum[i],data[i][j]); } } dfs (0 ); cout<<Max<<endl; return 0 ; }
5.时间复杂度分析 考虑最坏情况,每个节点都需要搜索,对于非叶子节点,其需要时间为$O(n)$,对于叶子节点,其所需时间为$O(1)$,故所需时间为$T=n(n+n (n-1)+…+n!)+n!=O(n*n!)$
5-5 无分隔符字典问题
1. 解空间和解结构 由S为$L_k$子集可知n个符号,长度为k的无分隔符字典问题的解空间为长度为$n^k$的向量,其中向量每一项为0或1,表示$L_k$中某一项是否在S中,解结构为子集树
2. 剪枝策略 首先将$L_k$中所有字字符串放入数组L中,考虑是否放入下标为i的字符串:
约束函数:若该字符串与当前S集合中的任意字符串按照题目所给方式进行拼接,结果均不在S集合中,且不为该字符串本身,则考虑将其放入,搜索其左子树,否则剪去该节点的左子树
限界函数:考虑不放入该字符串,若将i+1~$n^k$的字符串均放入,若n^k^-i+S.size<best,则无需搜索其右子树,剪去该节点的右子树
3. 示例 假设$\sum=(a,b),L_k=\{aa,ab,ba,bb\}$,则$L_k$的一个最大无分隔符字典为$\{aa,bb\}$
4. 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 #include <iostream> #include <fstream> #include <vector> #include <set> using namespace std;int *ak;int lk;int n, k;int best = 0 ; vector<int > L; set<int > S; void insert (int i) { S.insert (L[i]); } void erase (int i) { S.erase (L[i]); } int digi (int i) { int x = 0 ; for (int j=0 ; j<k; j++) { x *= 10 ; x += ak[j]; } return x; } bool pref (int a, int b) { int bb =b; int x = a; int y = bb/10 ; for (int i=0 ; i<k-1 ; i++) { ak[k-i-2 ] = x % 10 ; x /= 10 ; ak[2 *k-i-3 ] = y % 10 ; y /= 10 ; } for (int i=0 ; i<k-1 ; i++) if (S.count (digi (i)) > 0 ||digi (i)==bb) return true ; x = bb; y = a/10 ; for (int i=0 ; i<k-1 ; i++) { ak[k-i-2 ] = x % 10 ; x /= 10 ; ak[2 *k-i-3 ] = y % 10 ; y /= 10 ; } for (int i=0 ; i<k-1 ; i++) if (S.count (digi (i)) > 0 ||digi (i)==bb) return true ; return false ; } bool oka (int b) { int bb = L[b]; set<int >::iterator it; it = S.begin (); while (it != S.end ()) { int a = *it; if (pref (a, bb)) return false ; it++; } return true ; } void Perm (int list[], int dep, int m, int n) { if (dep>m) { int x = 0 ; for (int i=1 ; i<=m; i++) x = x*10 + list[i]; L.push_back (x); } else for (int j=1 ;j<=n;j++) { swap (list[dep], list[j]); Perm (list, dep+1 , m, n); swap (list[dep], list[j]); } } void backtrack (int dep) { if (dep >= lk) { if (S.size () > best) best = S.size (); return ; } if (oka (dep)) { insert (dep); backtrack (dep+1 ); erase (dep); } if (lk-dep+S.size ()>best) backtrack (dep+1 ); } int main () { ifstream fin ("input.txt" ) ; cout << "输入正整数n:" ; fin >> n; cout << n; cout << "\n输入正整数k:" ; fin >> k; cout << k; ak = new int [2 *k]; lk = n; for (int i=1 ; i<k; i++) lk *= n; lk--; int *x = new int [n+1 ]; for (int i=1 ; i<=n; i++) x[i] = i; Perm (x, 1 , k, n); backtrack (0 ); cout << "\n最大无分隔符字典元素个数为:" << best; cout << endl; cout << endl; fin.close (); return 0 ; }
5. 时间复杂度 考虑最坏情况,对于n个字符,长度为k的问题,对于每个非叶子节点的左子节点,搜索需要耗费时间为$O(n^kk)$,对于右子节点搜索需要的时间为$O(1)$,对于叶节点需要时间$O(1)$,故耗费的时间$T=O(2^{n^k-1} n^k*k)$
5-6 无合集问题
大概的思路是从1开始每个数都尝试放入n个子集的每个子集,然后用题目给的条件x+y不属于S进行剪枝
1. 解空间和解结构 从1开始尝试放入每个子集中,对于子集为n的无合集问题,其解空间为长度为k的向量,向量的每一项为1~n,表示对应下标的数分配到的子集,其中k为本题所求,以n=3为例,解结构如下:
2. 剪枝策略 据题意,若数i无法分配到集合j(不满足和不在集合的条件),则该节点的第j个分支无需遍历,剪去该分支
3. 示例
4. 代码 定义存储当前解的数组a[N][N],其中a[n][0]表示第n个子集的元素个数a[n][1]到a[n][a[n][0]]为这个子集的所有元素。定义n。设置初始的结果ans为1(最后要减1,所以初始值其实是0),定义最优值为best,定义最优解为e[N][N],其中的结构和a[N][N]一样,以及定义判定数组h[N][N],h[i][j]表示第i个子集中是否有j这个元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <bits/stdc++.h> using namespace std;const int N = 110 ;int a[N][N], n, ans = 1 , best, e[N][N];bool h[N][N];void dfs (int level) { if (level == n){ if (ans <= best) return ; best = ans; for (int i = 0 ; i < n; i ++){ for (int j = 0 ; j <= a[i][0 ]; j ++) e[i][j] = a[i][j]; } return ; } else { bool flag = true ; for (int i = 1 ; i <= a[level][0 ]; i ++){ if (h[level][ans - a[level][i]] && ans - a[level][i] != a[level][i]) flag = false ; } if (!flag) dfs (level + 1 ); else { a[level][++ a[level][0 ]] = ans; h[level][ans ++] = true ; dfs (0 ); -- a[level][0 ]; h[level][-- ans] = false ; dfs (level + 1 ); } } } int main () { cin >> n; dfs (0 ); cout << --best << endl; for (int i = 0 ; i < n; i ++){ for (int j = 1 ; j <= e[i][0 ]; j ++) printf ("%d " , e[i][j]); puts ("" ); } return 0 ; }
参考:无和集问题 - AcWing
5. 时间复杂度 考虑最坏情况,对于非叶子节点,假设其位于第i层,则时间消耗为$O(i)$,对于非叶子节点,其时间消耗为$O(1)$,本题时间消耗还与树的层数有关,而层数又是要求的。。。,需要一些推导来得到一个上界吧~
5-7 n色方柱问题
为了提高效率,用图论的知识简化了题目(我没怎么看懂为啥这样简化的),谢谢有被恶心到
有点难懂,我觉得不会考。。。,理解一下代码吧
代码 input.txt:
1 2 3 4 5 6 4 RGBY 0 2 1 3 0 0 3 0 2 1 0 1 2 1 0 2 1 3 1 3 3 0 2 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 #include <iostream> #include <fstream> #include <algorithm> using namespace std;const int MAX = 50 ;int board[MAX][6 ]; int solu[MAX][6 ]; int n; int ans = 0 ; int used[MAX];char color[MAX];void out (int edge[]) { int i, j, k, a, b, c, d; for (i=0 ; i<2 ; i++) { for (j=0 ; j<n; j++) used[j] = 0 ; do { j = 0 ; d = c = -1 ; while (j<n && used[j]>0 ) j++; if (j < n) do { a = board[j][edge[i*n+j]*2 ]; b = board[j][edge[i*n+j]*2 +1 ]; if (b == d) swap (a, b); solu[j][i*2 ] = a; solu[j][i*2 +1 ] = b; used[j] = 1 ; if (c<0 ) c = a; d = b; for (k=0 ; k<n; k++) if (used[k]==0 && (board[k][edge[i*n+k]*2 ]==b || board[k][edge[i*n+k]*2 +1 ]==b)) j = k; }while (b != c); }while (j<n); } for (j=0 ; j<n; j++) { k = 3 - edge[j] - edge[j+n]; a = board[j][k*2 ]; b = board[j][k*2 +1 ]; solu[j][4 ] = a; solu[j][5 ] = b; } for (i=0 ; i<n; i++) { for (j=0 ; j<6 ; j++) cout << color[solu[i][j]] << " " ; cout << endl; } } void search () { int i, t, cube; bool ok, newg, over; int *vert = new int [n]; int *edge = new int [n*2 ]; for (i=0 ; i<n; i++) vert[i] = 0 ; t = -1 ; newg = true ; while (t > -2 ) { t++; cube = t % n; if (newg) edge[t] = -1 ; over = false ; ok = false ; while (!ok && !over) { edge[t]++; if (edge[t]>2 ) over = true ; else ok = (t<n || edge[t]!=edge[cube]); } if (!over) { if (++vert[board[cube][edge[t]*2 ]] > 2 +t/2 *2 ) ok = false ; if (++vert[board[cube][edge[t]*2 +1 ]] > 2 +t/2 *2 ) ok = false ; if (t%n == n-1 && ok) for (i=0 ; i<n; i++) if (vert[i] > 2 +t/n*2 ) ok = false ; if (ok) { if (t == n*2 -1 ) { ans++; out (edge); return ; } else newg = true ; } else { --vert[board[cube][edge[t]*2 ]]; --vert[board[cube][edge[t]*2 +1 ]]; t--; newg = false ; } } else { t--; if (t > -1 ) { cube = t % n; --vert[board[cube][edge[t]*2 ]]; --vert[board[cube][edge[t]*2 ]]; } t--; newg = false ; } } } int main () { ifstream fin ("input.txt" ) ; cout << "输入立方体个数:" ; fin >> n; cout << n; cout << "\n输入颜色:" ; for (int i=0 ; i<n; i++) { fin >> color[i]; cout << color[i] << " " ; } cout << "\n输入立方体各面颜色:\n" ; for (int i=0 ; i<n; i++) { for (int j=0 ; j<6 ; j++) { fin >> board[i][j]; cout << board[i][j] << " " ; } cout << endl; } cout << "\n立方体叠置方案为:\n" ; search (); if (ans == 0 ) cout << "No Solution!\n" ; cout << end; fin.close (); return 0 ; }
5-8 整数变换问题
这道题是一定能找到的,有点类似于3n+1?,要不然会要求输出no solution
的,而且我们也不能简单地通过n和m的大小关系来得到此时需要做的操作。题目的思路大概就是DFS暴力搜索,左子树f变换,右子树g变换,主要问题在于需要限制搜索的层数,因为有些分支是得不到解的
1. 解空间和解结构 解空间是一个长度为k的向量,向量的每一项是f或者g,k的大小与输入有关,解结构如下:左子树是进行f变换,右子树是进行g变换。
2. 剪枝策略 虽然看到一些博客根据n和m的大小剪枝,但是实际上在代码中他们没有这样做,而且这种做法应该是不对的。。。
我觉得这里更加类似于DFS,代码也没体现出什么剪枝的策略,这里的搜索过程有点像是一层一层往下搜索,k控制着层数
3. 示例
4. 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include <bits/stdc++.h> using namespace std;#define N 25 #define inf 0x3f3f3f3f int n, m;int k = 1 ;queue<char > q; bool DFS (int x, int n) { if (x > k) return false ; if (n == m) return true ; int temp = n; for (int i = 0 ; i < 2 ; i++) { if (i == 0 ) temp = n * 3 ; else temp = n / 2 ; if (DFS (x+1 , temp)) { if (i == 0 ) q.push ('f' ); else q.push ('g' ); return true ; } } return false ; } int main () { cin >> n >> m; while (!DFS (0 , n)) k++; cout << k << endl; while (!q.empty ()) { cout << q.front (); q.pop (); } return 0 ; }
5. 时间复杂度 树的层数和输入的数关系比较大,不太好分析。。。,如果树的层数可以得到一个上界的话,可以推导一下最坏情况
参考:https://blog.csdn.net/Small___ming/article/details/103218990
5-9 拉丁矩阵问题
可以从左到右从上到下来填充矩阵的每个“格子”,即树的每一层就是考虑每个格子里放什么,这将得到一棵比较大的树,剪枝的策略就是题目所要求的,每行每列都没有相同的形状,若在该格子里放入第i种宝石能满足要求,则将其放入,否则剪去该条分支,对于检查每行每列是否有相同形状,可以采用两个矩阵来记录,这样时间效率会高点
1. 解空间和解结构 本题是求可行解的个数,解空间是一个n行m列的矩阵,也可以理解为n*m长度的向量,其中向量的每个元素为1~n,对应的解结构为排列树
2. 剪枝策略 维护矩阵row[N][N],其中row[i][j]=1表示第i行已经使用了形状j,矩阵col[N][N],其中col[j][i]=1表示第i列使用了形状j,考虑在为矩阵(x,y)位置上元素选择宝石时,若选择宝石i,则需要保证row[x][i]==0&&col[i][y]==0
,即不与x行和y列上已有的形状冲突,若不满足该条件,则可剪去该子树
3. 示例
4. 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <stdio.h> #define N 10 int m,n; int count=0 ;int a[N][N]={0 };int row[N][N]={0 };int col[N][N]={0 };void backtrack (int t) { int i,j; int x,y; if (t==m*n) { count++; return ; } x=t/n; y=t%n; for (i=1 ;i<=n;i++) { if (row[x][i]==0 &&col[i][y]==0 ) { row[x][i]=1 ; col[i][y]=1 ; a[x][y]=i; backtrack (t+1 ); row[x][i]=0 ; col[i][y]=0 ; a[x][y]=0 ; } } } int main () { scanf ("%d%d" ,&m,&n); backtrack (0 ); printf ("count=%d\n" ,count); return 0 ; }
5. 时间复杂度 考虑最坏情况,对于叶子节点,其时间消耗为$O(1)$,对于非叶子节点其时间消耗为$O(n)$,对于n种宝石,排成n行m列的拉丁矩阵问题,时间消耗为$T=O(n^{mn})$
5-10 排列宝石问题
这道题是类似于5-9的(直接在5-9上面改的几行),说一下思路吧:类似于对待不同宝石类型要求每行每列都不重复,引入数组color_row
和数组color_col
,其大致结构和含义与row
和col
类似,用于记录该行或者该列是否已经使用过该种颜色的宝石,还引入used
数组用于满足题中的每种宝石n颗且不同色(避免一种宝石的一种颜色使用了多次),其中used[i][j]=1
表示类型i的宝石的第j种颜色已用。
与5-9对比,排列树可选的分支大大增加了,不过剪去的分支也比较多,时间复杂度往上蹭
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #include <bits/stdc++.h> #define N 10 using namespace std;int n; int res=0 ;int row[N][N]={0 };int col[N][N]={0 };int color_row[N][N]={0 };int color_col[N][N]={0 };int used[N][N]={0 };void backtrack (int t) { int i,j; int x,y; if (t==n*n) { res++; return ; } x=t/n; y=t%n; for (i=1 ;i<=n;i++) { for (j=1 ;j<=n;j++){ if (!row[x][i]&&!col[i][y]&&!color_row[x][j]&&!color_col[j][y]&&!used[i][j]) { row[x][i]=1 ; col[i][y]=1 ; color_row[x][j]=1 ; color_col[j][y]=1 ; used[i][j]=1 ; backtrack (t+1 ); row[x][i]=0 ; col[i][y]=0 ; color_row[x][j]=0 ; color_col[j][y]=0 ; used[i][j]=0 ; } } } } int main () { scanf ("%d" ,&n); backtrack (0 ); printf ("res=%d\n" ,res); return 0 ; }
5-11 重复拉丁矩阵问题
这题是类似于5-9的,我的代码一直没AC,参考了下答案的,思路是:第一行按照题意,由于对每行每种宝石数量的限制,且要求第一行最小字典序,所以是固定的,无需回溯,故从第二行开始进行搜索。答案遵照教材的排列树的写法,先初始化矩阵,然后再做swap操作,矩阵初始化情况如虚线上矩阵(示例为例),在搜索的时候,只需要考虑每列宝石要求即可(因为初始化的原因,不需要考虑每行了),但是很奇怪的是,代码没有考虑第一列的情况了,即认为第一列也是固定的,但是我觉得不太对,比如虚线下面的一种也是可以的。
1 2 3 4 5 6 7 8 9 10 省略第0行、第0列 1 1 2 2 3 3 3 1 <1> 2 2 3 3 3 2 1 1 2 3 3 3 2 1 2 1 3 3 3 ------------- 1 1 2 2 3 3 3 3 3 3 2 2 1 1 3 3 3 1 2 2 1 3 3 2 1 3 1 2
input:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 #include <iostream> #include <fstream> #include <algorithm> using namespace std;const int MAX = 50 ;int n, m, k;int times[MAX]; int id[MAX]; int board[MAX][MAX]; bool ok (int r, int c, int s) { int k = board[r][s]; int i; if (s > c) for (i=c; i<s; i++) if (board[r][i] == k) return false ; int count = 0 ; for (i=1 ; i<r; i++) if (board[i][c] == k) count++; if (count >= times[k]) return false ; else return true ; } double num = 0 ; void backtrack (int r, int c) { for (int i=c; i<=n; i++) if (ok (r, c, i)) { swap (board[r][c], board[r][i]); if (c == n) { if (r == m) { num += 1 ; return ; } else backtrack (r+1 , 2 ); } else backtrack (r, c+1 ); swap (board[r][c], board[r][i]); } } int main () { ifstream fin ("input.txt" ) ; cout << "\n输入行数m:" ; fin >> m; cout << m; cout << "\n输入列数n:" ; fin >> n; cout << n; cout << "\n输入宝石价值种数:" ; fin >> k; cout << k << endl; int i, temp; int t = 1 ; for (i=1 ; i<=k; i++) { cout << "输入第" << i << "种宝石在每行每列出现的最多次数:" ; fin >> times[i]; cout << times[i] << "\n" ; temp = times[i]; while (temp>0 ) { id[t++] = i; temp--; } } int j; for (i=1 ; i<=m; i++) for (j=1 ; j<=n; j++) board[i][j] = id[j]; for (i=2 ; i<=n; i++) swap (board[i][1 ],board[i][i]); backtrack (2 , 2 ); cout << "\n不同的宝石排列方案数为:" << num; cout << endl << endl; return 0 ; }
下面是我的代码,在5-9基础上改的,输出的文件out.txt是所有的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <bits/stdc++.h> #define N 10 using namespace std;int m,n,k; int res=0 ;int a[N][N]={0 };int row[N][N]={0 };int col[N][N]={0 };int maxtime[N]={0 };void backtrack (int t,ofstream&out) { int x,y; if (t==m*n) { res++; out<<"---" <<res<<endl; for (int i=0 ;i<m;i++) { for (int j=0 ;j<n;j++) { out<<a[i][j]<<" " ; } out<<endl; } out<<endl; return ; } x=t/n; y=t%n; for (int i=1 ;i<=k;i++) { bool flag=true ; if (x==0 &&y>0 &&i<a[x][y-1 ]) flag=false ; if (y==0 &&x>0 &&i<a[x-1 ][y]) flag=false ; if (flag&&row[x][i]<maxtime[i]&&col[i][y]<maxtime[i]) { row[x][i]++; col[i][y]++; a[x][y]=i; backtrack (t+1 ,out); row[x][i]--; col[i][y]--; a[x][y]=0 ; } } } int main () { ofstream out; out.open ("out.txt" ); scanf ("%d%d%d" ,&m,&n,&k); for (int i=1 ;i<=k;i++) scanf ("%d" ,&maxtime[i]); backtrack (0 ,out); printf ("res=%d\n" ,res); out.close (); return 0 ; }
5-12 罗密欧与朱丽叶的迷宫问题
从罗密欧的位置开始,每次都可以走八个方向,用数字记录方向,若下一次走的方向与直接的不同则记转向次数加一,在考虑进入八个方向的下一位置前,需要考虑该位置是否越界,是否是封闭位置,是否已经走过,且是否走入后当前转向次数大于当前最优解此时,以此作为剪枝策略,当遍历完所有的位置后,若到达了朱丽叶的位置,则考虑更新当前最优解或者是增加最优解的次数
1. 解空间和解结构 对于n行m列,封闭房间数为k的迷宫,迷宫问题的解空间为1~n*m-k的全排列,表示某个房间第几次到达,解结构为排列树
2. 剪枝策略 约束函数:在尝试走入下一位置前,检查该位置是否合法,即是否数组越界,是否为封闭房间,是否已经走过,若不合法,则剪去该子树
限界函数:维护变量curr_rotation表示当前转向次数,min_rotation表示当前最优解的转向次数,若在考虑下一位置时,到达该位置后的转向次数大于最优解的转向次数,则剪去该子树
3. 代码 input:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 #include <bits/stdc++.h> using namespace std;struct Point { int x, y; }; Point luo; Point ye; Point pos; int dx[8 ] = { 1 , 0 , -1 , 0 , 1 , 1 , -1 , -1 }; int dy[8 ] = { 0 , 1 , 0 , -1 , 1 , -1 , 1 , -1 };const int MAX = 10 ;int n, m, k;int board[MAX][MAX];int best[MAX][MAX];int curr_rotation = 0 ; int min_rotation = 100000 ; int min_count = 0 ; bool flag = false ;bool Point_check (Point pos) { if (pos.x > 0 && pos.x <= n && pos.y > 0 && pos.y <= m && board[pos.x][pos.y] == 0 ) return true ; return false ; } void upgrade () { for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) best[i][j] = board[i][j]; flag = true ; } void traceBack (int depth, Point pos, int di) { if (depth == m * n - k && pos.x == ye.x && pos.y == ye.y && curr_rotation <= min_rotation) { if (curr_rotation < min_rotation) { min_rotation = curr_rotation; min_count = 1 ; upgrade (); } else { min_count++; } return ; } else { for (int i = 0 ; i < 8 ; i++) { Point next_pos; next_pos.x = pos.x + dx[i]; next_pos.y = pos.y + dy[i]; if (Point_check (next_pos)) { board[next_pos.x][next_pos.y] = depth + 1 ; if (depth > 1 && di != i) curr_rotation++; if (curr_rotation <= min_rotation) traceBack (depth + 1 , next_pos, i); board[next_pos.x][next_pos.y] = 0 ; if (depth > 1 && di != i) curr_rotation--; } } } } int main () { memset (board, 0 , sizeof (board)); memset (best,0 ,sizeof (board)); ifstream datain ("input.txt" ) ; cout << "输入迷宫的宽度:" ; datain >> n; cout << n<<endl; cout << "\n输入迷宫的长度:" ; datain >> m; cout << m << endl; cout << "\n输入封闭房间个数:" ; datain >> k; cout << k << endl; Point forbidden_rooms; for (int i = 0 ; i < k; i++) { datain >> forbidden_rooms.x >> forbidden_rooms.y; board[forbidden_rooms.x][forbidden_rooms.y] = -1 ; } datain >> luo.x >> luo.y; cout << endl << "罗密欧位置坐标:[" << luo.x << ", " << luo.y << "]" << endl; board[luo.x][luo.y] = 1 ; datain >> ye.x >> ye.y; cout << endl << "朱丽叶位置坐标:[" << ye.x << ", " << ye.y << "]" << endl; traceBack (1 , luo, 0 ); ofstream dataout ("output_data1.txt" , ios::trunc) ; if (flag) { cout << "\n最小转弯次数:" << min_rotation << endl; cout << "\n最小转弯次数的转弯道路数:" << min_count << endl; cout << "行走的路线图:" << endl; for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= m; j++) { cout << best[i][j] << " " ; } cout << endl; } } else { for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= m; j++) { cout << board[i][j] << " " ; } cout << endl; } cout<< "\nNo Solution!" << endl; } return 0 ; }
5-13 工作分配问题
由于每个工作仅由一人完成,每个人仅做一个工作,对于示例的输入矩阵,所求即为在每一行取一个数,且保证所取数不在同一列,求他们的和最小值,易知如果用暴力解法,设矩阵的行数为n,则解为n个数的排列组合,其中第i个数表示第i个工作分配给第j个人,则对于上述输入矩阵,可以得到可行解(3,2,1),(2,1,3),……
1. 解空间和解结构 对于n个工作的工作分配问题,其解空间即为1~n的全排列,解结构为排列树,具体而言树的第i层表示第i个工作的分配,每层的顶点表示该工作分配的人,如红色方框框出的节点即表示第1个工作分配给C1
2. 剪枝策略 在搜索的过程中记录当前费用cv和最优费用bestv,若cv大于等于bestv,则无需再遍历该节点的子树(因为向下搜索的过程费用一定是单调递增的)
3. 示例 分成两幅图来画了,实际上是一幅图
4. 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #include <bits/stdc++.h> using namespace std;const int N=10010 ;int num;int Min=INT_MAX;int arr[N][N];int sum=0 ;int state[N];void dfs (int t) { if (t>=num) { if (Min>sum) { Min=sum; return ; } } for (int i=0 ;i<num;i++) { if (!state[i]) { state[i]=1 ; sum+=arr[t][i]; if (sum<Min) dfs (t+1 ); state[i]=0 ; sum-=arr[t][i]; } } } int main () { ifstream in; ofstream out; in.open ("input.txt" ); out.open ("output.txt" ); in>>num; Min=INT_MAX; sum=0 ; for (int i=0 ;i<num;i++) { for (int j=0 ;j<num;j++) { in>>arr[i][j]; } state[i]=0 ; } dfs (0 ); cout<<"最小费用为:" <<Min<<endl; out<<"最小费用为:" <<Min<<endl; in.close (); out.close (); return 0 ; }
5. 时间复杂度 考虑最坏情况,对于非叶子节点,其搜索所需时间为$O(n)$,对于叶子节点,其搜索所需时间为$O(1)$,故对于n个工作的工作分配问题,其时间消耗$T=n(n+n n-1+…+n!)+n!=O(n!*n)$
5-14 布线问题
经典排列树,然后剪枝策略可以维护一个当前最优解的成本,若当前成本大于等于该变量,则剪去该分支。
1. 解空间和解结构 对于n个元件的布线问题,其解空间为1~n的全排列,解结构为排列树
2. 剪枝策略 维护当前最优解的成本bestv
和当前费用cv
,在考虑线路板t位置上是否放置元件i时,若当前费用加上放置元件i所带来的成本小于当前最优解的成本,则可继续搜索,否则剪去该分支
3. 示例
4. 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #include <bits/stdc++.h> using namespace std;const int N=25 ;int n;int conn[N][N];int bestx[N];int bestv=INT_MAX;int cv;int cx[N];int currentvalue (int cx[],int t) { int res=0 ; for (int i=0 ;i<t;i++) { res=res+conn[cx[i]][cx[t]]*(t-i); } return res; } void traceback (int t) { if (t==n) { if (cv<bestv) { for (int i=0 ;i<n;i++) { bestx[i]=cx[i]; } bestv=cv; } return ; } for (int i=t;i<n;i++) { swap (cx[t],cx[i]); int value=currentvalue (cx,t); if (value+cv<bestv) { cv+=value; traceback (t+1 ); cv-=value; } swap (cx[t],cx[i]); } } int main () { cin>>n; for (int i=1 ;i<n;i++) { for (int j=i+1 ;j<=n;j++) { cin>>conn[i][j]; conn[j][i]=conn[i][j]; } } for (int i=0 ;i<n;i++) cx[i]=i+1 ; traceback (0 ); cout<<bestv<<endl; for (int i=0 ;i<n;i++) cout<<bestx[i]<<" " ; return 0 ; }
5. 时间复杂度 考虑最坏情况,对于n个元件的布线问题,对于非叶子节点,其消耗的时间与其层数t有关,为$O(n-t)$,对于叶子节点,其消耗的时间为$O(n)$,故该问题时间消耗为:$T=nn+n (n-1)(n-1)+n (n-1)(n-2) (n-2)+…+n!+n!n=O(n! n)$
5-15 最佳调度问题 可以一一为每个任务分配工作,每个任务都可以尝试分配给每台机器,从而对排列树进行搜索。用一个数组存储每台机器已经分配的任务的总耗时,搜索到可行解后这个数组中的最大值即为当前解所需时间,在剪枝的过程中,将更新后该数组的值(即表示选择分配给该机器)与当前最优解的耗时比较,若小于则可继续搜索。
1. 解空间和解结构 对于n个任务k台机器的最佳调度问题,其解空间为长度为n的向量,向量的每一项为1~k,解结构为排列树
2. 剪枝策略 按照题意,要求完成全部任务的时间最早,故维护变量mintime
表示当前最优解的完成时间,数组ans
存储每台机器完成当前任务后的时间,在任务分配过程中,尝试将任务i分配给机器j,若t[i]+ans[j]>=mintime
,则将该分支剪去,无需再搜索
3. 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <iostream> #include <algorithm> using namespace std;int n,k,t[99 ],ans[99 ],min_time=0x3f3f3f3f ;void dfs (int level) { if (level==n) { int tmp=*max_element (ans,ans+n); if (tmp<min_time) min_time=tmp; return ; } for (int i=0 ;i<k;i++) { ans[i]+=t[level]; if (ans[i]<min_time) dfs (level+1 ); ans[i]-=t[level]; } } int main () { cin>>n>>k; for (int i=0 ;i<n;i++) cin>>t[i]; dfs (0 ); cout<<min_time<<endl; }
4. 时间复杂度 考虑最坏情况,对于非叶子节点,其时间消耗为$O(k)$,对于叶子节点,其时间消耗为$O(n)$,故对于n个任务k台机器的最佳调度问题,其时间消耗为$T=k(k+k^2+…+k^{n-1})+n k^n=O(n*k^n)$
5-16 无优先级运算问题
有点类似于整数变换问题,这里也是不知道树的层数,可以从1个数,2个数,…,n个数逐步增加树的层数,在选择数的时候,只能使用未选过的数,以此作为剪枝策略(可以用过数组记录某个数是否选过),使用数组记录该数右侧的运算符,每种符号的选择均作为分支向下搜索,当到达叶子节点时,检查当前运算结果是否为m,若为m,则不继续搜索,程序结束
1. 解空间和解结构 对于n个正整数的无优先级运算问题,其解空间为长度为2k-1(k从1到n)的向量,该向量的每一个奇数项为1~n的数且相互之间不重复,偶数项为1、2、3、4,分别表示+,-,*,\
,解的结构为排列树,具体而言如下所示:
2. 剪枝策略 维护数组flag
表示n个数是否使用,flag[i]=1
表示第i个数已经使用,当给第dep位数选择数时,若该数已经使用过,则无法选择,剪去该分支
3. 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 #include <iostream> using namespace std;const int N=100 ;int n,m;int a[N];int num[N];int oper[N];int flag[N];int k;bool found () { int x=num[0 ]; for (int i=0 ;i<k;i++) { switch (oper[i]){ case 0 :x+=num[i+1 ]; break ; case 1 :x-=num[i+1 ]; break ; case 2 :x*=num[i+1 ]; break ; case 3 :x/=num[i+1 ]; break ; } } return (x==m); } bool traceback (int dep) { if (dep>k){ if (found ()) return true ; else return false ; } for (int i=0 ;i<n;i++) { if (flag[i]==0 ) { num[dep]=a[i]; flag[i]=1 ; for (int j=0 ;j<4 ;j++) { oper[dep]=j; if (traceback (dep+1 )) return true ; } flag[i]=0 ; } } return false ; } void out () { for (int i=0 ;i<k;i++) { cout<<num[i]; switch (oper[i]){ case 0 :cout<<"+" ;break ; case 1 :cout<<"-" ;break ; case 2 :cout<<"*" ;break ; case 3 :cout<<"/" ;break ; } } cout<<num[k]; return ; } int main () { cin>>n>>m; for (int i=0 ;i<n;i++) { cin>>a[i]; flag[i]=0 ; } for (k=1 ;k<=n;k++) { if (traceback (0 )) { cout<<k<<endl; out (); return 0 ; } } cout<<"No Solution!" <<endl; return 0 ; }
4. 时间复杂度 考虑最坏情况,需要n个数,每个非叶子节点的时间消耗为$O(n)$,每个叶子节点的时间消耗为$O(n)$,此时时间消耗为$T=n(1+n+4n+4 n^2+4^2n^2+…+4^{n-1} n^{n})=O(n^nn 4^{n-1})$,真的很大。。。
5-17 世界名画陈列馆问题
input:
output:
1 2 3 4 5 4 0 0 1 0 1 0 0 0 0 0 0 1 0 1 0 0
这题的思路参考了书的答案,他的剪枝策略有点复杂
1. 解空间和解结构 解空间为长度为n*m的向量,向量的每一项为0或者1,表示是否放置警卫,解结构为子集树
2. 剪枝策略
可以证明,当前访问的格点(i,j)已被监视时,放置在(i,j)的情况一定不会比放置在(i+1,j+1)的情况好.当(i+1,j+1)不在网格中时,(i+1,j)和(i,j+1)同理.所以,如果(i,j)已被监视,则不需要在此处放置机器人,直接跳过即可.(update,证明:因为是从上到下从左到右使得格子进入监视状态,当前正在检查访问(i,j)。说明在这之前的(i-1,j)和(i,j-1)已被监视。此时如果放在(i,j)处,只会使得(i+1,j)和(i,j+1)进入访问状态。而如果放在(i+1,j+1)处,显然在完成上述目标的情况下可以使更多格子进入访问状态)
当(i,j)未被监视时,若(i,j+1)已被监视,则在(i,j)放置一定不会比在(i+1,j)放置的情况好.所以当且仅当(i,j)在网格右下角或者(I,j+1)未被监视时才考虑放置在(i,j)的情况.
当(i,j)未被监视时,若(i,j+1)和(i,j+2)均被监视,则在(i+1,j)放置一定不会比在(i+1,j)放置的情况好,所以当且仅当(i,j+1)或(i,j+2)未被监视时才考虑放置在(i,j+1)的情况.
当i=n时,不考虑放置在(i+1,j)的情况.
记录已经监视的格点数,(当前最优值减去当前已放置个数)*5如果小于未监视的格点数,则一定达不到比当前最优值更好的情况,剪去.
类似于(5),考虑更紧的情况,并非每个机器人都能独立监视5个格点,至少会有m/4+5的冗余,这个剪枝仅适用于i<n-1的情况,因为最后两行由于最优值和已放置个数非常接近,总是达不到这个值.
大概就是分为下界剪枝法和控制剪枝法两种,下界剪枝法就是计算剩余需要的警卫数,然后加上当前警卫数和当前最优解警卫数比较,控制剪枝法就是一堆已经证明的放置策略,如2、3、4,
3. 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 #include <bits/stdc++.h> using namespace std;const int N=100 ;int d[6 ][3 ]={{0 ,0 ,0 },{0 ,0 ,0 },{0 ,0 ,-1 },{0 ,-1 ,0 },{0 ,0 ,1 },{0 ,1 ,0 }};int x[N][N];int y[N][N];int bestx[N][N];int n,m,best,k=0 ,t=0 ,t1,t2,more;bool p;void place (int i,int j) { x[i][j]=1 ; k++; for (int s=1 ;s<=5 ;s++) { int p=i+d[s][1 ]; int q=j+d[s][2 ]; y[p][q]++; if (y[p][q]==1 ) t++; } } void noplace (int i,int j) { x[i][j]=0 ; k--; for (int s=1 ;s<=5 ;s++) { int p=i+d[s][1 ]; int q=j+d[s][2 ]; y[p][q]--; if (y[p][q]==0 ) t--; } } void traceback (int i,int j) { do { j++; if (j>m){ i++; j=1 ; } }while (!(y[i][j]==0 ||i>n)); if (i>n){ if (k<best){ best=k; for (int i=1 ;i<=m;i++) { for (int j=1 ;j<=n;j++) { bestx[i][j]=x[i][j]; } } } return ; } if (k+(t1-t)/5 >=best) return ; if ((i<n-1 )&&(k+(t2-t)/5 )>=best) return ; if (i<n){ place (i+1 ,j); traceback (i,j); noplace (i+1 ,j); } if (j<m&&(y[i][j+1 ]==0 ||y[i][j+2 ]==0 )){ place (i,j+1 ); traceback (i,j); noplace (i,j+1 ); } if ((y[i+1 ][j]==0 &&y[i][j+1 ]==0 )) { place (i,j); traceback (i,j); noplace (i,j); } } void output () { cout<<best<<endl; for (int i=1 ;i<=m;i++) { for (int j=1 ;j<=n;j++) { cout<<bestx[i][j]<<" " ; } cout<<endl; } } void solve () { more=m/4 +1 ; if (m%4 ==3 ) more++; else if (m%4 ==2 ) more+=2 ; t2=m*n+more+4 ; t1=m*n+4 ; best=INT_MAX; memset (y,0 ,sizeof (y)); memset (x,0 ,sizeof (x)); if (n==1 &&m==1 ) { cout<<1 <<endl<<1 <<endl; return ; } for (int i=0 ;i<=m+1 ;i++) { y[0 ][i]=1 ; y[n+1 ][i]=1 ; } for (int i=0 ;i<=n+1 ;i++) { y[i][0 ]=1 ; y[i][m+1 ]=1 ; } traceback (1 ,0 ); output (); } int main () { cin>>m>>n; solve (); return 0 ; }
5-18 世界名画陈列馆问题(不重复监视)
在5-17上改了下,框架没变,去掉了答案那些控制剪枝法的剪枝策略,加上了题目中要求的不重复监视的剪枝策略,且在叶子节点处加上了对整个矩阵检查是否全部监视了,主要加了check函数,checkall函数,改了traceback函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 #include <bits/stdc++.h> using namespace std;const int N=100 ;int d[6 ][3 ]={{0 ,0 ,0 },{0 ,0 ,0 },{0 ,0 ,-1 },{0 ,-1 ,0 },{0 ,0 ,1 },{0 ,1 ,0 }};int x[N][N];int y[N][N];int bestx[N][N];int n,m,best,k=0 ,t=0 ,t1,t2,more;bool p;void place (int i,int j) { x[i][j]=1 ; k++; for (int s=1 ;s<=5 ;s++) { int p=i+d[s][1 ]; int q=j+d[s][2 ]; y[p][q]++; if (y[p][q]==1 ) t++; } } bool check (int i,int j) { for (int s=1 ;s<=5 ;s++) { int p=i+d[s][1 ]; int q=j+d[s][2 ]; if (y[p][q]) { return false ; } } return true ; } bool checkall () { for (int i=1 ;i<=m;i++) { for (int j=1 ;j<=n;j++) { if (!y[i][j]) return false ; } } return true ; } void noplace (int i,int j) { x[i][j]=0 ; k--; for (int s=1 ;s<=5 ;s++) { int p=i+d[s][1 ]; int q=j+d[s][2 ]; y[p][q]--; if (y[p][q]==0 ) t--; } } void traceback (int i,int j) { do { j++; if (j>m){ i++; j=1 ; } }while (!(y[i][j]==0 ||i>n)); if (i>n){ if (k<best&&checkall ()){ best=k; for (int i=1 ;i<=m;i++) { for (int j=1 ;j<=n;j++) { bestx[i][j]=x[i][j]; } } } return ; } if (k+(t1-t)/5 >=best) return ; if ((i<n-1 )&&(k+(t2-t)/5 )>=best) return ; if (check (i,j)) { place (i,j); traceback (i,j); noplace (i,j); } traceback (i,j); } void output () { cout<<best<<endl; for (int i=1 ;i<=m;i++) { for (int j=1 ;j<=n;j++) { cout<<bestx[i][j]<<" " ; } cout<<endl; } } void solve () { more=m/4 +1 ; if (m%4 ==3 ) more++; else if (m%4 ==2 ) more+=2 ; t2=m*n+more+4 ; t1=m*n+4 ; best=INT_MAX; memset (y,0 ,sizeof (y)); memset (x,0 ,sizeof (x)); if (n==1 &&m==1 ) { cout<<1 <<endl<<1 <<endl; return ; } traceback (1 ,0 ); output (); } int main () { cin>>m>>n; solve (); return 0 ; }
5-19 算m点问题
类似于5-16,但是这里一定用到了k个整数,所以只需要稍微改改(把k设置为n-1然后改输出),而且这种计算方式可以看成是无优先级的
示例应该给错了,应该是下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 #include <iostream> using namespace std;const int N=100 ;int n,m;int a[N];int num[N];int oper[N];int flag[N];int k;bool found () { int x=num[0 ]; for (int i=0 ;i<k;i++) { switch (oper[i]){ case 0 :x+=num[i+1 ]; break ; case 1 :x-=num[i+1 ]; break ; case 2 :x*=num[i+1 ]; break ; case 3 :x/=num[i+1 ]; break ; } } return (x==m); } bool traceback (int dep) { if (dep>k){ if (found ()) return true ; else return false ; } for (int i=0 ;i<n;i++) { if (flag[i]==0 ) { num[dep]=a[i]; flag[i]=1 ; for (int j=0 ;j<4 ;j++) { oper[dep]=j; if (traceback (dep+1 )) return true ; } flag[i]=0 ; } } return false ; } void out () { int ans=num[0 ]; for (int i=0 ;i<k;i++) { cout<<ans; switch (oper[i]){ case 0 :cout<<"+" ;ans+=num[i+1 ];break ; case 1 :cout<<"-" ;ans-=num[i+1 ];break ; case 2 :cout<<"*" ;ans*=num[i+1 ];break ; case 3 :cout<<"/" ;ans/=num[i+1 ];break ; } cout<<num[i+1 ]<<"=" <<ans<<" " ; } return ; } int main () { cin>>n>>m; for (int i=0 ;i<n;i++) { cin>>a[i]; flag[i]=0 ; } k=n-1 ; if (traceback (0 )) { out (); return 0 ; } cout<<"No Solution!" <<endl; return 0 ; }
5-20 部落卫队问题
类似于0-1背包问题,子集树,左子树为1,右子树为0,约束函数就是不能和当前卫队里已有的居民为仇敌,若为仇敌,则剪去左子树。限界函数是当前卫队里的人数加上除去当前考虑的居民的剩余的居民,若数量还是比best小,则剪去右子树
1. 解空间和解结构 对于n个居民的部落卫队问题,其解空间为长度为n的向量,向量的每一项为0或者1,解结构为子集树
2. 剪枝策略 考虑居民t是否加入卫队,加入卫队即进入左子树,否则进入右子树
约束函数:若居民t和1~t-1中已经加入卫队的居民直接有仇视关系,则t不能加入卫队,剪去左子树
限界函数:若居民t不加入卫队,维护变量cv为当前卫队中居民人数,则卫队未来可能的最多人数为cv+n-t
,若该值大于n,才搜索右子树,否则剪去右子树
3. 代码 input:
1 2 3 4 5 6 7 8 9 10 11 7 10 1 2 1 4 2 4 2 3 2 5 2 6 3 5 3 6 4 5 5 6
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <iostream> using namespace std;const int N=100 ;int n,m;int relation[N][N];int cbest=0 ;int bestx[N];int cv=0 ;int x[N];void traceback (int t) { if (t==n) { if (cv>cbest) { for (int i=0 ;i<n;i++) { bestx[i]=x[i]; cbest=cv; } } return ; } bool flag=true ; for (int i=0 ;i<t;i++) { if (x[i]==1 &&relation[t+1 ][i+1 ]==1 ) { flag=false ; break ; } } if (flag) { x[t]=1 ; cv++; traceback (t+1 ); x[t]=0 ; cv--; } if (cv+n-t>cbest) { traceback (t+1 ); } } int main () { cin>>n>>m; int t1,t2; for (int i=0 ;i<m;i++) { cin>>t1>>t2; relation[t1][t2]=1 ; relation[t2][t1]=1 ; } traceback (0 ); cout<<cbest<<endl; for (int i=0 ;i<n;i++) { cout<<bestx[i]<<" " ; } return 0 ; }
4. 时间复杂度 考虑最坏情况,对于非叶子节点,搜索所需时间为$O(t+1)$,其中t为当前的层数,对于叶子节点,其搜索所需时间为$O(n)$,故居民数量为n的部落卫队问题的时间复杂度为$O(n*2^n)$
5-21、5-22 装载问题和0-1背包问题,见书上例题~
5-23 圆排列问题
排列树,在选择一个圆的时候,就能计算他带来的长度(需要单独考虑第一个和最后一个的半径带来的长度),维护当前最优解,若当前解小于最优解才继续搜索,看看代码大概就能懂啦(突然发现书上有这道例题,也可以看看书上的)
1. 解空间和解结构 n个圆的圆排列问题的解空间为1~n的全排列,解结构为排列树
2. 剪枝策略 维护变量minlen
表示当前最优解的长度,clen
表示当前长度,当考虑放置第t个圆时,计算其带来的长度l
,若l+clen>=minlen
,则剪去该子树
3. 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <bits/stdc++.h> using namespace std;const int N=100 ;int n;double r[N];double minlen=DBL_MAX;double clen=0 ;int x[N];double bring (int t) { if (t==0 ) return r[x[t]]; else { double res=0 ; res=sqrt ((r[x[t]]+r[x[t-1 ]])*(r[x[t]]+r[x[t-1 ]])-abs (r[x[t]]-r[x[t-1 ]])*abs (r[x[t]]-r[x[t-1 ]])); if (t==n-1 ) res+=r[x[t]]; return res; } } void traceback (int t) { if (t==n) { if (clen<minlen) minlen=clen; return ; } for (int i=t;i<n;i++) { swap (x[t],x[i]); double ctmp=clen; if (clen+bring (t)<minlen) { clen=clen+bring (t); traceback (t+1 ); clen=ctmp; } swap (x[t],x[i]);t } } int main () { cin>>n; for (int i=0 ;i<n;i++) { cin>>r[i]; x[i]=i; } traceback (0 ); cout<<minlen; return 0 ; }
4. 时间复杂度 考虑最坏情况,对于非叶子节点,搜索所需时间为$O(n-t)$,t为叶子节点的层数,对于叶子节点,搜索所需时间为$O(1)$,故n个圆的圆排列问题的最坏情况下时间复杂度为$O(n!)$
5-24 图着色问题,见书上例题~
5-25 最短加法链问题
类似于整数变换那题,也是树的层数未知,所以可以用变量控制层数慢慢加大,可以采用无优先级运算问题的类似框架去解这道题,不同的是这题随着层数的加大分支数也加大了,剪枝策略是当前考虑的项不能大于23,看看代码就差不多~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <iostream> using namespace std;const int N=100 ;int n;int x[N];int k;bool traceback (int t) { if (t==k) { if (x[k-1 ]==n) return true ; else return false ; } for (int i=0 ;i<t;i++) { for (int j=0 ;j<t;j++) { if (x[i]+x[j]<=n) { x[t]=x[i]+x[j]; if (traceback (t+1 )) { return true ; } } } } return false ; } int main () { cin>>n; x[0 ]=1 ; for (k=1 ;k<=n;k++) { if (traceback (1 )) { cout<<k-1 <<endl; for (int i=0 ;i<k;i++) { cout<<x[i]<<" " ; } return 0 ; } } return 0 ; }
完结撒花🎉~