题解 P1725 【琪露诺】

· · 题解

Update 6.8

原来的做法已更新,并且更新了单调队列做法。

最后两组数据会WA,主要是因为没有判断DP状态的转移是否合法,如果该点是不能到达的,那么就不能用它更新后面的状态。

用一个数组use[i]标记一下f[i]的合法性就可以了。

这是一道DP优化入门的好题。

读懂题意后,我们很容易想到这道题的状态以及转移。

f[i]表示跳到第i个格子的最优值。

初始状态:f[0]=0,表示在原点的价值为0.

状态转移:f[i]=max{f[j]+a[i]} (j\in[i-r,i-l]).

最终状态:max(f[n+1]....f[n+l]).(因为琪露诺最终跳到的区间范围是[n+1,n+l]).

于是我们就能拿到暴力的部分分。

考虑优化,我们发现,f[i]的取值只能由决策区间[i-r,i-l]转移而来,而a[i]是定值,所以我们只用考虑f[i-r].... f[i-l]的最优决策.

由DP的转移方程告诉我们,我们需要求的是决策的最大值。

那么我们自然地可以想到,用一种数据结构去维护决策的最优。

结合题目的情况,即:

$[i-l+1,i-r+1]$的决策影响状态$f[i+1]$的最大值。 每个$f[i]$相对应的是一个决策区间,而区间移动的长度是固定的,这不禁让人想到滑动窗口这道题,我们可以通过动态维护区间的最大值,从而节省一维枚举决策的时间。 维护区间最值可以用优先队列或者单调队列。 上面两种方式都能过,我们可以比较一下两种做法。 首先说优先队列的做法,对于堆里的元素我们需要用一个$vis[i]$数组来表示是否在队列里,然后看看堆里维护的区间是否超出了决策区间,删除堆顶元素直到符合条件为止。 每次移动的操作时间复杂度为$O(logn)$,所以总时间复杂度为$O(nlogn)$. 以下为代码: ```cpp #include<bits/stdc++.h> #define mp make_pair #define ll long long #define rep(i,a,b) for(int i=a;i<=b;i++) using namespace std; const int N=1e6+50; int n,l,r,a[N],f[N],vis[N],ans=-1e9,use[N];//use[i]标记f[i]是否合法 priority_queue< pair<int,int> > q; int main(){ scanf("%d%d%d",&n,&l,&r); rep(i,1,2*n) f[i]=-1e9; rep(i,0,n) scanf("%d",&a[i]); rep(i,l,r) use[i]=1; rep(i,0,n){ if(i<l) {q.push(mp(-1e9,0));f[i+l]=a[i+l];continue;} int y=q.top().second; if(i-r+l-1>=0) vis[i-r+l-1]=1; while(vis[y]||!use[y]) { q.pop(); if(q.empty()) break; y=q.top().second;} //删除堆顶不合法的决策 q.push(mp(f[i],i)); f[i+l]=q.top().first+a[i+l]; if(use[i]) use[i+l]=1; } rep(i,n+1,n+l) if(use[i]) ans=max(ans,f[i]); printf("%d\n",ans); return 0; } ``` 类似的,我们也可以只维护一个单调递减的队列,每次决策的时候取出队头即为最优决策,省去了之前入堆的$log$(STL自带的$log$)。 由于每个状态只进出队一次,故总时间复杂度为$O(n)$. 以下为代码: ```cpp #include<bits/stdc++.h> #define mp make_pair #define ll long long #define rep(i,a,b) for(int i=a;i<=b;i++) using namespace std; const int N=1e6+50; int n,l,r,a[N],f[N],vis[N],ans=-1e9,use[N],q[N],L,R; int main(){ scanf("%d%d%d",&n,&l,&r); rep(i,1,2*n) f[i]=-1e9; rep(i,0,n) scanf("%d",&a[i]); rep(i,l,r) use[i]=1; rep(i,0,n){ while(L<=R&&f[i]>f[q[R]]) R--;//维护的是单调递减的队列 q[++R]=i; while(L<=R&&q[L]<i-r+l) L++;//合法的区间转移范围 f[i+l]=f[q[L]]+a[i+l]; if(use[i]) use[i+l]=1; } rep(i,n+1,n+l) if(use[i]) ans=max(ans,f[i]); printf("%d\n",ans); return 0; } ``` 可以发现,对于区间移动的最值,单调队列明显比优先队列好写,并且好维护,时间复杂度少一个$log$。 所以对于区间移动的最值问题,单调队列是一个非常好的解法。 (~~比STL的优先队列维护不知高到哪里去了~~)