目录
四、整数划分问题/自然数拆分(递归方法也可以实现,见递归部分)
一、概述
- 以深度优先的方式系统地搜索问题的解的方法称为回溯法。
- 回溯算法是深度优先策略的典型应用,回溯算法就是沿着一条路向下走,如果此路不通了,则回溯到上一个分岔路,再选一条路走,一直这样递归下去,直到遍历完所有的路径。
- 回溯算法的解空间结构可以组成成子集树(0-1背包问题、全排列)、排列树。会画出解决某个问题的解空间树,比如会画出描述3个物品的背包问题的解空间树等
旅行商问题就是一颗排列数
4.使用回溯法解题,通常包括的三个步骤
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先的方式搜索解空间,并且在搜索过程中使用剪枝函数避免无效搜索。
5.在回溯法搜索解空间树时,通常采用剪枝函数(包括约束函数和限界函数)避免无效搜索以提高回溯法的搜索效率。针对0-1背包问题,可以定义哪些剪枝规则,并能对这些剪枝规则进行简单描述。
- 用约束函数在扩展结点处剪去不满足约束条件的子树;
- 用限界函数剪去不能得到最优解的子树。
6.回溯算法搜索子集树的伪代码(之一)
void backtrack (int t){ //形参t为树的深度,根为1
if (t>n)
update(x); //扩展到叶子结点,得到了一组解决方案
else
for (int i=0; i<2; i++) //每个结点有2个子树
{
x[t]=i;// 即0/1,表示第 t个元素是否是可选元素
if (constraint(t) && bound(t)) //判断当前结点是否是扩展结点
backtrack(t+1); //对当前结点进行扩展
} }
7.回溯算法搜索子集树的伪代码(之二)
void backtrack (int t){ //形参t为树的深度,根为1
for (int i=0; i<2; i++){ //每个结点有n个子树
if (t>n) {
update(x); //扩展到叶子结点,得到了一组解决方案
break;
}
x[t]=i; //即0/1,表示第 t个元素是否是可选元素
if (constraint(t) && bound(t)) //判断当前结点是否是扩展结点
backtrack(t+1); //对当前结点进行扩展
}
}
8.回溯算法搜索排列树的伪代码
void backtrack (int t){//形参t为树的深度,根为1
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]); //恢复状态
}
}
9.回溯算法解决:素数环问题、n个数中取出任意r个数进行排列、自然数拆分(递归方法也可以实现,见递归部分)、装载问题(最大装载量)、0-1背包问题、n皇后问题、子集和问题
知道图的m着色问题、旅行商问题、流水作业调度问题可以使用回溯法解决即可。
二、素数环问题
素数环:从1到20这20个数摆成一个环,要求相邻的两个数的和是一个素数。
分析:
从1开始,每个空位有20种可能,只要填进去的数合法:
1、与前面已经填过的数不相同;
2、与左边相邻的数的和是一个素数。
3、第20个数还要判断和第1个数的和是否素数
子集数—全排列
算法流程:
1、数据初始化(布尔数组,标记某个数是否已经填入到素数环中);
2、递归填数:判断第i个数填入是否合法;
A、如果合法(剪枝条件):
填数;判断是否到达目标(20个已填完):
是,打印结果;
不是,递归填下一个;
- 如果不合法:选择下一种可能;
代码:
- 子集数:
void search(int t)//寻找所有解
{
int i;
for(i=1;i<=20;i++)
{
if(pd(a[t-1],i)&&(!b[i])) //与前一个数是否构成素数及该数是否可用
{
a[t]=i;//i进入素数环
b[i]=1;//标志1
if(t==20)//到最后了,得到一个解
{
if(pd(a[20],a[1]))//判断最后一个和第一个
print();
}
else
search(t+1);
b[i]=0;
}
}
}
- 排列数:
void search(int t)//寻找所有解
{
int i;
for(i=t;i<=20;i++)
{
swap(a[t],a[i]);
if(pd(a[t-1],a[t]))
{
if(t==20)
{
if(pd(a[20],a[1]))
print();
}
else
search(t+1);
}
swap(a[t],a[i]);
}
}
完整代码:
#include <iostream>
#include<algorithm>
#include<cstring>
#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,int);//判断两数之和是不是素数
void search(int t)//寻找所有解
{
int i;
for(i=t;i<=20;i++)
{
swap(a[t],a[i]);
if(pd(a[t-1],a[t]))
{
if(t==20)
{
if(pd(a[20],a[1]))
print();
}
else
search(t+1);
}
swap(a[t],a[i]);
}
}
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;
}
int main()
{
for(int i=1;i<=20;i++)
a[i]=i;
search(1);
cout<<total<<endl; //输出总方案数
return 0;
}
三、n个数中取出任意r个数进行排列
设有n个整数【1-n】的集合{1,2,…,n},从中取出任意r个数进行排列(r<n),试列出所有的排列
输入:n r
输出:方案数
代码:
#include <iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
int num=0,a[101]={0},n,r;//从1-n,取r个数
bool b[101]={0};//标志
//void search(int ,int);//回溯过程
void print();//输出方案
int search(int k,int start) //两个参数。k代表组合中的第几个数,start从哪个位置开始选择数字
{
for(int i=start;i<=n;i++)
{
a[k]=i;//保存结果
if (k==r)
print();
else
search(k+1,i+1);
}
}
void print()
{
num++;
for(int j=1;j<=r;j++)
cout<<a[j]<<" ";
cout<<endl;
}
int main()
{
cin>>n>>r;
search(1,1);
cout<<num<<endl;//输出方案总数
}
四、整数划分问题/自然数拆分(递归方法也可以实现,见递归部分)
任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和。
#include <iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
int total,a[101]={1},n;
int search(int s,int t);//表待拆分的数; 序列中t 代表拆分序列中的数的编号
int print(int);//输出方案
int search(int s,int t)
{
int i;
for(i=a[t-1];i<=s;i++)
if(i<n)当前数i要大于等于前1位数,且不过n
{
a[t]=i;//保存当前拆分的数i
s-=i;//s减去数i, s的值将继续拆分
if(s==0)
print(t); //当s=0时,拆分结束输出结果
else
search(s,t+1);//当s>0时,继续递归
s+=i;//回溯:加上拆分的数,以便产生所有可能的拆分
}
}
int print(int t)
{
cout<<n<<"=";
for(int i=1;i<=t-1;i++)
cout<<a[i]<<"+";
cout<<a[t]<<endl;
total++;
}
int main()
{
cin>>n;
search(n,1);//将要拆分的数n传递给s
cout<<total<<endl;//输出方案总数
}
五、装载问题(最大装载量)
给定n个集装箱要装上一艘载重量为c的轮船,其中集装箱i的重量为wi。集装箱装载问题要求确定在不超过轮船载重量的前提下,将尽可能多的集装箱装上轮船(贪心算法中的装载问题讨论的是装载件数;本题讨论的是最大装载重量。)
输入:
每组测试数据:第1行有2个整数c和n。c是轮船的载重量(0<c<30000),n是集装箱的个数(n≤20)。第2行有n个整数w1, w2, …, wn,分别表示n个集装箱的重量。
输出:
对每个测试例,输出两行:第1行是装载到轮船的最大载重量,第2行是集装箱的编号。
分析:
假设解向量为X(x1, x2, …, xn),其中xi∈{0, 1}, xi =1表示集装箱i装上轮船, xi =0表示集装箱i不装上轮船。
用回溯法解装载问题时,其解空间是一棵子集树,与0 - 1背包问题的解空间树相同。
int cw, 当前的轮船的荷载
int bestcw , 当前的最大荷载,荷载到达叶子结点才会更新
int r ; 剩余的未考察的集装箱重量和
约束函数剪枝:cw>c
限界函数剪枝:cw+r<bestw
不明白为什么关于这个问题的的解决方案大都用了类
方法一:课上方法,函数参数太多,有点乱
#include <iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
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;
}
};
void load(goods *g, int *x, int t, int n,int cw, int &bestcw ,int *best,int r,int c)//轮船载重
//goods *g,集装箱列表,*x,满足当前最大荷载的装载方案,t,子集树的层号,n,集装箱的总数
// cw, 当前的轮船的荷载,bestcw ,当前的最大荷载, *best,待求解的最优装载方案;r,剩余的未考察的集装箱重量和
{
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>>c>>n;
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;
}
方法二:如果用类,为什么不直接全部使用类,方法单独定义,这样就不用传那么多参数了,两个方法其实是一样的,但是这样看起来是不是简洁好理解一点了
#include<bits/stdc++.h>
using namespace std;
int bestx[101];//满足当前最大荷载的装载方案
class Loading
{
int MaxLoading(int[],int,int);//初始化的函数
public:
void Backtrack(int i);//回溯
int n;//集装箱数
int* w,//集装箱重量数组
c,//轮船的载重量
cw,//当前载重量,当前的轮船的荷载
bestw,//当前的最大荷载,当前最优载重量
r;//剩余未考察的集装箱重量
};
void Loading::Backtrack(int i)
{
//搜索第i层结点
if(i>n)//到达叶节点
{
if(cw>bestw)
bestw=cw;//更新最优解
return;
}
r-=w[i];//修改剩余集装箱重量
if(cw+w[i]<=c)//约束函数进行剪枝
{
cw+=w[i];//装入
bestx[i]=1;
Backtrack(i+1);
cw-=w[i];//回溯
}
if(cw+r>bestw) //限界规则
{
Backtrack(i+1);
bestx[i]=0;
r+=w[i];//修改剩余集装箱重量
}
}
int MaxLoading(int w[],int c,int n)
{
//初始化
Loading X;
X.w = w;
X.c = c;
X.n = n;
X.bestw = 0;
X.cw = 0;
//初始化
X.r = 0;
for(int i=1;i<=n;i++)
{
X.r+=w[i];
}
//初始时的r为全体物品重量和
X.Backtrack(1);//从第一层开始搜索
return X.bestw;
}
int main()
{
int c,n,m;
cin>>c>>n;
int w[101];
for(int i=1;i<=n;i++)
cin>>w[i];
m=MaxLoading(w, c, n);
cout<<"最大装载量为:"<<endl;
cout<<m<<endl;
for(int i=1;i<=n;i++)
{
cout<<"x["<<i<<"]:"<<bestx[i]<<endl;
}
return 0;
}
六、0-1背包问题
给定一个物品集合s={1,2,3,…,n},物品i的重量是wi,其价值是vi,背包的容量为W,即最大载重量不超过W。在限定的总重量W内,我们如何选择物品,才能使得物品的总价值最大。
输入:
第一行是背包容量 c,物品个数n
接下来n行是物品i的重量是wi,其价值为vi
输出:
输出装入背包中物品的最大价值。
分析:0-1背包问题(子集数)
- 令cw(i)表示目前搜索到第i层已经装入背包的物品总重量
- 令cv(i)表示目前到第i层结点已经装入背包的物品价值
- 对于左子树, xi =1 ,其约束函数为:construction(i)=cw(i-1)+wi。若constraint(i)>W,则停止搜索左子树,否则继续搜索。
- 对于右子树,r(i)表示剩余物品的总价值,限界函数Bound(i)=cv(i)+r(i)
- r(i)越小, Bound(i)越小,剪掉的分支就越多(越靠近树根剪枝,剪掉的分支越多。从而能加快搜索速度)。
- 为了构造更小的r(i) ,将物品以单位重量价值比di=vi/wi递减的顺序进行排列(贪心策略)
代码:
#include<bits/stdc++.h>
using namespace std;
int c,n;//背包容量、物品数量
int cw,cv;//背包当前重量,当前价值
int bestv;//当前最优价值
struct bag
{
int w; //物品的重量
int v; //物品的价值
double d; //物品的单位重量价值比
}a[101];
bool cmp(bag a,bag 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&&a[i].w<=cleft)
{
cleft-=a[i].w;
b+=a[i].v;
i++;
}
/*剩余的部分空间也装满。0-1是可能装不满的。
但此处主要计算最大值,所以,需要从装满的角度考虑该问题
(尽管这不是一个可行解,但可以证明其价值是最优值的上界)
*/
if(i<n)
b+=1.0*cleft*a[i].v/a[i].w;
return b;
}
int ff(int i)//形参i是回溯的深度,从0开始.商品编号从0开始编号
{
if(i==n)
{
bestv=cv;//当前最优价值=当前价值
// return ;
}
//进入左子树搜索, 表示选择第i件物品
if(cw+a[i].w<=c)//满足约束条件
{
cw+=a[i].w;//选择第i件物品
cv+=a[i].v;
ff(i+1);
cw-=a[i].w;//回溯的需要
cv-=a[i].v;//回溯需要
}
//进入右子树搜索,表示不选择第i件物品,相关的cw, cv不改变
if(Bound(i+1)>bestv)//大于当前最优价值,满足条件不剪枝
{
ff(i+1);
}
}
int main()
{
cin>>c>>n;
for(int i=0;i<n;i++)
{
cin>>a[i].w>>a[i].v;
a[i].d=1.0*a[i].v/a[i].w;
}
sort(a,a+n,cmp);
ff(0);
cout<<bestv<<endl;
}
七、n皇后问题
在n×n格的棋盘上放置彼此不受攻击的n个皇后。
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n皇后问题等价于在n×n格的棋盘上放置n个皇后,任何两个皇后不放在同一行或同一列或同一斜线上。
编程要求:找出一个n×n格的棋盘上放置n个皇后并使其不能互相攻击的所有方案。
输入样例:5【皇后个数】
输出样例:
1 3 5 2 4
1 4 2 5 3
2 4 1 3 5
2 5 3 1 4
3 1 4 2 5
3 5 2 4 1
4 1 3 5 2
4 2 5 3 1
5 2 4 1 3
5 3 1 4 2
Total= 10
【输出样例含义:拿第一个来说,第一例的1,第二列的3,第二列的5.......】
分析:
由于棋盘的每列/行只有一个皇后,所以可以用一维向量X( x1, x2, …, xn),其中xi∈{1, 2, …, n},表示第i列皇后所在的行x[i]。解空间的每个结点都有n个儿子,因此解空间的大小为nn,这是一棵子集树。
同一对角线判断:
#include<bits/stdc++.h>
using namespace std;
int n;//棋盘大小
int x[101];//解向量
int sum=0;//当前已经找到的可行方案数
inline 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;
}
void Back(int t)//t是回溯的深度,从1开始
{
int i;
if(t>n)//到达叶子结点
{
sum++;//方案数+1
for(i=1;i<=n;i++)
cout<<x[i]<<" ";
cout<<endl;
}
else
{
for(i=1;i<=n;i++)
{
x[t]=i;
if(Place(t))//判断是否符合条件
Back(t+1);
}
}
}
int main()
{
cin>>n;
Back(1);
cout<<sum<<endl;
}
八、子集和问题
子集和问题的一个实例为<S,c>。其中,S={w1, w2, …, wn}是一个正整数的集合,c是一个正整数。子集和问题判定是否存在S的一个子集S1,使得S1的和为c。
编程任务:对于给定的正整数集合S={w1, w2, …, wn}和正整数c,编程计算S的一个子集S1,使得S1的和为c。
输入:
第一行有2个正整数n和c,n表示S的大小,c是子集和的目标值。接下来的一行,有n个正整数(1≤n≤10000),表示集合S中的元素。
输出:
输出子集和问题的全部解,每个数据后面都有一个空格。当问题无解时,输出“No Solution!”
分析:用回溯法求解子集和问题,与0-1背包问题类似,解空间树是一棵子集树。
代码:
#include<bits/stdc++.h>
using namespace std;
class Sum{
int n; //集合中数据的个数
int *set;//集合
int *x;//解向量,当x[i]=1表示第i 个元素属于子集,否则,x[i]=0;
int c;//输入的条件。要计算的子集和
int s;//正在构造的子集中元素的和
int r;//不在子集中的未考察的其他数据元素之和;
int total;//记录符合条件的子集的个数
public:
Sum(int n=0,int c=0);//构造函数
void calculate(int t);//计算
void display();
int get_T()//方案总数
{
return total;
}
};
Sum::Sum(int n,int c)
{
this->n=n;
this->c=c;
x=new int [n+1];
set=new int [n+1];
cout<<"请输入集合中的元素"<<endl;
r=0;
total=0;
for(int i=1;i<=n;i++)
{
cin>>set[i];
x[i]=0;
r=r+set[i];
}
s=0;
}
void Sum::calculate(int t)
{
if(t>n)
{
if(c==s)
{
display();
total++;
}
}
else{
r=r-set[t];
if(s+set[t]<=c)//约束规则
{
x[t]=1;
s=s+set[t];
calculate(t+1);
s=s-set[t];
x[t]=0;
}
if(s+r>=c)//限界函数
calculate(t+1);
r=r+set[t];
}
}
void Sum::display()
{
int i;
for(int i=1;i<=n;i++)
if(x[i]==1)
cout<<set[i]<<" ";
cout<<endl;
}
int main()
{
int n,c;
cout<<"输入n 和c 的值:";
cin>>n>>c;
Sum s(n,c);
s.calculate(1);
if(s.get_T()==0)
cout<<"NO Solution!!"<<endl;
else
cout<<"方案数="<<s.get_T()<<endl;
return 0;
}
这几章不好懂,大家一起加油呀!!!