算法设计与分析期末复习
主要参考:
华中科技大学 计算机科学与技术学院 算法设计与分析 课堂教学PPT
五、回溯法
回溯算法的例子:4后问题、0-1背包问题、货郎问题
解的形式:向量
搜索空间:树,可能是n叉树,子集树、排列树等等,输的节点对应与部分向量,可行解在叶节点
搜索方法:深度优先,宽度有限,…,跳跃式遍历搜索树,找到解
约束条件、回溯判定
可用回溯法求解的问题:
问题的解可以用一个n元组(x1,…,xn)来表示,其中的xi取自于某个有穷集Si,并且这些解必须使得某一规范函数P(x1,…,xn)取极值或满足该规范函数条件。
5.1 回溯算法基本思想
(1)适用:求解搜索问题和优化问题
(2)搜索空间:树,结点对应部分解向量,可行解在树叶上
(3)搜索过程:采用系统的方法隐含遍历搜索树
(4)搜索策略:深度优先,宽度优先,函数优先,宽深结合等
(5)结点分支判定条件:
满足约束条件–分支扩张解向量,不满足约束条件–回溯到该结点的父结点
显式约束条件:限定每个xi只能从一个给定的集合上取值
隐式约束条件:xi必须彼此相关的情况
(6)结点状态:动态生成
白结点(尚未访问),灰结点(正在访问该结点为根的子树),黑结点(该结点为根的子树遍历完成)
(7)存储:当前路径
状态空间树:
- 问题状态(problem state):树中的每一个结点确定所求解问题的一个问题状态。
- 状态空间(state space):由根结点到其他结点的所有路径则确定了这个问题的状态空间。
- 解状态(solution states):是这样一些问题状态S,对于这些问题状态,由根到S的那条路径确定了这解空间中的一个元组。
- 答案状态(answer states):是这样的一些解状态S,对于这些解状态而言,由根到S的这条路径确定了这问题的一个解。
- 静态树(static trees):树结构与所要解决的问题的实例无关。
- 动态树(dynamic trees):根据不同的实例而使用不同的树结构。
- 活结点:自己已经生成而其所有的儿子结点还没有全部生成的结点。
- E-结点(正在扩展的结点):当前正在生成其儿子结点的活结点。
- 死结点:不再进一步扩展或者其儿子结点已全部生成的生成结点。
构造状态空间树的两个方法:回溯法、分枝-限界方法
- 回溯法------当前E-结点R,生成一个新的儿子C,则C就变成一个新的E-结点,对子树C完全检测后,R结点再次成为E-结点
- 分枝-限界方法------一个E-结点一直保持到变成死结点为止
5.2 回溯算法的适用条件
多米诺性质:
- 通俗表达:
- 若x1到xi-1满足,就可以继续向下搜索
- 若不满足,则x1到xi也不满足,就回溯
5.3 回溯算法的设计步骤
(1)定义解向量和每个分量的取值范围
解向量为 <x1,x2,…,xn>
确定xi的取值集合为Xi,i=1,2,…,n
(2)在<x1,x2,…,xk-1>确定如何计算xk取值集合Sk,Sk包含于Xk
(3)确定结点儿子的排列规则
(4)判断是否满足多米诺性质(个人感觉算法实现中华科ppt表述 更为直观)
(5)确定每个结点分支的约束条件
(6)确定搜索策略:深度优先,宽度优先等
(7)确定存储搜索路径的数据结构
5.4 回溯算法的实现一般性描述
递归实现
(北大mooc表述)
- Sk是xk的取值集合
算法 ReBack(k)
if k>n then <x1,x2,...,xn>是解
else while Sk ≠ 空集 do
xk <- Sk 中最小值
Sk <- Sk-{xk}
计算Sk+1
ReBack(k+1)
另一种表述(华科ppt表述):
(x1, x2, …xi-1)是由根到一个结点的路径
T(x1, x2, …xi-1)是所有xi的集合:对于每一个xi , (x1, x2, …xi)是由根到结点xi的路径(也就是根据某一确定的x1到xi-1所得到的xi取值的集合)
限界函数Bi,如果路径(x1, x2, …xi)不可能延伸到一个答案结点,则Bi(x1, x2, …xi)取假值,否则取真值(也就是添加上xi仍可能是答案,则为真,不可能为答案则为假)
procedure RBACKTRACK(k)
global n,X(1:n)
for 满足下式的每个X(k)
X(k) ∈ T(X(1),...,X(k-1)) and B (X(1),...,X(k)) = true do
if(X(1),...,X(k)) 是一条已抵达答案结点的路径
then print(X(1),...,X(k)) endif
call RBACKTRACK(k+1)
repeat
end RBACKTRACK
迭代实现
Backtrack
输入: n
输出:所有的解
对于 i = 1,2,...,n 确定Xi //确定初始取值
k <- 1
计算Sk
while Sk ≠ 空集 do //满足约束分支搜索
xk <- Sk中的最小值,Sk <- Sk-{xk}
if k<n then
k <- k+1
else <x1,x2,...,xn>是解
if k>1 then k <- k-1 goto (第7行) //回溯
另一种表述:
procedure BACKTRACK(n)
integer k,n; local X(1:n)
k <- 1
while k>0 do
if 还剩有没检验过的X(k)使得
X(k) ∈ T(X(1),...,X(k-1)) and B(X(1),...,X(k)) = true
then
if(X(1),...,X(k)) 是一条已抵达一答案结点的路径
then print(X(1),...,X(k)) endif
k <- k+1
else
k <- k-1
endif
repeat
end BACKTRACK
5.5 搜索树结点数的估计
Monte Carlo方法
-
从根开始,随机选择一条路径,直到不能分支为止,即从x1,x2,…,依次对xi赋值,每个xi的值是从当时的Si中随机选取,直到向量不能扩张为止
-
假定搜索树的其他|Si|-1个分支与以上随机选出的路径一样,计数搜索树的点数
-
重复步骤1和2,将结点数进行概率平均
(如果没看明白,去北大MOOC里看例子讲解)
-
目的:估计搜索树真正访问结点数
-
步骤:
-
随机抽样,选择一条路径
用这条路径代替其他路径
逐层累加树的结点数
-
多次选择,取结点数的平均值
-
Monte Carlo
输入:n为皇后数,t为抽样次数
输出:sum,即t次抽样路长平均值
sum <- 0
for i <- 1 to t do //凑杨次数t
m <- Estimate(n) //m为结点数
sum <- sum + m
sum <- sum / t
一次抽样
-
m为本次取样得到的树节点总数
-
k为层数
-
r2位上层结点数
-
r1位本层结点数
-
r1 = r2*分枝数
-
n为数的层数
-
从树根向下计算,随机选择,直到树叶
伪代码
Estimate(n)
m <- 1; r2 <- 1; k <- 1 //m为一次估计结点总数
while k ≤ n do
if Sk = ∅ then return m //不能继续
r1 <- |Sk|*r2 //r1为扩张后的结点总数
m <- m+r1 //r2位扩张前的结点总数
xk <- 随机选择Sk的元素 //随机选择一步
r2 <- r1
k <- k+1
左侧红线路径为一次抽样,右侧为扩张后的数(用于估计一次结点个数)
通过多次抽样,取平均值估计搜索树的结点数(Monte Carlo方法)
5.6 实例伪代码
例1 装载问题
有n个集装箱,需要装上两艘载重分别为c1,c2的轮船,wi为第i个集装箱的重量,且w1+w2+…+wn≤c1+c2,。
问:是否存在一种合理的装载方案把这n个集装箱装上船?如果有,请给出一种方案。
求解思路
-
输入:W=<w1,w2,…,wn>为集装箱重量,c1和c2为船的最大载重量
-
算法思想:
令第一艘船的装入量为W1(让第一艘船装的尽量多)
- 用回溯算法求使得c1-W1达到最小的装载方案
- 若满足w1+w2+…+wn-W1≤c2,则回答yes,否则回答no
伪代码
算法 Loading(W,c1)
Sort(W);
B <- c1; best <- c1; i <- 1; //B为当前船1的空隙,best为最优解情况下船1的空隙
while i ≤ n do //相当于赋值一个初值
if 装入i后重量不超过c1
then B <- B-w1 ; x[i] <- 1 ; i <- i+1
else x[i] <- 0 ; i<- i+1
if B<best then 记录解 best <- B
Backtrack(i); //注意这里的Backtrace不是递归,只是一个子函数
if i=1 then return 最优解 //回溯到根节点了
else goto (第3行)
算法 Backtrace(i)
while i>1 and x[i]=0 do
i<- i-1;
if x[i] = 1 //左子树换到右子树
then x[i] <- 0
B <- B + wi
i <- i+1
时间复杂度 可以达到O(2^n)
例2 n皇后问题
怎么判断是否形成了互相攻击的格局?
是否在同一列,是否在同一条对角线
procedure PLACE(k) //判断当前X(k)是否可以放置
global X(1:k); integer i,k //是否会和前k-1行放置的皇后冲突
i <- 1
while i<k do
if X(i)=X(k)
or ABS(X(i)-X(k))=ABS(i-k)
then return false
endif
i <- i+1
repeat
return true
end PLACE
procedure NQUEENS(n)
integer k,n,X(1:n);
X(1) <- 0; k<-1
while k>0 do
X(k) <- X(k)+1 //第一个皇后放置的位置
while X(k) <= n and not PLACE(k) do
X(k) <- X(k)+1
repeat
if X(k) <= n //如果可以放置
then if k=n
then print(X)
else k <- k+1;X(k) <- 0 //还没到最后一行,继续下一行
else k <- k-1; //如果不能放置
endif
repeat
end NQUEENS
例3 子集和数问题
n个数里面存在某些个数的和等于M
元组大小固定
W(k)是按照从小到大的顺序排好序的(对理解伪代码中的剪枝很重要)
限界函数
伪代码
procedure SUMOFSUB(s,k,r) //当前和s,当前元素位置k,k之后所有数的和
global integer M,n; global real W(1:n); global boolean X(1:n)
real r,s; integer k,j
X(k) <- 1
if s+W(k)=M then //和等于了M,找到结果
print(X(j),j <- 1 to k)
else
if s+W(k)+W(k+1)<=M then //算上Wk和Wk+1也不够
call SUMOFSUB(s+W(k),k+1,r-W(k))//那么就算上Wk,继续搜索
endif
endif
if s+r-W(k)>=M and s+W(k+1) <= M
then X(k) <- 0
call SUMOFSUB(s,k+1,r-W(k))
endif
end SUMOFSUB
伪代码第12行解释:剪枝
如果算上k之后所有的还不够,那就不继续向下搜索了
由于是从小到大排序好的,如果k+1也超过了,那么后面的全都超过了,也不继续搜索
两个条件都满足的条件下才进行继续搜索