回溯算法
-
以深度优先的方式系统地搜索问题的解的方法称为回溯法。
-
可以系统地搜索一个问题的所有解或任意解。
-
有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。
-
回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。
0 —1背包问题
假设背包容量C=30,w={16,15,15},v={45,25,25}
旅行商问题
在回溯法搜索解空间树时,通常采用两种策略(剪枝函数)避免无效搜索以提高回溯法的搜索效率:
- 用约束函数在扩展结点处剪去不满足约束条件的子树;
- 用限界函数剪去不能得到最优解的子树。
解0—1背包问题的回溯法用剪枝函数剪去导致不可行解的子树。
解旅行商问题的回溯算法中,如果从根结点到当前扩展结点的部分周游路线的费用已超过当前找到的最好周游路线费用,则以该结点为根的子树中不包括最优解,就可以剪枝。
回溯算法搜索子集树的伪代码
(深度优先遍历)
void backtrack (int t){ //形参t为树的深度,根为1
if (t>n)
update(x); //扩展到叶子结点,得到了一组解决方案
else
for (int i=0; i<=1; i++) //每个结点只有两个子树
{
x[t]=i; //即0/1,表示第 t个元素是否是可选元素
if (constraint(t) && bound(t)) //判断当前结点是否是扩展结点
backtrack(t+1); //相当于对当前结点按照深度优先搜索的方式进行扩展
}
}
回溯算法搜索排列树的伪代码
(根据全排思想)
//形参t为树的深度,根为1
void backtrack (int t){
if (t>n)
update(x); //得到了一个全排列,对排列结果进行更新
else
for (int i=t; i<=n; i++) {
//为了保证排列中每个元素不同,通过交换 来实现
swap(x[t], x[i]);
if (constraint(t) && bound(t))
backtrack(t+1);
swap(x[t], x[i]); //恢复状态
}
}
素数环*
分析:
从1开始,每个空位有20种可能,只要填进去的数合法:
- 与前面的数不相同;
- 与左边相邻的数的和是一个素数。
- 第20个数还要判断和第1个数的和是否素数。
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<cmath>
using namespace std;
bool b[21]={0}; //判断i是否出现在素数环中
int total=0,a[21]={0}; //a记录素数环中的每一个数
void search(int t); //回溯过程。形参表示素数环中的数的编号
void print(); //输出方案
bool pd(int x,int y); //判断素数
void search(int t){ //寻找所有解
int i;
for (i=1;i<=20;i++) //有20个数可选
if (pd(a[t-1],i)&&(!b[i])){ //判断与前一个数是否构成素数及该数是否可用
a[t]=i; //素数环中的第t个数
b[i]=1; //i进入素数环
if (t==20) { //一个解
if (pd(a[20],a[1])) print();}
else
search(t+1);
b[i]=0;
}
}
int main(){
search(1);
cout<<total<<endl; //输出总方案数
}
void print(){
total++;
cout<<"<"<<total<<">";
for (int j=1;j<=20;j++)
cout<<a[j]<<" ";
cout<<endl;
}
bool pd(int x,int y){
int k=2,i=x+y;
while (k<=sqrt(i)&&i%k!=0) k++;
if (k>sqrt(i)) return 1;
else return 0;
}
设有n个整数的集合{1,2,…,n},从中取出任意r个数进行排列(r<n),试列出所有的排列。
#include<cstdio>
#include<iostream>
#include<iomanip>
using namespace std;
int num=0,a[10001]={0},n,r;
bool b[10001]={0};
void search(int); //回溯过程
void print(); //输出方案
int main(){
cout<<"input n,r:";
cin>>n>>r;
search(1);
cout<<"number="<<num<<endl; //输出方案总数
}
void search(int k){
int i;
for (i=1;i<=n;i++)
if (!b[i]) { //判断i是否可用
a[k]=i; //保存结果
b[i]=1;
if (k==r)
print();
else
search(k+1);
b[i]=0;
}
}
void print(){
num++;
for (int i=1;i<=r;i++)
cout<<setw(3)<<a[i];
cout<<endl;
}
装载问题
分析:
-
贪心算法中的装载问题讨论的是装载件数;本题讨论的是最大装载重量。
-
用回溯法解装载问题时,其解空间是一棵子集树,与0 - 1背包问题的解空间树相同。
可行性约束函数(剪去不满足约束条件的子树):
cw(t)表示从根结点到第t层结点为止装入轮船的重量
当cw(t)>c时,表示该子树中所有结点都不满足约束条件,可将该子树剪去。
#include <iostream>
using namespace std;
class goods{
int weight;
public:
goods(int w=0):weight(w)
{}
int get_w(){
return weight;
}
void set(int w){
weight=w;
}
};
//goods *g,集装箱列表
//int *best,待求解的最优装载方案
//int t,子集树数的层号。根节点在第0层,叶节点在第n层
//int n,集装箱的总数
//int &cw, 当前的轮船的荷载
//int bestcw ,当前的最大荷载
//int *x,满足当前最大荷载的装载方案
//int r剩余的集装箱重量和
void load(goods *g, int *x, int t, int n,int cw, int &bestcw ,int *best,int r,int c){
if(t>n) { //已经遍历的到叶子结点,得到了一个解决方案
if(cw>bestcw) {
for(int i=0;i<n;i++)
best[i]=x[i];
bestcw=cw;
}
}
else{ //每个结点可以有两个分支,分别利用约束规则和限界规则进行剪枝
r=r-g[t].get_w();//剩余未处理的物品的重量和,与是否选取当前物品无关
if(cw+g[t].get_w()<=c){ // 根据题意中的约束条件进行剪枝
x[t]=1;
cw=cw+g[t].get_w(); //当前装入的物品的重量和
load(g,x,t+1,n,cw,bestcw,best,r,c);
cw=cw-g[t].get_w(); //回溯的需要
}
if(cw+r>bestcw) { //限界规则
x[t]=0;
load(g,x,t+1,n,cw,bestcw,best,r,c);
}
r=r+g[t].get_w(); //回溯的需要
}
}
int main(){
int n,c,bestcw=0;
int *x,*best, r=0;
cout<<"请输入物品的件数和轮船的装载重量:";
cin>>n>>c;
goods *g;
g=new goods[n];
x=new int [n];
best=new int[n];
cout<<"请输入每件物品的重量:";
for(int i=0;i<n;i++) {
int w; cin>>w; g[i].set(w);r=r+w;
}
load(g,x,0,n,0,bestcw,best,r,c);
cout<<bestcw<<endl;
for(int i=0;i<n;i++)
cout<<best[i]<<" ";
cout<<endl;
return 0;
}
0—1背包问题(贪心+回溯的优化)
分析:
令cw(i)表示目前搜索到第i层已经装入背包的物品总重量,即部分解(x1, x2 , …, xi)的重量:
对于左子树,约束函数为:
constraint(i)=cw(i-1)+W(i)
若constraint(i)>W,则停止搜索左子树,否则继续搜索。
对于右子树,第i层装入的价值为:
界限函数:
Bound(i)=cv(i)+r(i);
- 物品的价值会对限界函数产生影响
- 物品的重量会对约束函数产生影响
- 由于根据物品价值制定贪心策略,所以,通过单位价值排序,加速剪枝
#define NUM 100
#include<iostream>
using namespace std;
#include <algorithm>
int c; //背包的容量
int n; //物品的数量
int cw; //当前重量
int cv; //当前价值
int bestv; //当前最优价值
//描述每个物品的数据结构
struct Object{
int w; //物品的重量
int v; //物品的价值
double d; //物品的单位重量价值比
}Q[NUM];
//对物品以单位重量价值比递减排序的因子是:
bool cmp(Object a, Object b)
{
if(a.d>=b.d) return true;
else return false;
}
int Bound (int i){
int cleft=c-cw;
int b=cv;
while(i<n&&Q[i].w<=cleft){
cleft-=Q[i].w;
b+=Q[i].v;
i++;}
if(i<n) b+=1.0*cleft*Q[i].v/Q[i].w;
return b;
}
void backtrack(int i){
if(i+1>n){bestv=cv;return;}
if(cw+Q[i].w<=c){
cw+=Q[i].w;
cv+=Q[i].v;
backtrack(i+1);
cw-=Q[i].w;
cv-=Q[i].v;
}
if(Bound(i+1)>bestv) backtrack(i+1);
}
int main(){
n=5;
for(int i=0;i<n;i++)
{
cin>>Q[i].w>>Q[i].v;
Q[i].d = 1.0*Q[i].v/Q[i].w;
}
sort(Q,Q+n,cmp);
backtrack(1);
}
图的m着色问题
图的m着色问题的约束函数是相邻的两个顶点需要着不同的颜色,但是没有限界函数。
数据结构:
#define NUM 100
int n; //图的顶点数量
int m; //可用颜色数量
int a[NUM][NUM]; //图的邻接矩阵
int x[NUM]; //当前的解向量
int sum ; //已经找到的着色的方案数量
回溯方法:
//形参t是回溯的深度,从1开始
void BackTrack(int t ){
int i;
//到达叶子结点,获得一个着色方案
if( t > n ) {
sum ++ ;
for(i=1; i<=n ;i++)
printf("%d ",x[i]);
printf("\n");
}
else //搜索当前扩展结点的m个孩子
for(i=1; i<=m; i++ ){//尝试每种颜色
x[t] = i;
if( Same(t) ) //判断t 和其邻接点的着色方法是否相同
BackTrack(t+1);
x[t] = 0;//回溯的需要,恢复到未着色的状态
}
}
检查相邻结点是否一样的约束函数:
//形参t是回溯的深度
bool Same(int t)
{
int i;
for(i=1; i<=n; i++ ) //检查t与所有顶点的邻接关系,包括染色和未染色的。
if( (a[t][i] == 1) && (x[i] == x[t]))
return false;
return true;
}
n皇后问题
分析:
由于棋盘的每列/行只有一个皇后,所以可以用一维向量X( x1, x2, …, xn),其中xi∈{1, 2, …, n},表示第i列皇后所在的行x[i],即解空间的每个结点都有n个儿子,因此解空间的大小为n^n,这是一棵子集树。
数据结构:
#define NUM 20
int n; //棋盘的大小
int x[NUM]; //解向量
int sum; //当前已经找到的可行方案数
回溯算法:
void Backtrack(int t) {//形参t是回溯的深度,从1开始
int i;
//到达叶子结点,获得一个可行方案。累计总数,并输出该方案
if (t>n) {
sum++; //是全局变量
for (i=1; i<=n; i++)
printf(" %d", x[i]);
printf("\n");
}
else
for (i=1; i<=n; i++) {
x[t] = i;
if (Place(t)) Backtrack(t+1);
}
}
约束函数:
bool Place(int t) {
int i;
for (i=1; i<t; i++)
if ((abs(t-i) == abs(x[i]-x[t])) || (x[i] == x[t])) //同一条对角线;同一行
return false;
return true;
}
旅行商问题
分析:解空间为一棵排列树
数据结构:
#define NUM 100
int n; //图G的顶点数量
int m; //图G的边数
int a[NUM][NUM]; //图G的邻接矩阵
int x[NUM]; //当前解
int bestx[NUM]; //当前最优解向量
int cc; //当前费用
int bestc; //当前最优值
int NoEdge = -1; //无边标记
注意:
a[i][j]:初始值为NoEdge
bestc:初始值为NoEdge
x[i]:初始值为i
回溯算法的实现:
//形参t是回溯的深度,从2开始。根结点为第1层。城市编号从1开始.从第1个城市出发
void Backtrack(int t){
//到达叶子结点的父结点。旅行路径上的倒数第二个城市。最后一个城市是出发点
if(t==n) {
if(a[x[n-1]][x[n]]!= NoEdge && a[x[n]][1]!= NoEdge &&
(cc + a[x[n-1]][x[n]]+a[x[n]][1]<bestc||bestc== NoEdge))
{
for(int i=1; i<=n; i++)
bestx[i] = x[i];
bestc = cc + a[x[n-1]][x[n]] + a[x[n]][1];
}
return;
}
else {
for(int i=t; i<=n; i++)
{
if(a[x[t-1]][x[i]]!= NoEdge &&
(cc + a[x[t-1]][x[i]]< bestc||bestc == NoEdge))
{
swap(x[t],x[i]); // 先交换。此时,X[t]的值发变化。
cc += a[x[t-1]][x[t]]; //再计算。不能交换顺序。
Backtrack(t+1); // 如果要改变顺序,应该为cc += a[x[t-1]][x[i]]。
cc -= a[x[t-1]][x[t]];
swap(x[t],x[i]);
}
}
}
}