第六章 回溯法
一、基本思想
1.解空间
2.确定易于搜索的解空间结构
解
空
间
结
构
{
子
集
树
(
O
(
2
n
)
)
排
列
数
O
(
n
!
)
解空间结构\begin{cases} 子集树(O(2^n))\\ 排列数O(n!) \end{cases}
解空间结构{子集树(O(2n))排列数O(n!)
3.深度优先方式搜索解空间结构,同时用剪枝函数进行剪枝
剪
枝
函
数
{
约
束
函
数
界
限
函
数
剪枝函数\begin{cases} 约束函数\\ 界限函数 \end{cases}
剪枝函数{约束函数界限函数
二、基本概念
显约束:对分量xi取值范围的限定
隐约束:对不同分量之间施加的约束
2.1子集树
背包问题和旅行售货员问题的解空间树是两种典型的解空间 树。当所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树为子集树。这类子集树常有2n个叶结点,其结点个数为2n+1-1。遍历子集树的任何算法均需Ω(2n)的计算时间。
//x存储每个结点的两种可能,选1,不选0
void backtrack(int t){
if(t>n){
output(x);
return;
}
for(int i =0;i<=1;i++){
x[t]=i;
if(legal(t))backtrack(t+1);
}
}
2.2排列数
当所给问题是确定n个元素的满足某种性质的排列时,相应的解空间树为排列树。排列树通常有n!各叶结点。因此遍历排列树所需的计算时间需要Ω(n!) 的计算时间。
前提假设:有四个数1,2,3,4,为他们全排列,图片是从1开始,选了1之后,第二个结点就只能选择:2,3,4,以此类推。
void backtrack(int t){
if(t>n){
output(x);
return;
}
for(int i =t;i<=n;i++){
swap(x[t],x[i]);
if(legal(t))backtrack(t+1);
swap(x[t],x[i]);
}
}
以0—1背包问题为例讨论约束函数和界限函数
2.3约束函数
tw表示现已装载重量,w[i]表示当前第i个物品的重量。W是可装载的总重量。
约束函数就是剪去不满足约束条件的分支。对于第i层的有些 结点,如果tw+w[i] >= W,则扩展是没有必要的。
if(tw+w[i] <=W){
op[i]=1;
dfs(i+1,tw+w[i],tv+v[i],op);
}
2.4限界函数
限界函数:剪去得不到最优解的分支。如果当前价值+未选择的所有物品的价值 < 现有最优解的价值,也不用扩展这样的结点。
if ( tv+rv-v[i]< maxV )
{ op[i]=0; //不选取第i个物品
dfs(i+1,tw,tv,rv-v[i],op);
}
2.5时间复杂性
- 计算出解空间树中除叶子层之外的节点数目f(n)(一层如果是两个的话,2^n-1)
- 每个非叶子结点扩展出其下一层的所有分支的时间复杂性g(n)(1,常数级)
- 复杂性为O(f(n)*g(n))
三、解题步骤
3.1装载问题
有n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且
y
=
∑
i
=
1
n
w
i
≤
c
1
+
c
2
y = \sum_{i=1}^{n}{w_i}\le c_1+c_2
y=i=1∑nwi≤c1+c2
问题:
是否有一个合理的装载方案,可将这n个集装箱装上这2艘轮船?如果有,找出一种装载方案。
解题思路:
- 首先将第一艘轮船装载尽可能满
- 将剩下的装载进第二艘轮船
确定易于搜索的解空间结构:子集树
剪枝函数:
-
约束函数(左子树):对自己节点的约束条件:每个结点加入时要判断该结点加进来之后总重量是否小于c。
即:w[i]+cw<=c,op[i]=1;否则op[i]=0;
-
限界函数(右子树):如果已装入重量+后面的未装入的重量<maxw当前装入重量最大值,那么不选择次路径拓展
#include<iostream>
using namespace std;
const int N = 1000;
//n是集装箱数目,maxw是目前计算出来的结果当中,装入c1的最大重量
//r是剩余集装箱重量,x来记录是否装入
//c1,是第一艘轮船容量,c2是第二艘轮船的容量,w记录每个集装箱的重量
int n, maxw,r, x[N],bx[N][N], bk;
int c1, c2, w[N];
void input() {
cout << "请依次输入第一艘轮船的容量,第二艘,以及集装箱数量:" << endl;
cin >> c1 >> c2 >> n;
cout << "请依次输入集装箱的重量:" << endl;
for (int i = 1; i <= n; i++) {
cin >> w[i];
r += w[i];
}
}
void output() {
for (int i = 1; i <= bk; i++) {
cout << "第" << i << "种方案:" << endl << "第一艘轮船放: ";
for (int j = 1; j <= n; j++) {
if (bx[i][j] == 1) {
cout << j << " ";
}
}
cout << endl << "第二艘轮船放: ";
for (int j = 1; j <= n; j++) {
if (bx[i][j] == 0) {
cout << j << " ";
}
}
cout << endl;
}
cout << endl;
}
void backtrack(int t, int tw) {
if (t > n) {
if (tw >= maxw) {
//cout << "tw " << tw << " maxw " << maxw << endl;
if (tw == maxw) {
bk++;
}
else {
maxw = tw;
bk = 1;
}
for (int i = 1; i <= n; i++) {
bx[bk][i] = x[i];
}
}
return;
}
r -= w[t];
if (tw + w[t] <= c1) {
x[t] = 1;
backtrack(t + 1, tw + w[t]);
}
x[t] = 0;
if (r + tw >= maxw) {
x[t] = 0;
backtrack(t + 1, tw);
}
r += w[t];
}
int main() {
input();
backtrack(1,0);
output();
system("pause");
return 0;
}
3.2n后问题
问题描述:
要求在一个n*n的棋盘上放置n个皇后,使得它们彼此不受攻击。一个皇后可以攻击与之处在同一行或同一列或同一斜线上的任何其他棋子。
算法设计:
问题的解可表示为x[1:n], 表示皇后i放在棋盘的第i行的第x[i]列。
a)x[i]≠x[j] ,i≠j :不允许将任何两个皇后放在同一列上; b)|j-i|≠|x[j]-x[i]| :不允许两个皇后位于同一条斜线上。
解空间结构:
子集树,每个结点n个分支
剪枝函数:
约束函数,限界函数
我分不清,但是这个的剪枝函数就是:
- x[i]≠x[j] ,i≠j
- |j-i|≠|x[j]-x[i]|
代码实现:
#include<iostream>
using namespace std;
#include<cmath>
#define N 1000
int n, x[N],num;
bool check(int t) {
for (int i = 1; i < t; i++) {
if (x[t] == x[i] || abs(x[i] - x[t]) == abs(i - t))return false;
}
return true;
}
void bfs(int t) {
if (t > n) {
num++;
for (int i = 1; i <= n; i++) {
cout << x[i] << " ";
}
cout << endl;
return;
}
for (int i = 1; i <= n; i++) {
x[t] = i;
if (check(t)) {
bfs(t + 1);
}
else {
x[t] = 0;
}
}
}
int main() {
cin >> n;
bfs(1);
cout << "num: " << num << endl;
system("pause");
return 0;
}
3.3排列问题
3.3.1自然数排列
问题描述:
给定正整数n,使设计一个算法,列举出1,2,…,n的所有排列。
解空间:
排列数
剪枝函数:
没有吧!
代码实现:
#include<iostream>
using namespace std;
#define N 11
int n, x[N],num;
void swap(int i, int j) {
int temp = x[i];
x[i] = x[j];
x[j] = temp;
}
void output() {
for (int i = 1; i <= n; i++) {
cout << x[i] << " ";
}
cout << endl;
}
void Qpai(int t) {
if (t > n) {
num++;
output();
return;
}
for (int i = t; i <= n; i++) {
swap(t, i);
Qpai(t + 1);
swap(t, i);
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
x[i] = i;
}
Qpai(1);
cout << "num: " << num << endl;
system("pause");
return 0;
}
结果展示:
但是结果部分是逆的,我不知道怎么正过来.
但是这样写有个问题就是,就是结果不是从小到大依次来的,所以要调整一下,换一种写法.
不用swap,而是用use[]来记录有没有被用过.
int p[N];
void Upai(int t) {
if (t > n) {
num++;
for (int i = 1; i <= n; i++) {
cout << " "<<x[p[i]] ;
}
cout << endl;
return;
}
for (int i = 1; i <= n; i++) {
if (a[i] == 0) {
a[i] = 1;
p[t] = i;
Upai(t + 1);
a[i] = 0;
}
}
}
3.3.2给定数排列
其实跟上面一样,只是要输入要排列的数组.
3.3.3带重复元素全排列
去重:去重包含数层去重(可以先排序,然后如果前面出现过了,就不要再重复出现了,这个是指重复元素),剪枝函数(去掉下面跟上面重复的,用use[])
要在上面的基础上,加一句去重操作.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
对剪枝函数的理解:
- 子集树:
约束函数:对左边结点的约束,即对自身的约束,整体的约束
限界函数:对右边结点的约束,即结点间的相互关系
- 排列数:
约束函数:一条分支上下不能一样,用use[]来判断
限界函数:同一层,结点,前面出现过,后面就不可以出现了,用a[i]==a[i-1]&&use[i-1]=false,continue;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void Cpai(int t) {
if (t > n) {
num++;
for (int i = 1; i <= n; i++) {
cout << x[p[i]] << " ";
}
cout << endl;
return;
}
for (int i = 1; i <= n; i++) {
//数层上去重
if (x[i] == x[i - 1] && a[i-1] == 0) {
continue;
}
//树枝去重
if (a[i] == 1)continue;
a[i] = 1;
p[t] = i;
Cpai(t + 1);
a[i] = 0;//复原
}
}
这个要先对要排列的数组进行排序才可以,
结果:
3.4婚姻搭配问题
问题描述:
有n对男女要配成n对夫妇,其中第i位男士对第j位女士的爱恋程度为p[i][j],第i位女士对第j位男士的爱恋程度为q[i][j],显然p[i][j]不一定等于q[j][i],设计一个算法,确定各对夫妇的最佳婚配,使方案中各对夫妇的婚姻最满意。
算法分析:
用 s = ∑ i = 1 n ( p [ i ] [ j ] ∗ q [ j ] [ i ] ) s=\sum_{i=1}^n(p[i][j]*q[j][i]) s=∑i=1n(p[i][j]∗q[j][i])来表示n对夫妻的婚姻状况,要找到s最大的配对方案。
解空间:
排列树。
剪枝函数:(我自己理解的)
- 约束函数:
排列树,在同一条支路上的,下面不与上面的重复出现
- 限界函数:
没想到
程序实现:
我的想法是,男的不动,女的按排列树来排列对应男的。
输入:
3
0.6 0.7 0.5
0.7 0.8 0.9
0.9 0.8 0.7
0.4 0.6 0.5
0.5 0.7 0.8
0.7 0.6 0.5
#include<iostream>
using namespace std;
#define N 100
double n, p[N][N], q[N][N], ms;
int x[N], mx[N];
void swap(int i, int t) {
double temp = q[i][i];
q[i][i] = q[t][t];
q[t][t] = temp;
int temp01 = x[i];
x[i] = x[t];
x[t] = temp01;
}
void app(int t) {
if (t > n) {
double s = 0;
for (int i = 1; i <= n; i++) {
s += p[i][x[i]] * q[x[i]][i];
}
if (s > ms) {
ms = s;
for (int i = 1; i <= n; i++) {
mx[i] = x[i];
}
}
return;
}
for (int i = 1; i <= n; i++) {
swap(i, t);
app(t + 1);
swap(i, t);
}
}
int main() {
cout << "请输入男女对数:" << endl;
cin >> n;
cout << "请输入男对女爱意值:" << endl;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> p[i][j];
}
}
cout << "请输入女对男爱意值:" << endl;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> q[i][j];
}
}
for (int i = 1; i <= n; i++) {
x[i] = i;
}
app(1);
for (int i = 1; i <= n; i++) {
cout <<"男:"<<i<<" 女: " <<mx[i] << endl;
}
system("pause");
return 0;
}
如果要考虑稳定性,那就是另外的问题了
3.5图的着色问题
问题描述:
给定一个无向连通图G和m种不同的颜色。用这些颜色为图G的各顶点着色,每个顶点着一种颜色。若一个图最少需要m种颜色才能使图中任何一条边连接的2个顶点着有不同的颜色,则称这个数m为该图的色数。求一个图的色数m的问题称为图的m可着色优化问题。设计一个算法,找出用m种颜色对一个图进行着色的不同方案。
算法分析:
用数组g[][]表示图,g[i][j]=1,表示i与j之间连接,不可以用同一种颜色。
解空间:排列数
剪枝函数:
- 约束函数:应该没有
- 限界函数:与此点相连接的点使用过的颜色,此点不可以使用
代码实现:
#include<iostream>
using namespace std;
#define N 1000
int n, m, k,g[N][N],x[N],ms;
bool check(int t,int i) {
for (int j = 1; j <= t; j++) {
if (g[t][j] == 1 && x[j] == i) { return false; }
}
return true;
}
void dfs(int t) {
if (t > n) {
ms++;
return;
}
for (int i = 1; i <= m; i++) {
/*bool b = 0;
for (int j = 1; j <= t; j++) {
if (g[t][j] == 1 && x[j] == i) { b = 1; break; }
}
if (!b) {
x[t] = i;
dfs(t + 1);
x[t] = 0;
}*/
if (check(t, i)) {
x[t] = i;
dfs(t + 1);
}
x[t] = 0;
}
}
int main() {
cin >> n >> k >> m;
for (int i = 1; i <= k; i++) {
int x, y;
cin >> x >> y;
g[x][y] = 1;
g[y][x] = 1;
}
dfs(1);
cout << ms << endl;
system("pause");
return 0;
}
3.6符号三角形
- 问题描述:
下图是由14个“+”和“-”组成的符号三角形。2个同号下面都是“+”,2个异号下面都是“-”。
一般情况下,符号三角形的第一行有n个符号,符号三角形问题要求对于给定的n,计算有多少不同的符号三角形,使其所含的“+”和“-”的个数相同。
-
问题分析:其实整个符号三角形中“+”,“-”的数目只与第一行有关,第一行确定之后,整个符号三角形就确定了,
-
算法设计:用x[i]来记录第一行第i个位置的元素为:0(-),1(+)。然后写一个函数,根据第一行的排列,求整个图形中的“+”,±“数目。
但是显然上面这种方法没有剪枝函数,时间复杂度很高,现在我们发现,其实这个图形是一种递归结构,最大的图形的求解,可以转化为,去掉最右边的一条,然后剩下的根原问题类似,最后加上最右边一条就可以。在这个求解过程中,会有子问题重叠的问题,所以转化为动态规划求解图形的排列。
求解公式为:a[][]来记录图形的样子,图形每一行都是从1开始排列,只是每一行最后结束的位置不一样。
a
[
i
]
[
j
]
=
a
[
i
−
1
]
[
j
]
&
a
[
i
−
1
]
[
j
+
1
]
i
>
1
a[i][j]=a[i-1][j]\&a[i-1][j+1]\quad i>1
a[i][j]=a[i−1][j]&a[i−1][j+1]i>1
这里不详细解释动态规划了。
- 剪枝函数:
约束函数:应该没有
限界函数:如果目前已有的’+'或’-‘超过了 1 2 ∗ ( 1 + n ) ∗ n ∗ 1 2 \frac{1}{2}*(1+n)*n*\frac{1}{2} 21∗(1+n)∗n∗21就不选择这条路,换下一条。
- 代码实现:
#include<iostream>
using namespace std;
#define N 1000
int n, x[N][N], num;
bool b(int s) {
int k = 1.0 / 4.0 * (n + 1) * n;
if (s > k)return false;
return true;
}
void dfs(int t,int s) {
if ((n + 1) * n % 4 != 0) {
return;
}
if (t > n) {
if (s == 1.0 / 4.0 * (n + 1) * n) {
num++;
cout << "方案" << num << ":" << endl;
for (int i = 1; i<=n;i++) {
for (int j = 1; j <= n-i+1; j++) {
cout << x[i][j] << " ";
}
cout << endl;
}
}
return;
}
int ms = s;
for (int i = 0; i <= 1; i++) {
x[1][t] = i;
s += i;
for (int j = 2; j <= t; j++) {
x[j][t-j+1] = !(x[j-1][t-j+1] ^ x[j - 1][t-j+2]);
s += x[j][t - j + 1];
}
if (b(s)) {
dfs(t + 1, s);
}
s = ms;
}
}
int main() {
cin >> n;
dfs(1,0);
cout << num << endl;
system("pause");
return 0;
}
3.7旅行售货员问题
1.题目描述
某乡有 n ( 2 ≤ n ≤ 20 ) n\ (2\le n\le 20) n (2≤n≤20) 个村庄,有一个售货员,他要到各个村庄去售货,各村庄之间的路程 s ( 0 < s < 1000 ) s\ (0<s<1000) s (0<s<1000) 是已知的,且 A A A 村到 B B B 村与 B B B 村到 A A A 村的路大多不同。为了提高效率,他从商店出发到每个村庄一次,然后返回商店所在的村,假设商店所在的村庄为 1 1 1,他不知道选择什么样的路线才能使所走的路程最短。请你帮他选择一条最短的路。
2.输入格式
村庄数 n n n 和各村之间的路程(均是整数)。
第一行,第 i + 1 i+1 i+1 行第 j j j 个数代表村庄 i i i 到 j j j 的单向路径的路程。
3.输出格式
最短的路程。
4.样例 #1
4.1样例输入 #1
3
0 2 1
1 0 2
2 1 0
4.2样例输出 #1
3
#include<iostream>
using namespace std;
#define N 100
int n, s[N][N], mins=1e9,x[N];
void swap(int i, int j) {
int temp = x[i];
x[i] = x[j];
x[j] = temp;
}
//从第二层开始,第一层只有一个1
void dfs(int t, int ss) {
if (t > n) {
//ss += s[1][x[2]];
if (ss+ s[x[n]][1] < mins) {
mins = ss+ s[x[n]][1];
/*for (int i = 1; i <= n; i++) {
cout << x[i] << " ";
}
cout << endl;*/
}
return;
}
for (int i = t; i <= n; i++) {
swap(i, t);
if (ss < mins&& s[x[t - 1]][x[t]]!=0) {
dfs(t + 1,ss+ s[x[t-1]][x[t]]);
}
swap(i, t );
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> s[i][j];
}
x[i] = i;
}
dfs(2, 0);
cout << mins << endl;
system("pause");
return 0;
}
不知道为什么超时了,完了再修改吧!
#include
using namespace std;
#define N 100
int n, s[N][N], mins=1e9,x[N];
void swap(int i, int j) {
int temp = x[i];
x[i] = x[j];
x[j] = temp;
}
//从第二层开始,第一层只有一个1
void dfs(int t, int ss) {
if (t > n) {
//ss += s[1][x[2]];
if (ss+ s[x[n]][1] < mins) {
mins = ss+ s[x[n]][1];
/for (int i = 1; i <= n; i++) {
cout << x[i] << " ";
}
cout << endl;/
}
return;
}
for (int i = t; i <= n; i++) {
swap(i, t);
if (ss < mins&& s[x[t - 1]][x[t]]!=0) {
dfs(t + 1,ss+ s[x[t-1]][x[t]]);
}
swap(i, t );
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> s[i][j];
}
x[i] = i;
}
dfs(2, 0);
cout << mins << endl;
system("pause");
return 0;
}
不知道为什么超时了,完了再修改吧!