回溯法
背景:
寻找问题的解比较直观的方法是穷举法,但是有时候候选解的数量非常大,通常是指数级别,甚至是阶乘级别的,即使使用最快的计算机,也只能解决规模很小的问题。因此需要一种系统化检查候选解的方法,借助这种方法,将搜索空间减少到最低的程度。这种系统并有组织的搜索方法,称为回溯法。
基本思想:
(1)针对所给问题,定义问题的解空间。
(2)确定易于搜索的解空间结构。
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
|| 图的三着色问题
问题描述:
给出一个无向图G = (V,E),需要三种颜色之一为图中每个结点着色,三种颜色分别用1、2、3来表示。若没有两个邻接节点有相同的颜色,则称着色是不合法的。具有n个结点的图的一种着色,可以用向量(c1,c2,…cn)来表示。
递归算法-ColorRec
输入:无向图G = (V,E)
输出:图的结点3着色向量 c[1...n] , 1 <=c[k]<= 3
1. GraphColorRec (k)
过程 GraphColorRec (k)
for color<-1 to 3
c[k]<-color
if c[1...k]为合法着色 then //部分解或解
if k=n then
output c[1...n] and exit //解
else GraphColorRec(k+1) //进入下一个结点
end if
end if
end for
//
非递归算法
c[1...n]<-0
k<-1
while k > 0
while c[k] < 3
c[k]<-c[k]+1
if c[1...n]为合法着色 then
if k=n then
output c[1...n] and exit
else k<-k+1
end if
end if
end while //试完三种颜色后回退
c[k]<-0 , k<-k-1
end while
下面展示 合法性检验算法
。
建立邻接矩阵A[][]
bool CheckColor()
{
for(int i=1;i<=k-1;i++)
{
if(A[k][i]==1&&c[i]==c[k])
return false;
}
return true;
}
|| 八皇后问题
问题描述:
在8*8的国际象棋棋盘上安排8个皇后,使得没有两个皇后相互攻击。也就是说,没有两个皇后处于同一行、同一列或同一对角线上。为了简化讨论,我们先讨论4皇后问题。在此基础上,可简单地将研究结果推广到n为任意值的情况。
穷举法
for c[1]=1 to 4
for c[2]=1 to 4
for c[3]=1 to 4
for c[4]=1 to 4
if c[1..4]是解 then
output c[1..4]and exit
end if
end for
end for
end for
end for
下面展示 回溯法
。
递归解
输入:空
输出:对应于4皇后问题的向量c[1..4]
1.advanced(1)
过程advanced(k)
for col=1 to 4
c[k]=col
if c[1..k]是解 then
if k=4 then
output c[1..4] and exit
else advanced(k+1)
end if
end if
end for
//
非递归
c[1..4]=0
k=1
while k>0
while c[k]<4
c[k]=c[k]+1
if c[1..k]是解 then
if k=4 then
output c[1..4]and exit
else k=k+1
end if
end if
end while
c[k]=0
k=k-1 //清零后退
end while
下面展示判断冲突函数
。
//检查放置第k个皇后时是否产生冲突
bool check(int c[],int k)
{
if(k===1) return true;
for(int i=1;i<k;i++) //同列
{
if(c[i]==c[k]) return true;
}
for(int i=1;i<k;i++) //同一对角线
{
if( (k-i)==fabs(c[k]-c[i]) )
return false;
}
return true;
}
|| 实战演练
例 1:现在有一个候选数组C,一个目标值T,从C中选取部分数字(不可重复)使其和为T,使用回溯法求所有可能的情形,给出伪代码和时间复杂度。
基本思想:
首先对数组进行排序提纯筛去重复值,再深度优先遍历数组。复杂度为O( n! )
imput: candidates target
output: result
result={} //存放所有结果
curr={} // 存放暂时解
sort and pure(candidates)
dfs(candidates,start,target,curr,result)
1.dfs(num,0,target,curr,result)
if target==0
result.add(curr)
return
end if // success
if start>= num.length||num[start]>target
return;
end if // error
i = start;
while(i<num.length&&num[i]<=target) //头元素尝试n种可能
curr.add(num[i])
dfs(num,i+1,target-num[i],curr,result) //子树
curr.remove(curr.size()-1) // back ,删去根
i++ // 指向下一头元素作为根
end while
例 2: 用回溯法输出1-N(N个不重复正整数)的所有全排列(如N=3,则1 2 3;1 3 2;2 3 1;3 2 1…均为它的全排列)
(1)用自然语言描述算法思想。
(2)以N=3为例,画出问题的解空间树。
(3)给出算法的伪代码程序。
解:
(1)该题采用回溯方法,对问题进行深度优先遍历,对于当前已得的排列,判 断该排列是否已经是一个完整的全排列,如果是则输出,否则,不断检测出下一 个整数(保证不与前面的重复),以此作为下一个元素继续进行回溯遍历。
(2)
(3)伪代码如下
c[1...n] 存储得到的一个全排列
result{} 所有全排列的集合
1. solve(k)
for num=1 to N
c[k]=num
if c[1...k] legal then
if k=n then
result.add(c[1...n])
else solve(k+1)
end if
end if
end for
例 3: 现在有n种纸币,V1, V2, V3…Vn,每种面值的纸币有3个,现在甲需要给乙T元,请利用回溯法设计一个算法,判断甲能否利用这n种纸币恰好凑齐T元。如果能凑齐,返回凑齐方法(每个面值钱币选择的个数);如果不能,返回“no solution”.
c[1...k] 存储得到的一种解
result{} 所有解的集合
调用 solve(1)
solve(k)
{
if(k<=n)
{
for i=0 to 3
c[k] = i;
if(c[1...k]是最终解)
result.add(c[1...k])
else if(c[1...k]是部分解) solve(k+1)
end if
end for
}
}
判断子程序
FinalSolution(c[1...k]) // 只要满足和为T就是最终解
{
sum = 0;
for i=1 to k
sum = sum + Vi*c[i]
end for
if(sum = T) return true
else return false
}
PartialSolution(c[1...k])
{
sum = 0
for i=1 to k
sum = sum + Vi*c[i]
end for
if(sum<T) return true
else return false
}
// 最后一步 如果result为 ∅ ,返回 no solution