P5194
一 题目理解 :
题目地址:
[P5194 USACO05DEC] Scales S - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目可以简单描述为,我们有n ( n ≤ 1000 ) (n\leq1000) (n≤1000)个砝码,我们可以选择其中任意x( 0 ≤ x ≤ n 0\leq x\leq n 0≤x≤n)个砝码,让这些砝码的总和在不超过一个上界限c ( c ≤ 2 30 ) (c\leq2^{30}) (c≤230)的情况下,找出一个最大值。然后,题目给出了一个条件,也就是从第三个砝码开始,每个砝码至少为前两个砝码的和。
这个题目,第一眼,就是一个简单的0-1背包,但是,我们看看背包重量要到 2 30 2^{30} 230,就算我们使用一维的0-1背包,空间复杂度要到1e9的级别,如果说空间复杂度还可以接受,那么时间复杂度 O ( n ∗ m ) O(n*m) O(n∗m)大约要到 1 e 12 1e^{12} 1e12,很明显会TLE。
但是题目,给出了一个重要的条件,也就是从第三个砝码开始,每个砝码至少为前两个砝码的和,也就是斐波那契数列,这样的话,在大约40项左右(如果存在第40项的话),我们的砝码值应该就要大于我们的c了,那么其实这个题我们用到的砝码最多是40个左右,考虑dfs。
二 解题方法
对于这个题目,我们可以使用三个剪枝策略
1.倒序dfs
因为,只要我们所选的砝码的质量大于我们的c,我们就要return,所以我们可以从大数往小数遍历,这样可以大大减少搜索的数目。
对于这个问题,我们可以这么理解,我们都知道,dfs其实就是一棵搜索树,以这个题目为例,其实就是一颗二叉树,对于这棵二叉树,某一个节点可能有两个孩子节点,要么选则第x( 0 ≤ x ≤ n 0\leq x\leq n 0≤x≤n)个砝码,要么不选,当某个节点不满足条件(总质量大于c或者已经搜索完毕),那么,他就没有孩子,而一颗二叉树的深度越大,时间复杂度和空间复杂度越高,所以,我们剪枝要做的就是让二叉树的深度尽可能低,也就是一些不必要搜索的节点我们就不去搜索了。
对于这个题目,强行解释,好吧放弃强行解释,我们可以自己造一组数据看一下,比如,对于以下数据:
4 15
1 10 20 30
我们可以画出他们的搜索树的图辅助我们去理解。 可能画的有些抽象QAQ,但是主打一个理解
正序树如图1所示:
图一 正序列树图解
反序列树如图2所示:
图二 反序树图解
2.缩小n
如前文所说,我们的n不可能到1000,那么我们就可以让我们递归的起点减少,我们可以将递归的起点变成第一个比c小的数字,这样也会减少一些递归的时间,但是,我个人感觉,这样剪枝产生的效果只是锦上添花,不会让原本TLE的不TLE,因为,就算不使用这一步,在倒序dfs中,那些比c大的砝码递归也是直接进入不选这个砝码的叶子节点,时间开销花在了递归上。但是,小技巧加上比较好。
3.利用前缀和
我们还可以利用前缀和进行剪枝,如果到了某个节点,他前面所有砝码都选上也不会超过c,那么我们就不需要继续往下搜索了,这样也可以减小一定的复杂度。
总结一下,这个题目的一个解题方法为dfs+剪枝技巧,但是我感觉对于剪枝帮助最大的就是1,而且我们以后做题也很可能会用到。
综上,该题目的代码如下:
#P5194代码
#include<iostream>
using namespace std;
const int N=1e3+100;
typedef long long LL;
// 斐波那契额数列到 30左右就会超过int
LL f[N];
// LL dp[N][N];
LL n,c;
LL ans;
int flag=0;
LL pret[N];
#前缀和初始化
void init()
{
for(int i=1;i<=n;i++){
pret[i]=pret[i-1]+f[i];
}
}
#求解某一段的前缀和
LL check(int l,int r)
{
return pret[r]-pret[l-1];
}
void dfs(LL x,LL count){
if(x==0||count>c)
{
return ;
}
else
{
ans=max(ans,count);
if(count+check(1,x-1)<=c)
{
ans=max(ans,count+check(1,x-1));
// cout<<"lllllll"<<" "<<"cccc"<<x<<check(1,x-1)<<endl;
return ;
}
dfs(x-1,f[x-1]+count);
dfs(x-1,count);
}
}
int main()
{
cin>>n>>c;
int temp=n;
for(int i=1;i<=n;i++)
{
cin>>f[i];
if(f[i]>c)
{
temp=i-1;
}
}
n=temp;
init();
for(int i=n;i>=1;i--){
if(f[i]<c)
{
dfs(i+1,0);
break;
}
else if(f[i]==c)
{
cout<<c<<endl;
return 0;
}
}
cout<<ans<<endl;
return 0;
}
三总结
- 首先,学会了一种剪枝方式,反向dfs 当在一个题目当中,dfs的结束条件有一个上界的时候,我们可以从大往小dfs,这样可以大大减少搜索树的复杂度。
- 第二,就是,注意对题目当中的条件敏感,比如这个题目,从第三个砝码开始,每个砝码至少为前两个砝码的和,斐波那契数列,我一开始就没有分析出来,我还在想1000 dfs不得搜到明年,综上,还是刷题太少了,算法学习不刷题纯小丑哈哈。