版权声明:转载请注明出处 https://blog.csdn.net/weixin_42683993/article/details/89328862
一. 基本概念
- 有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。
- 回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。
- 回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。
2. 问题的解向量
- 问题的解向量:回溯法希望一个问题的解能够表示成一个n元式(x1,x2,…,xn)的形式。
- 显约束:对分量xi的取值限定。
- 隐约束:为满足问题的解而对不同分量之间施加的约束。
- 解空间:对于问题的一个实例,解向量满足显式约束条件的所有多元组,构成了该实例的一个解空间。
3.生成问题状态的基本方法
- 问题状态的生成是开始结点出发,在搜索过程中不断扩展已有的结点来完成。生成问题状态有两种本质上不同的方法:深度优先生成方法和广度优先生成方法。
- 深度优先的问题状态生成法:对一个结点R,一旦产生了它的一个子结点C,就把C作为当前结点进行扩展。在完成对以C为根的子树的穷尽搜索之后,将R重新变成当前结点,继续生成R的下一个孩子(如果存在)
- 宽度优先的问题状态生成法:在对一个结点扩展产生了它的一个子结点后,继续生成它的下一个孩子结点,直至所有的孩子结点都生成。
问题状态生成过程中结点的状态: - 扩展结点:一个正在产生子结点的结点称为扩展结点
活结点:结点已经生成但其子结点还全部生成的结点称为活结点。
死结点:一个所有子结点已经产生的结点称做死结点
4.步骤
-
回溯法:为了避免生成那些不可能产生最佳解的问题状态,要不断地利用限界函数(bounding function)来处死那些实际上不可能产生所需解的活结点,以减少问题的计算量。具有限界函数的深度优先生成法称为回溯法
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。 -
常用剪枝函数:
用约束函数在扩展结点处剪去不满足约束的子树;
用限界函数剪去得不到最优解的子树。
用回溯法解题的一个显著特征是在搜索过程中动态产生问题的解空间。在任何时刻,算法只保存从根结点到当前扩展结点的路径。如果解空间树中从根结点到叶结点的最长路径的长度为h(n),则回溯法所需的计算空间通常为O(h(n))。而显式地存储整个解空间则需要O(2h(n))或O(h(n)!)内存空间。
5. 子集树和排列树
- 子集树
所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间成为子集树。 - 排列树
所给的问题是确定n个元素满足某种性质的排列时,相应的解空间就是排列树。
6. 0-1背包问题
- 问题:给定n种物品和一背包。物品i的重量是wi,其价值为pi,背包的容量为C。问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
- 分析:问题是n个物品中选择部分物品,可知,问题的解空间是子集树。比如物品数目n=3时,其解空间树如下图,边为1代表选择该物品,边为0代表不选择该物品。使用x[i]表示物品i是否放入背包,x[i]=0表示不放,x[i]=1表示放入。回溯搜索过程,如果来到了叶子节点,表示一条搜索路径结束,如果该路径上存在更优的解,则保存下来。如果不是叶子节点,是中点的节点(如B),就遍历其子节点(D和E),如果子节点满足剪枝条件,就继续回溯搜索子节点。
#include <stdio.h>
#define N 3 //物品的数量
#define C 16 //背包的容量
int w[N]={10,8,5}; //每个物品的重量
int v[N]={5,4,1}; //每个物品的价值
int x[N]={0,0,0}; //x[i]=1代表物品i放入背包,0代表不放入
int CurWeight = 0; //当前放入背包的物品总重量
int CurValue = 0; //当前放入背包的物品总价值
int BestValue = 0; //最优值;当前的最大价值,初始化为0
int BestX[N]; //最优解;BestX[i]=1代表物品i放入背包,0代表不放入
//t = 0 to N-1
void backtrack(int t)
{
//叶子节点,输出结果
if(t>N-1)
{
//如果找到了一个更优的解
if(CurValue>BestValue)
{
//保存更优的值和解
BestValue = CurValue;
for(int i=0;i<N;++i) BestX[i] = x[i];
}
}
else
{
//遍历当前节点的子节点:0 不放入背包,1放入背包
for(int i=0;i<=1;++i)
{
x[t]=i;
if(i==0) //不放入背包
{
backtrack(t+1);
}
else //放入背包
{
//约束条件:放的下
if((CurWeight+w[t])<=C)
{
CurWeight += w[t];
CurValue += v[t];
backtrack(t+1);
CurWeight -= w[t];
CurValue -= v[t];
}
}
}
//PS:上述代码为了更符合递归回溯的范式,并不够简洁
}
} ```