几个典型例子
算法基本思想和适用条件
有些问题,如搜索和优化问题,它们的解分布在一个解空间里,求解这些搜索问题的算法就是一种遍历搜索解空间的系统方法,所以解空间又称为搜索空间。求解搜索问题就是在搜索空间里找到一个或者全部解,求解组合优化问题就是找到该问题的一个最优解或者所有最优解。
回溯算法将搜索空间看成是一定的结构,通常为树形结构,一个解对应于树中的一片树叶。算法从树根(即初始状态)出发,尝试所有可达的结点。当不能前行时,就后退一步或若干步,再从另一个结点继续搜搜,直到所有的结点都试探过。回溯算法遍历一棵树可以用深度优先,广度优先或广度-深度结合等多种方法。为加快搜索,人们又给出了分支限界等各种在树中剪枝的方法,以改善算法的运行时间。
简单来说,回溯是一种遵循某种规则(避免遗漏),跳跃式(带裁剪)地搜索解空间的技术。
几个典型例子
4皇后问题
- 在 4 × 4 的方格棋盘上放置4个皇后,使得没有两个皇后在同一行、同一列、也不在同一条45度的斜线上.问有多少种可能的布局?
解是 4 维向量 < x1, x2, x3, x4 >
解: <2,4,1,3>,< 3,1,4,2>
推广到8后问题
解: 8维向量,有 92个.
例如:<1,5,8,6,3,7,2,4>是解.
搜索空间:4叉树
每个结点有4个儿子,分别代表选择1,2,3,4列位置 第 i 层选择解向量中第 i 个分量的值 最深层的树叶是解 按深度优先次序遍历树,找到所有解。
在实际搜索过程中不是真正遍历所有结点,如果发现向下搜索不可能到达解结点,就回头,搜索过程是解向量不断生成的过程。根节点为空向量,算法依次对 x 1 , x 2 , . . . , x k x_1,x_2,...,x_k x1,x2,...,xk进行赋值。每进行一次赋值需要检查"互不攻击"的条件。当条件不满足时,算法不再继续向下搜索,而是从这个结点回到父结点。一旦对 x 1 , x 2 , . . . , x k x_1,x_2,...,x_k x1,x2,...,xk进行了赋值,算法就到达了某个结点,将该结点标记为向量 < x 1 , x 2 , . . . , x k > <x_1,x_2,...,x_k> <x1,x2,...,xk>,称为部分向量。从根结点到解结点的路径上的所有标记正是从空结点到可行解的生成过程。
不断往下搜索,如果得到一个满足约束条件的8维向量,它就是8后问题的一个解。如果从某个结点向下分支的所有方向都破坏了约束条件,部分向量不能继续扩张,则意味着以这个结点为根的子树中没有可行解的存在。算法从这个结点继续向上回溯到其父结点,接着探查父节点其他可能的向下的分支。如此下去,得到一个解,再遍历其他结点,得到其余解。
算法复杂度分析:
在每个结点处,要判断此位置的皇后与已经放置的皇后是否互相攻击,最多要看3n个位置。(沿列方向,主与副对角线方向)是否已放有皇后,故n皇后问题的该算法在最坏情况下的时间复杂度为
O
(
3
n
∗
2
n
n
)
=
O
(
n
n
+
1
)
O(3n*2n^n)=O(n^{n+1})
O(3n∗2nn)=O(nn+1)。这是个粗略的估计,实际运行中由于搜索树的剪枝,时间要少得多。
0-1背包问题
有n种物品,每种物品只有 1个. 第i 种物品价值为 vi , 重量为 wi , i =1,2,…, n. 问如何选择放入背包的物品,使得总重量不超过 B, 而价值达到最大?
实例:
V={12,11,9,8}, W={8,6,4,3}, B=13
最优解:
<0,1,1,1>,价值: 28,重量: 13
解:n维0-1向量<x1, x2, …, xn>,
xi =1⇔物品 i 选入背包
结点:<x1, x2,…, xk>(部分向量)
搜索空间:0-1取值的二叉树, 称为
子集树,有
2
n
2^n
2n片树叶
可行解:满足约束条件
∑
i
=
1
n
w
i
x
i
≤
B
\displaystyle∑_{i=1}^nw_ix_i≤B
i=1∑nwixi≤B的解
最优解:可行解中价值达到最大的解
时间复杂度分析
父节点记录在此结点之前已放入背包的物品的重量,再加上当前新放入背包物品的重量,就可得到该结点放入背包物品的重量。在最坏情况下,该算法只进行了一次加法和一次大小比较,即进行了 O ( 1 ) O(1) O(1)次运算,从而该算法在最坏情况下的时间复杂性为 O ( 2 n ) O(2^n) O(2n)
货郎问题
问题:有n个城市,已知任两个城市之间的距离,求一条每个城市恰好经过一次的回路,使得总长度最小.
建模:城市集C={c1,c2,…,cn}, 距离
d(ci,cj)=d(cj,ci)∈Z+, 1≤i<j≤n
求:1,2, …, n的排列
k
1
,
k
2
,
.
.
.
,
k
n
k_1, k_2, ..., k_n
k1,k2,...,kn使得
m
i
n
{
∑
i
=
1
n
−
1
d
(
c
k
i
,
c
k
i
+
1
)
+
d
(
c
k
n
,
c
k
1
)
}
min\{\displaystyle∑_{i=1}^{n-1}d(c_{k_i},c_{k_i+1})+d(c_{k_n},c_{k_1})\}
min{i=1∑n−1d(cki,cki+1)+d(ckn,ck1)}
解的搜索空间是排列树,有(n-1)!片树叶。
时间复杂度分析
在每个结点处要计算已得到的路径长度,这只要将父结点得到的路径长度加上父节点到本结点的距离即可;在叶结点处还要计算得到的回路长度,并判断得到的回路长度是否为当前的最短路,所以算法在每个结点处最多进行两次加法和一次大小比较,故该算法最坏情况下的时间复杂性为 O ( ( n − 1 ) ! ) ∗ O ( 1 ) = O ( ( n − 1 ) ! ) O((n-1)!)*O(1)=O((n-1)!) O((n−1)!)∗O(1)=O((n−1)!)。
小结
- 回朔算法的例子:n后问题,0-1背包问题,货郎问题
- 解:向量
- 搜索空间:树,可能是n叉树、子集树、排列树等等,树的结点对应于部分向量,可行解在叶结点
- 搜索方法:深度优先, 宽度优先, …跳越式遍历搜索树,找到解
问题分析
问题 | 解 | 解描述向量 | 搜索空间 | 搜索方式 | 约束条件 |
---|---|---|---|---|---|
n后 | 可行解 | <x1, x2,…,xn> xi:第 i 行列号 | n叉树 | 深度,宽度优先 | 彼此不攻击 |
0-1背包 | 最优解 | <x1, x2,…,xn> xi = 0,1,xi =1⇔选 i | 子集树 | 深度,宽度优先 | 不超背包重量 |
货郎 | 最优解 | < i1=1, i2,…,in>1,2,…,n的排列 | 排列树 | 深度,宽度优先 | 选没有经过的城市 |
特点 | 搜索解 | 向量,不断扩张部分向量 | 树 | 跳跃式遍历 | 约束条件,回溯判定 |
深度与宽度优先搜索
深度优先访问顺序:
1→2→3→5→8→9 →6→7→4
宽度优先访问顺序:
1→2→3→4→5→6→7→8→9
回溯算法基本思想
(1) 适用:求解搜索问题和优化问题.
(2) 搜索空间:树,结点对应部分解向量,可行解在
树叶上.
(3) 搜索过程:采用系统的方法隐含遍历搜索树
(4) 搜索策略:深度优先,宽度优先,函数优先,宽
深结合等
(5) 结点分支判定条件: 满足约束条件—分支扩张解向量
不满足约束条件,回溯到该结点的父结点
(6) 结点状态:动态生成
- 白结点(尚未访问)
- 灰结点(正在访问该结点为根的子树)
- 黑结点(该结点为根的子树遍历完成)
(7) 存储:当前路径
结点状态
深度优先
访问次序:
1→2→3→5→8
已完成访问:2, 8
已访问但未结束:1, 3, 5
尚未访问:9, 6, 7, 4
回溯算法的适用条件
在结点<x1,x2,…,xk>处 P(x1, x2, …, xk) 为真⇔ 向量<x1, x2, …, xk > 满足某个性质(n后中 k个皇后放在彼此不攻击的位置)
多米诺性质:
P
(
x
1
,
x
2
,
…
,
x
k
+
1
)
→
P
(
x
1
,
x
2
,
…
,
x
k
)
P(x_1,x_2,…,x_{k+1})→P(x_1,x_2,…,x_k)
P(x1,x2,…,xk+1)→P(x1,x2,…,xk) 0<k<n
¬
P
(
x
1
,
x
2
,
…
,
x
k
+
1
)
P(x_1,x_2,…,x_{k+1})
P(x1,x2,…,xk+1)→¬
P
(
x
1
,
x
2
,
…
,
x
k
)
P(x_1,x_2,…,x_k)
P(x1,x2,…,xk) 0<k<n
k 维向量不满足约束条件,扩张向量到k+1维仍旧不满足,可以回溯
小结
- 回溯算法的适用条件:
多米诺性质 - 回溯算法的设计步骤
(1) 定义解向量和每个分量的取值范围
解向量 为 < x1, x2, …,xn>
确定 xi 的取值集合为 Xi , i =1,2,…, n
(2) 在<x1,x2,…,xk-1>确定如何计算 x k x_k xk取值
集合 S k , S k ⊆ X k S_k, S_k ⊆ X_k Sk,Sk⊆Xk
(3) 确定结点儿子的排列规则
(4) 判断是否满足多米诺性质
(5) 确定每个结点分支的约束条件
(6) 确定搜索策略: 深度优先,宽度优先等
(7) 确定存储搜索路径的数据结构
回溯算法的实现及实例
回溯算法一般可以如下描述:设解向量为 < x 1 , x 2 , . . . , x n > <x_1,x_2,...,x_n> <x1,x2,...,xn>, x i x_i xi的可能取值集合为 X i X_i Xi,i=1,2,…,n。设当 x 1 , x 2 , . . . , x n − 1 x_1,x_2,...,x_{n-1} x1,x2,...,xn−1确定以后 x k x_k xk的取值集合为 S k S_k Sk,显然 S ∈ X k S∈X_k S∈Xk。回溯算法的实现有两种算法:递归回溯和迭代递归。下面给出算法的伪码。
回溯算法递归实现
算法 ReBack (k)
1. if k>n then <x1,x2,…,xn>是解
2. else while Sk ≠ ∅ do
3. xk ← Sk 中最小值
4. Sk ← Sk – { xk }
5. 计算 Sk+1
6. ReBack (k+1)
算法 ReBacktrack (n)
输入:n
输出:所有的解
1. for k←1 to n 计算 Xk且Sk←Xk
2. ReBack (1)
迭代实现
迭代算法 Backtrack
输入:n
输出:所有的解
1. 对于 i =1, 2, … , n 确定Xi //确定初始取值
2. k ← 1
3. 计算 Sk
4. while Sk ≠ ∅ do //满足约束分支搜索
5. xk←Sk中最小值; Sk←Sk –{xk}
6. if k < n then
7. k ← k+1; 计算 Sk
8. else < x1, x2, … , xn>是解
9. if k >1 then k ← k-1; goto 4 //回溯
装载问题
问题:有n个集装箱,需要装上两艘载重分别为c1和c2 的轮船. wi为第 i 个集装箱的重量,且 w1+w2+…+wn ≤ c1+ c2问:是否存在一种合理的装载方案把这 n 个集 装箱装上船?如果有,请给出一种方案.
实例: W = < 90,80,40,30,20,12,10 >
c1=152, c2=130
解: 1,3,6,7装第一艘船,其余第2艘船
参考
算法——分支限界法(装载问题)
0034算法笔记——【分支限界法】最优装载问题
0027算法笔记——【回溯法】回溯法与装载问题
经典问题-回溯法-装载问题
求解思路
输入:
W
=
<
w
1
,
w
2
,
.
.
.
,
w
n
>
W=<w_1,w_2, ..., w_n>
W=<w1,w2,...,wn>为集装箱重量
c
1
c_1
c1和
c
2
c_2
c2为船的最大载重量
算法思想:令第一艘船的装入量为
W
1
W_1
W1
- 用回溯算法求使得 c 1 − W 1 c_1−W_1 c1−W1达到最小的装载方案.
- 若满足 w 1 + w 2 + . . . + w n − W 1 ≤ c w_1+w_2+...+w_n−W_1≤ c w1+w2+...+wn−W1≤c,则回答“Yes”,否则回答“No”
伪码描述:
算法 Loading (W, c1),
1. Sort(W);
2. B←c1; best←c1; i←1; //B为当前空隙,best最小空隙
3. while i≤n do
4. if 装入 i后重量不超过 c1
5. then B←B−wi; x[i]←1; i←i+1;
6. else x[i]←0; i←i+1;
7. if B<best then 记录解;best←B;
8. Backtrack(i);
9. if i=1 then return 最优解
10. else goto 3 .
Backtrack
算法 Backtrack(i)
1. while i >1 and x[ i ] = 0 do
2. i←i−1;//沿右分支一直回溯发现左分支边,或到根为止
3. if x[i]=1//发现左分支边
4. then x[i]←0;//尝试剪枝,进入右分支边
5. B←B+wi;
6. i←i+1;//下一层
完整代码
n个结点,每个结点都可以进行左右划分,装与不装,共n层
进入左子树的含义:x[i]=1
,装进去
进入右子树的含义:x[i]=0
,不装
#include <iostream>
using namespace std;
const int num=100;//最多货物
int n,c1,c2,w[num];// n个集装箱,A,B货轮载重量分别为C1,C2,W[i],第i个集装箱的重量
int cw,bw,rw;//cw,当前集装箱货物重量(currentweight);bw,最优载重重量(bestweight),rw,剩余集装箱重量(restweight);
int x[num],bx[num];//x[],A货轮的当前结果;bx[],A货轮的最优结果;
void BackTrack(int i)
{
//处理完了前n个集装箱就返回结果
if(i>n){
if(cw>bw){//cw,目前A中装了cw重量的集装箱;
//更新最优解;
bw=cw;
for(int i=1;i<=n;i++) bx[i]=x[i];
}
return;
}
//rw表示处理完第i个之后(选或不选),还剩下rw-w[i]重量的集装箱未处理;
rw-=w[i];
if(cw+w[i]<=c1){//cw,第i个货箱之前的重量 + 第i个货箱小于A的最大重量C1;
cw+=w[i];//加上
x[i]=1;//标记i被选,进入左子树
BackTrack(i+1);
cw-=w[i];//减去重量
x[i]=0;//撤销标记;
}
//不选择第i个物品的话;
//if cw:表示[1:i)的数据 rw:表示(i,n]的数据 ,不包括第i个的数据
//如果不包括第i的数据的和(cw+rw) 大于 目前最优解bw,则可以递归下去;
if(cw+rw > bw){
x[i]=0;//进入右子树
BackTrack(i+1);
}
//处理完第i个物品当前的情况了;
//因为再上一层,有两种情况;
//1;选择第i物品;
//2:不选择第i个物品
//如果目前处理的是上一层第1种情况,那么我们就有必要加上这个w[i];
//否则会影响上一层处理第2种情况;
rw+=w[i];
return ;
}
int main()
{
cout << "-----读入数据-----"<< endl;
cout << "集装箱个数:";
cin >> n;
cout << "第一个货轮的载重:";
cin >> c1;
cout << "第二个货轮的载重:";
cin >> c2;
cout << "循序输入" << n << "个货物的重量"<<endl;
for(int i=1;i<=n;i++)
{
cin>>w[i];
rw+=w[i];//rw表示目前最优集装箱的剩余重量;
}
//递归回溯
BackTrack(1);
//bw表示A货轮装下的货物重量;剩余的重量 > B可以放下的最多,则不可;
if(rw-bw>c2){
cout<<"没有装载方案"<<endl;
}else{
cout<<"货轮A:"<<endl;
for(int i=1;i<=n;i++) if(bx[i]) cout<<i<<" ";
cout << endl;
cout<<"货轮B:"<<endl;
for(int i=1;i<=n;i++) if(0==bx[i]) cout<<i<<" ";
}
return 0;
}
按照书上伪代码,借鉴上述代码
#include <iostream>
using namespace std;
const int num=100;//最多货物
int n,c1,c2,w[num];// n个集装箱,A,B货轮载重量分别为C1,C2,W[i],第i个集装箱的重量
int cw,bw,rw; //cw,当前集装箱货物重量空隙(currentweight);bw,最优载重重量空隙(bestweight),rw,剩余集装箱重量(restweight)
int x[num],bx[num];//x[],A货轮的当前结果;bx[],A货轮的最优结果;
int i;
void BackTrack(int &i)//需要为引用,修改参数i的值
{
while(i>1&&x[i]==0)
{
--i;
}
if(x[i]==1)
{
x[i] = 0;
cw += w[i];
i = i + 1;
}
}
void Loading(int w[])
{
while(i<=n)
{
if(cw-w[i]>=0)
{
cw -= w[i];//装入w[i]
x[i] = 1;
i ++;
}
else
{
x[i] = 0;
i ++;
}
}
if(cw<bw)//更新最优解
{
bw = cw;
for(i=1;i<=n;i++) bx[i]=x[i];
}
BackTrack(i);
if(i==1)
{
cout<<"货轮A:"<<endl;
for(i=1;i<=n;i++) if(bx[i]) cout<<i<<" ";
cout << endl;
cout<<"货轮B:"<<endl;
for(i=1;i<=n;i++) if(0==bx[i]) cout<<i<<" ";
return;
}
else
Loading(w);
}
int main()
{
i = 1;
cout << "-----读入数据-----"<< endl;
cout << "集装箱个数:";
cin >> n;
cout << "第一个货轮的载重:";
cin >> c1;
cout << "第二个货轮的载重:";
cin >> c2;
cout << "循序输入" << n << "个货物的重量"<<endl;
cw = c1;
bw = c1;
w[0] = 0;
for(i=1;i<=n;i++)
{
cin>>w[i];
rw+=w[i];//rw表示目前最优集装箱的剩余重量;
}
i = 1;
Loading(w);
return 0;
}