POJ 1040
回溯法
所谓回溯法,非常考验人的耐心,是一道耐人寻味且趣味的题目,特喵的就是就是毒瘤题 ,所以呢在此去写一下这篇坑爹题目(bushi)的题解。
回溯法实际上是枚举算法的一种,它讲究的是控制策略,算法从初始状态出发,按照纵深顺序递归扩展所有可能情况情况,从中找出满足题意条件的答案。我们从这个定义看出回溯法是非常讲究如何扩展所有可能情况的解的,这就要求我们必须得找出可能情况的合理表示,通过问题的分析找出题目的搜索范围。和动态规划一样,回溯法非常注重状态是如何设计的,有时状态设计的不好甚至可能无从下手。紫书有言,回溯法实际上可以看作是隐式图的遍历。这样状态就可以看作是图中一个抽象的点,然后进行遍历。
当时我不理解的地方在于是为什么会有恢复状态的表示,实际上这是将全局变量当作是点的一个参数而已,扩展节点的时候局部变量可以轻易的作为递归函数,但是全局变量却是不可以的。所以这就是回溯法这一个名词的由来。
现在让我们去看一下回溯法的几个比较重要的方面:
- 回溯法的程序流程有什么特征
- 使用回溯法需要考虑哪些因素
- 怎样将这些思考把这些想法实现
void search(status){
if(status is the boundary conditions){
update the optimal answer
return;
}
for(every posible options){
update the latest status
if(the latest status
meets the constraint and optimal conditions){
search(latest status)
}
}
}
这就是基本的搜索框架,它并不是固定不变的,相反我们需要灵活的想法去实现些框架。比如统计路径的时候我们可以在满足约束条件下将答案写入动态数组,实现非最优化的时候可以将边界条件是否满足最优解可以去掉,然后计数问题统计答案的时候可以在每次扩展子节点的时候记录以防重复计数。
回溯法一般需要考虑的问题主要有:
- 定义状态:就是描述问题求解过程中每一步的状况。其实就是每一个阶段的各个参数的表示。我们可以把参与递归运算的变量写入状态中,可以更方便的回溯。
- 边界条件:即什么时候我们可以结束搜索。不同的搜索题目有不同的边界条件,比如小木棍POJ1011那道题目就是完整切下长度为len的木棍作为边界条件然后进一步判断剩余木棍的长度是否为0,而这道题目实际上就是处理订单的序号最大不超过n
- 约束条件和最优化解:即如何判断约束条件是否可以扩展子节点,以及扩展出的子状态是否满足题意要求。所谓剪枝,就是这样的思路,不过剪枝所涉及到的方面比较多;比如可以从最优性,可满足性这两个方面进行剪枝,所以还是得从题目条件出发得到这些剪枝。
说了这么多,我们来开始看一下POJ1040是什么鬼畜 题目吧。
题目大意是说给定一定数量的火车能搭载的乘客,乘客去往的终点站,以及订单信息。订单信息由出发的站点,结束的站点,以及人数,问在不超过这个容量下公司的收入最大化,每单收入的定义是经过的站点乘以人数。具体信息看题目。
这道题目能不能用动态规划来解决呢?其实是不能的,因为每个站点的人数不是固定的,还具有一定的流动性,所以我们不能用dp做(原因是我瞎说的 ,那么我们试着用一下回溯法来解决这一问题。
首先我们考虑这一个问题,我们状态该如何表示?这道题目可以用订单的标号以及当前获得的收入作为我们的状态,用(i,res)表示当前处理的订单是i,获得的收入是res。
其次考虑搜索的范围以及边界条件,其实这两个条件都不难确定,我们主要考虑的问题是这个,即约束条件以及最优化的条件。约束条件实际上就是说每一站火车都不能超过固定的人数,当超过时候就不能选择第i个订单,而且我们还需要通过最优性这一个角度去考虑剪枝,在这一道题目中,如果剩余的总订单收入不能大于best(当前状态下的最优解)那么肯定需要停止搜索。考虑最优化这一条件。显然当res>best,我们需要更新答案。
那么下面让我们看如何实践我们的想法。首先我们可以考虑如何设计订单这个类。通过以上分析可以知道,我们除了基本的一些信息,还需要将val(剩余的订单总收入)设计出来,见下面的结构体:
struct Ord{
int from,end,num;
int price,val;
void setPrice(){
price=(end-from)*num;
}
};
下面主要设计dfs这一函数:
我们设计dfs函数时,用train数组来表示有多少人在站点
int best=0;
void dfs(int i,int res){
if(i>num){
return;
}
if(res+ord[i].val<best)return;
int flag=1;
for(int k=ord[i].from;k<ord[i].end;k++){
train[k]+=ord[i].num;
if(train[k]>n)flag=0;
}
if(flag==1){
best=max(best,res+ord[i].price);
res+=ord[i].price;
dfs(i+1,res);
res-=ord[i].price;
}
for(int k=ord[i].from;k<ord[i].end;k++){
train[k]-=ord[i].num;
}
dfs(i+1,res);
}
然后是主函数的处理:
bool cmp(Ord a,Ord b){
return a.price<b.price;
}
int main(){
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
while(true){
scanf("%d%d%d",&n,&B,&num);
if(n==0&&B==0&&num==0)break;
int res=0;
memset(ord,0,sizeof(ord));
for(int i=1;i<=num;i++){
scanf("%d%d%d",&ord[i].from,&ord[i].end,&ord[i].num);
ord[i].setPrice();
}
sort(ord+1,ord+num+1,cmp);
for(int i=1;i<=num;i++){
for(int j=i;j<=num;j++){
ord[i].val+=ord[j].price;
}
}
best=0;
dfs(1,0);
cout<<best<<endl;
}
return 0;
}
其中处理时将订单这一个数组按照收入从小到大排序,然后再进行处理。
最后是所有的程序:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 100000
using namespace std;
int n,B,num;
struct Ord{
int from,end,num;
int price,val;
void setPrice(){
price=(end-from)*num;
}
};
Ord ord[maxn];
int train[maxn];
int best=0;
void dfs(int i,int res){
if(i>num){
return;
}
if(res+ord[i].val<best)return;
int flag=1;
for(int k=ord[i].from;k<ord[i].end;k++){
train[k]+=ord[i].num;
if(train[k]>n)flag=0;
}
if(flag==1){
best=max(best,res+ord[i].price);
res+=ord[i].price;
dfs(i+1,res);
res-=ord[i].price;
}
for(int k=ord[i].from;k<ord[i].end;k++){
train[k]-=ord[i].num;
}
dfs(i+1,res);
}
bool cmp(Ord a,Ord b){
return a.price<b.price;
}
int main(){
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
while(true){
scanf("%d%d%d",&n,&B,&num);
if(n==0&&B==0&&num==0)break;
int res=0;
memset(ord,0,sizeof(ord));
for(int i=1;i<=num;i++){
scanf("%d%d%d",&ord[i].from,&ord[i].end,&ord[i].num);
ord[i].setPrice();
}
sort(ord+1,ord+num+1,cmp);
for(int i=1;i<=num;i++){
for(int j=i;j<=num;j++){
ord[i].val+=ord[j].price;
}
}
best=0;
dfs(1,0);
cout<<best<<endl;
}
return 0;
}
P.S.是哪个大佬说条件是(n==0||num==0||B==0)
的,害的我调了好几个小时。佛了qwq