回溯法(内容摘自《算法设计技巧与分析》)
基本特征:
1.节点是用深度优先搜索的方法生成的
2.不需要存储整颗搜索树,只需要存储根到当前活动节点的路径
经典回溯问题——3着色问题
问题描述
给出一个无向图 G = (V, E),需要用三种颜色之一为V中的每个顶点着色,三种颜色分别为1,2和3,使得没有两个邻接的顶点有同样的颜色
问题解决
一般回溯问题有两种解法:递归和迭代
算法
递归法
//3-COLORREC
//输入:无向图G= (V, E)
//输出:G的顶点的3着色c[1...n],其中每个c[j]为1,2,3
main(){
for k = 1:n
c[k] = 0
flag = false
graphcolor(1)
if flag
return c
else
return "no solution"
}
graphcolor(k){
for color = 1:3
c[k] = color
if c is legal
flag = true
return
else if c is partially legal
graphcolor(k+1)
}
迭代法
//3-COLORITER
//输入:无向图G= (V, E)
//输出:G的顶点的3着色c[1...n],其中每个c[j]为1,2,3
main(){
for k = 1:n
c[k] = 0
flag = false
k = 1
while k >= 1
while c[k] <= 2
c[k] = c[k] + 1
if c is legal
flag = true
return
else if c is partially legal
k = k + 1
c[k] = 0
k = k - 1
if flag
return c
else
return "no solution"
}
八皇后问题
问题描述
如何在8*8的国际象棋棋盘上安排8个皇后,使得没有两个皇后能互相攻击?如果皇后处在同一行、同一列或同一条对角线上,则她们能互相攻击。
算法(在这里简单期间,讨论4皇后)
//4-QUEENS
//输入:空
//输出:对应于4皇后问题的解的向量层c[1...4]
main(){
for k = 1:4
c[k] = 0
flag = false
k = 1
while k >= 1
while c[k] <= 3
c[k] ++
if c is legal
flag = true
else if c is partially legal
k ++
c[k] = 0
k -- //回溯
if flag
return c
else
return "no solution"
/*
八皇后问题,迭代法;C++源代码:解出共92种放置方法
*/
#include<iostream>
#include<vector>
using namespace std;
int abs(int a, int b){
if (a > b)
return a - b;
else
return b - a;
}
int is_legal(int state[], int row, int n){ //一维数组,存储每行皇后所在列号,0开头
if(state[row] >= n) return 0;
for(int i = 0; i < row; ++ i){
if(state[i] == state[row] || abs(state[row], state[i]) == abs(row, i))
return 0;
}
if(row == n - 1) return 2;//0表示不合法,1表示部分合法,2表示全部合法
return 1;
}
void cout_queen(int state[]){
for(int i = 0; i < 8 ; ++ i){
for (int j = 0; j < state[i]; ++ j)
cout << "- ";
cout << "0 ";
for (int k = state[i] + 1; k < 8; ++ k)
cout << "- ";
cout << endl;
}
}
int queen(int n, int state[]){
int row = 0;
int count = 0;
while(row < n && row >= 0){
//cout << "test"<< endl;
while(state[row] < n){
++ state[row];
int flag = is_legal(state, row, n);
if(flag == 2){
cout << "the " << ++ count << " queen"<<endl;
cout_queen(state);
}
else if(flag == 1){
++ row;
}
}
state[row] = -1;
-- row;
}
return -1;
int main(){
int state[8] = {-1, -1, -1, -1, -1, -1, -1, -1};
queen(8, state);
}
一般的回溯方法
从上面两个例子中,可以很明显看到回溯法的特征,针对着色问题的描述给出抽象特征,来寻求一般回溯问题的解。
对于n个点的着色问题,每个点有m种颜色取值情况。回溯算法则是按照字典序考虑笛卡尔积
X
1
∗
X
2
∗
.
.
.
∗
X
n
X_1*X_2*...*X_n
X1∗X2∗...∗Xn中的所有元素。某种特定解法可以用
{
x
1
,
x
2
,
.
.
.
x
n
}
\lbrace x_1, x_2, ...x_n\rbrace
{x1,x2,...xn}表示,
X
i
X_i
Xi取值范围
{
1
,
2
,
.
.
.
m
}
\lbrace 1, 2, ...m\rbrace
{1,2,...m}。
算法从最初空集开始,然后选择
X
1
X_1
X1中最小的元素作为
x
1
x_1
x1,如果
(
x
1
)
(x_1)
(x1)是一个部分解,算法从
X
2
X_2
X2选择最小的元素作为
x
2
x_2
x2继续,如果
(
x
1
,
x
2
)
(x_1, x_2)
(x1,x2)是一个部分解,那么继续往
(
X
3
)
(X_3)
(X3)推进,否则
(
x
2
)
(x_2)
(x2)被置为下一个元素。一般地,假定算法已经检测到部分解为
(
x
1
,
x
2
,
.
.
.
,
x
j
)
(x_1, x_2, ... , x_j)
(x1,x2,...,xj),然后再去考虑向量
v
=
(
x
1
,
x
2
,
.
.
.
,
x
j
,
x
j
+
1
)
v = (x_1, x_2, ... , x_j,x_{j+1})
v=(x1,x2,...,xj,xj+1),我们有下面的情况。
- 如果v表示问题的最后解,算法记录下它作为一个解,在仅希望获得一个解时终止,或者继续去找出其他解。
- (向前步骤)。如果v表示一个部分解,算法通过选择集合 X j + 2 X_{j+2} Xj+2中的最小元素向前。
- 如果v既不是最终的解,也不是部分解,则有两种子情况。
(a)如果从集合 X j + 1 X_{j+1} Xj+1中还有其他的元素可选择,算法将 x j + 1 x_{j+1} xj+1置为 X j + 1 X_{j+1} Xj+1中的下一个元素
(b)(回溯步骤) 如果从集合 X j + 1 X_{j+1} Xj+1中没有更多的元素可选择,算法通过将 x j x_j xj置为 X j X_{j} Xj中的下一个元素回溯;如果从集合 X j X_{j} Xj中仍然没有其他的元素可以选择,算法通过将 x j − 1 x_{j-1} xj−1置为 X j − 1 X_{j-1} Xj−1中的下一个元素回溯,以此类推。
算法
就是迭代和递归,跟上文提到的程序一样
递归法
//BACKTRACKREC
//输入:集合X1,X2,...,Xn的描述
//输出:解向量v = (x1, x2, ..., xi),0 <= i <= n
main(){
v = () //初始化空向量
flag = false
advance(1)
if flag
return c
else
return "no solution"
}
advance(k){
for x in X_k
x_k = x
x_k -> v
if c is legal
flag = true
return
else if c is partially legal
graphcolor(k+1)
}
迭代法
//3-COLORITER
//输入:无向图G= (V, E)
//输出:G的顶点的3着色c[1...n],其中每个c[j]为1,2,3
main(){
v = ()
flag = false
k = 1
while k >= 1
while X_k has left
x_k = X_k.next
x_k -> v
if c is legal
flag = true
return
else if c is partially legal
k = k + 1
reset X_k
k = k - 1
if flag
return c
else
return "no solution"
}
##分支限界法
求和问题
问题描述
给定一个集合 X = { 10 , 20 , 30 , 40 , 50 , 60 } , y = 60 X = \lbrace10,20,30,40,50,60\rbrace,y = 60 X={10,20,30,40,50,60},y=60,找出不同的 X X X的子集使得他们的和为 y y y,例如{10, 20,30}是其中一个满足条件的子集
解题思维
将每个数据的选择与否看成0,1情况,也就是有 2 6 2^6 26种搜索情况,而在这种题设条件下,可以有一个剪枝条件,就是一旦发现总和大于等于 y y y,便可以立即停止该节点以后的搜索,我也不知道这种想法算不算分支限界。
程序
//C++ 源码
#include<iostream>
using namespace std;
int judge(int x[], int flag[], int y,int n = 6){
int count = 0;
for(int i = 0; i < n; ++ i){
if(flag[i] == 2)
count += x[i];
else if(flag[i] == 0)
break;
else if(flag[i] == 3)
return 0;//不合法
}
if (count > y) return 0;//不合法
if(count < y) return 1;//部分合法
if (count == y) return 2;//合法,且此时后面数据无需再次搜索
}
void cout_output(int x[], int flag[], int n = 6){
for(int i = 0; i < n; ++ i){
if(flag[i] == 2)
cout << x[i] << " ";
else if(flag[i] == 0)
break;
}
cout << endl;
}
int main(){
int a[6] = {10, 20, 30, 40, 50, 60};
int flag[6] = {0};
int n = 6;
int count = 0;
int y = 60;
for(int i = 0; i >= 0; -- i){
while(flag[i] <= 2){
++ flag[i];
int sum = judge(a, flag, y);
if(sum == 0){
break;
}
else if(sum == 2){
cout << "The " << ++ count << "st solution: ";
cout_output(a, flag);
break;
}
if(i < 5) ++ i;//该问题的特殊限制条件,也可以放入judge函数中
}
flag[i] = 0;
}
}