前言
题目
初步分析
正解
结尾
此题是用单调队列来优化DP的实例,十分适合初学单调队列的人做,并且类似于单调队列优化多重背包,对今后进一步学习DP有很大的作用。
(如果你不知道什么是单调队列,看看这篇博客(矩形面积)
LGOJ P3572 Little Bird
问题 A(3768): 猴子
时间限制: 2 Sec 内存限制: 128 MB
题目描述
有Q只猴子要从第一棵树到第n棵树去,第i只猴子一次跳跃的最远距离为Ki。如果它在第x棵树,那它最远可以跳到第x+Ki棵树。如果第j棵树的高度比第i棵树高或相等,那么它从第i棵树直接跳到第j棵树,它的劳累值会增加1。所有猴子一开始在第一棵树,请问每只猴子要跳到第n棵树花费的劳累值最小。
输入
第一行一个整数n,表示有n棵树。(2<=n<=1000000)
接下来第二行给出n个正整数D1,D2,……,Dn(1<=Di<=10^9),其中Di表示第i棵树的高度。
第三行给出了一个整数Q(1<=Q<=25),接下来Q行,给出了每只猴子一次跳跃的最远距离Ki(1<=Ki<=N-1)。
输出
输出Q行,每行一个整数,表示一只猴子的最小的劳累值。
样例输入
4 6 3 6 3 7 2 6 5 2 2 5
样例输出
2 1
此题,懂点DP的人第一眼看到便知道是一个DP,很简单的,用f[i]表示猴子爬到第i棵树的最小花费,h[i]表示第i棵树的高度,k[i]
表示第i个猴子最多爬多远的距离,那么我们就可以得到第一个状态转移方程:
枚举一个j表示可以从第j棵树爬到第i棵树,表示如果第i棵树的高度如果大于等于第j棵树的高度,那么就会返回1,否则返回0,最后取最小值,便是最优状态,代码如下
//此代码是我考场上打的代码,尽管不是最优解,但也骗了许多分,虽然与上面的转移方程有出入,但主体思想大体是一样的
//这个DP代码理解起来应该十分容易,所以我就不打注释了
#include
#include
#include
#include
using namespace std;
int read() {
int f=1,s=0;char a=getchar();
while(!(a>='0'&&a<='9')) { if(a=='-') f=-1; a=getchar(); }
while(a>='0'&&a<='9') {s=s*10+a-'0';a=getchar();}
return f*s;
}
#define N 1000010
#define M 30
#define LL long long
struct node {
LL h,num;
};
LL h[N],n,k,s[M],dp[N],inf;
deque < LL > q;
int main()
{
//freopen("monkey.in","r",stdin);
//freopen("monkey.out","w",stdout);
memset(dp,0x3f,sizeof(dp));
inf=dp[1];
dp[1]=0;
n=read();
for(int i=1;i<=n;++i)
h[i]=read();
k=read();
for(int i=1;i<=k;++i)
s[i]=read();
for(int i=1;i<=k;++i) {
for(int j=1;j<=n;++j) {
for(int p=1;p<=s[i];++p) {
if(h[j]>h[j+p])
dp[j+p]=min(dp[j+p],dp[j]);
else
dp[j+p]=min(dp[j+p],dp[j]+1);
}
}
cout<
那么这道题就解决了
如果你觉得这道题就这么简单的解决,那就太天真了,此题的数据N是10^6,这个代码的复杂度是O(N^2Q),明显不够。
那么,我们能否在DP的基础上有一定的优化呢?答案是肯定的,我们观察,发现枚举p这个阶段是有重复性的,也就是进行了多余的运算,这也是我们优化的切入口(如果你无法理解这句话,请看下面这张图)。
红色部分表示在这两次我们重复计算的部分,我们要求的是f[i]到f[i+ki]这个部分的f[j]的最小值,那么他就肯定在这个红色的重叠部分中,那我们为什么还要再计算一次呢?只需要一个单调队列就可以用O(1)的时间把这个min给求出来,并把这个单调队列维护到f[i-1]到f[i-ki]这个区间内,最后把这个最小值存储到f这个数组内,就完美的把时间复杂度缩短到O(NQ)
此外,还有一些细枝末节的事需要我们考虑:如果在维护时,两个f值是相等的,那么应该选择哪一个?我们可以贪心式地想,选择高度更大地那个,因为这样可以保证结果最优(如果不懂可以在这里好好想想)
有了矩形面积这道题的铺垫,我们就可以熟练地打出以下的代码:
#pragma GCC optimize(2)
#include
#include
#include
#include
using namespace std;
#define inf 0x7f7f7f7f
#define N 1000010
int read() {
int f=1,s=0;char a=getchar();
while(!(a>='0'&&a<='9')) {if(a=='-') f=-1 ; a=getchar(); }
while(a>='0'&&a<='9') {s=s*10+a-'0'; a=getchar();}
return f*s;
}
int n,m,a[N],f[N],dis[N];
deque q;//q的队尾存储的是最优解f数组的下标
int main()
{
n=read();
for(int i=1;i<=n;i++)
a[i]=read();
m=read();
for(int i=1;i<=m;i++)//dis用来存储这m个询问
dis[i]=read();
for(int k=1;k<=m;k++) {
q.clear();
memset(f,0,sizeof(f));
f[1]=0;
q.push_back(1);//初始化,不用赘述
for(int i=2;i<=n;i++) {
while(!q.empty() && q.front()+dis[k] f[i] || ( f[q.back()] == f[i] && a[i] >= a[q.back()] ) ) )//维护这个单调队列,原因我已经解释过了,这里不再赘述
q.pop_back();
q.push_back(i);
}
cout<
这是我一次接触到单调队列对DP的优化,感觉受益颇多呢。
我的QQ:2716776569欢迎跟我一起交流算法