回溯法一般解题步骤:
- 针对所给问题的解空间,首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个最优解。
- 确定结点的扩展搜索规则。
- 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
结点扩展:类似暴力法的深度优先搜索的扩展方式。
剪枝:用约束函数剪不满足约束条件的子树,用界限函数剪除不能得到最优解的子树。
回溯法的非递归框架
int x[n],i; //x存放解向量,全局变量
初始化数组x[];
i = 1;
while (i > 0 (有路可走) and (未达到目标)) { //还未回溯到头
if (i > n) { //搜索到叶子结点
搜索到1个解,输出;
}
else //处理第i个元素
{
x[i]取第1个可能的值;
while (x[i]不满足约束条件且在搜索空间内) {
x[i]下一个可能的值;
}
if (x[i]在搜索空间内) {
标识占用的资源;
i = i + 1;
}
else
{
清理所占用的状态空间; //回溯
i = i - 1;
}
}
}
子集树和排列树
1. 子集树
所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间成为子集树。
如0-1背包问题,从所给重量、价值不同的物品中挑选几个物品放入背包,使得在满足背包不超重的情况下,背包内物品价值最大。它的解空间就是一个典型的子集树。
回溯法搜索子集树的算法范式如下:
void backtrack(int i) //i为树的根
{
if (i > n) //达到叶子结点
output(x); //找到一个解
else
{
for (int j=0; j<=1; j++) { //每个结点只有两个子树
x[i]=j;
if (constraint(i) && backtrack(i))
backtrack(i+1);
}
}
}
2. 排列树
所给的问题是确定n个元素满足某种性质的排列时,相应的解空间就是排列树。
如旅行售货员问题,一个售货员把几个城市旅行一遍,要求走的路程最小。它的解就是几个城市的排列,解空间就是排列树。
回溯法搜索排列树的算法范式如下:
void backtrack (int i)
{
if (i>n)
{
output(i);
}
else
{
for (int j=i;i<=n;i++)
{ //为了保证每个元素的不同,通过x[i], x[j]来交换实现
swap(x[i], x[j]);
if (constraint(i)) backtrack(i+1);
swap(x[i], x[j]); //回复
}
}
}
1. 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 4 //物品的数量
#define C 7 //背包的容量
int w[N]={5, 3, 2, 1}; //每个物品的重量
int v[N]={4, 4, 3, 1}; //每个物品的价值
int x[N]={0, 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)
{
//如果找到了一个更优的解
if(CurValue > BestValue)
{
//保存更优的值和解
BestValue = CurValue;
for(int i=0; i<N; ++i) BestX[i] = x[i];
}
}
else
{
x[t]=1;
//约束条件:放的下
if((CurWeight + w[t]) <= C) //不超过背包承受的重量
{
CurWeight += w[t];
CurValue += v[t];
backtrack(t+1);
CurWeight -= w[t];
CurValue -= v[t];
}
x[t] = 0;//回溯
backtrack(t+1);
}
}
int main(int argc, char* argv[])
{
backtrack(0);
printf("最优值:%d\n",BestValue);
for(int i=0;i<N;i++) {
printf("最优解:%-3d",BestX[i]);
}
return 0;
}
求n个元素中m个元素的全排列
如:1,2,3,当m = 2 时,全排列为:(1,2),(1,3),(2,3)
#include <stdio.h>
#include <string.h>
#define MAXN 20
#define MAXM 10
int m, n;
int x[MAXM]; //用装载一个排列
bool used[MAXN]; //表示第i个元素是否在当前排列中
int mcount = 0; //统计部分全排列的个数
void partperm(int i) //求n个元素中m个元素的全排列
{
int j;
if (i > m - 1) {
printf("[");
for (j = 0; j < m; ++j)
printf("%d", x[j]);//输出一个排列
printf("] ");
if (++mcount % 5 == 0) printf("\n");
} else {
for (j = 0; j < n; ++j) {
if (!used[j]) {
used[j] = true;
x[i] = j;
partperm(i + 1);
used[j] = false;
}
}
}
}
int main(int argc, char* argv[])
{
m = 2;
n = 5;
memset(used, 0, sizeof(used));
partperm(0);
printf("cout = %d\n", mcount);
return 0;
}
输出:
[01] [02] [03] [04] [10]
[12] [13] [14] [20] [21]
[23] [24] [30] [31] [32]
[34] [40] [41] [42] [43]
cout = 20