单调队列优化dp
如果 dp 方程需要你从一个区间的 dp 数组的值中选择某些最大值,那么太好了,dp 方程设定的这个区间相当于滑动窗口,需要最大最小值都可以用单调队列优化。
P2627 [USACO11OPEN]Mowing the Lawn G
不能选超过 \(k\) 个奶牛,那么状态中必定要有关于是否被选的一维。一开始我想的是 \(dp_{i,j}\) 表示目前选到第 \(i\) 个,上一个被选的奶牛是第 \(j\) 个,但很显然,这样的做法空间无法被接受。
在我思考了一段时间后,我想到了可以考虑当前这个位置是否被选,这样也可以转移:\(dp_{i,0}\) 表示目前选到第 \(i\) 个,且第 \(i\) 个没有被选。\(dp_{i,1}\) 表示目前选到第 \(i\) 个,且第 \(i\) 个被选了。
容易发现,如果你不选第 \(i\) 个,那么你这一位对答案没有任何贡献,使用上一位的答案转移就行了,也就是:
如果你选了第 \(j\) 个,你就可以从之前的选或者不选来转移了。
如果你从之前的不选的位置转移。
按照常理来说,应该还需要做一个 \(dp_{i,1} = \max(dp_{j,1})\) ,因为如果你枚举被选过的位置,这一位被选的个数不同,所以不能取区间和了。
但是仔细思考后会发现,这个方程就相当于上面那个方程所做的事情。只是你从中间选了一个断点,甚至还浪费了后面位置的贡献。所以该方程没有作用,舍去。
整理出目前的两条方程,发现按照常理只能做到 \(O(n^2)\) 。但是,由于这是一道单调队列优化 dp 的题,我们需要用单调队列优化它。
按照目前的方程形式,我们无法将 \(dp_{j,0}\) 或者是 \(dp_{j,0} + \text{sum}(j + 1,i)\) 推入单调队列,因为无法消除 \(\text{sum}(j + 1,i)\) 对我们答案的影响,甚至还有可能影响单调队列的单调性。
在这个节点,我卡了很久,最终使用了一种大佬介绍的方式解决。
考虑将 \(\text{sum}(j + 1,i)\) 拆成前缀和形式,那么便是 \(pre_i - pre_j\) 。这个时候,只有 \(pre_i\) 随着位置的变化而变化,那么我们维护 \(dp_{j,0} - pre_j\) ,在计算到每一位的时候,取队列中最大的值,加上 \(pre_i\) 就好了!
/*
深山踏红叶,耳畔闻鹿鸣
飘摇风雨中,睹物思故乡
可叹,落叶飘零
*/
#include<bits/stdc++.h>
#define int long long
#define mem(x,y) memset(x,y,sizeof(x))
#define si set <node> :: iterator
using namespace std;
inline int read(){
int s = 0,w = 1;
char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-')w = -1;ch = getchar();}
while(ch >= '0' && ch <= '9')s = s * 10 + ch - '0',ch = getchar();
return s * w;
}
struct node{
int w;
int id;
};
int n,m;
int a[1000010];
int dp[1000010][2];
int pre[1000010];
deque <node> q;
signed main(){
cin>>n>>m;
for(int i = 1;i <= n;i ++)a[i] = read();
for(int i = 1;i <= n;i ++)pre[i] = pre[i - 1] + a[i];
//dp[1][1] = a[1];
q.push_back({0,0});
for(int i = 1;i <= n;i ++){
dp[i][0] = max(dp[i - 1][0],dp[i - 1][1]);
int v;
v = dp[i][0] - pre[i];
while(!q.empty() && q.back().w < v)q.pop_back();
q.push_back({v,i});
while(!q.empty() && i - q.front().id > m)q.pop_front();
dp[i][1] = q.front().w + pre[i];
}
cout<<max(dp[n][0],dp[n][1]);
}
P3957 [NOIP2017 普及组] 跳房子
考虑二分答案。
对于选出来的 \(mid\) ,用一次 \(dp\) 判断它是否可行。
具体一点:
设 \(dp_i\) 为到达第 \(i\) 个格子能够获得的最大金币。
可得:
由于不是每个位置都有格子,而且位置的值域非常大,无法将复杂度依赖于此(\(n\log^2n\) 的复杂度甚至无法通过),所以只能使用两个指针,指向可以转移的最左位置和最右位置,相当于手动滑动窗口。对于滑动出来的窗口使用单调队列处理,复杂度为 \(O(n)\) 级别的。
每一次到下一个格子,都手动把两个指针右移。两个指针分别最多右移 \(n\) 次,复杂度为 \(O(n)\) ,二分复杂度为 \(O(\log \max a_i)\),所以最终复杂度为 \(O(n\log\max a_i)\) ,足以通过此题。
P1725 琪露诺
这道题太菜了,没资格让我写题解。
/*
深山踏红叶,耳畔闻鹿鸣
飘摇风雨中,睹物思故乡
可叹,落叶飘零
*/
#include<bits/stdc++.h>
#define int long long
#define mem(x,y) memset(x,y,sizeof(x))
#define si set <node> :: iterator
using namespace std;
int read(){
int s = 0,w = 1;
char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-')w = -1;ch = getchar();}
while(ch >= '0' && ch <= '9')s = s * 10 + ch - '0',ch = getchar();
return s * w;
}
struct node{
int w;
int id;
};
int n,l,r;
int a[1000010];
int dp[1000010];
int ans = -1e18;
int pre[1000010];
int pos;
deque <node> q;
vector <int> p;
signed main(){
mem(dp,-0x3f);
cin>>n>>l>>r;
for(int i = 1;i <= n + 1;i ++)a[i - 1] = read();
dp[0] = a[0];
for(int i = l;i <= n + r + 1;i ++){
int v = dp[i - l];
while(!q.empty() && q.back().w <= v)q.pop_back();
q.push_back({v,i - l});
while(!q.empty() && i - q.front().id > r)q.pop_front();
dp[i] = q.front().w + a[i];
pre[i] = q.front().id;
}
for(int i = n + 1;i <= n + r + 1;i ++){
if(dp[i] > ans){
ans = dp[i];
pos = i;
}
}
cout<<ans<<endl;
while(1){
pos = pre[pos];
p.push_back(pos);
if(pos == 0)break;
}
for(int i = p.size() - 1;i >= 0;i --)printf("%lld ",p[i]);
puts("-1");
}
P3572 [POI2014]PTA-Little Bird
这个题第一眼看到时,由于发现有高低限制,所以不能直接用单调队列维护 dp 。
但是思考了一下,转移的位置只有两种:
-
从该位置走到这里,不需要增加疲劳值,且这个位置是在可以走过来的区间内 dp 值最小,或者比最小值仅大 1 的。
-
从该位置走到这里,需要增加疲劳值,且这个位置是在可以走过来的区间内 dp 值最小的。
那么,我们从单调队列左边开始往右遍历,由于单调队列满足单调性,所以到达第一个比最小值大 2 的位置就可以停下了。记录遍历到的地方的最小值,这个最小值要算上是否要增加 1 疲劳值,也就是说我们优先选择高度较高的位置。
但是,这个做法过不了。
可以看到,经过我的优化,速度确实加快了,但没什么实际作用。
然后,我思考了一下,不一定要比我即将入队的这个值大才可以出队,如果和我这个值一样大,而且这个位置的高度还比我低,也可以出队。
把这个东西加上去,就过了。
然后觉得好牛逼!我乱搞AC了!!!
思考了一下,发现好像如果这样出队的话,我根本不需要遍历了。
把遍历去掉,AC了!
一点开提交记录,我草,怎么是最优解,很震撼。
点开题解,我草,怎么我的做法和题解一样。
/*
深山踏红叶,耳畔闻鹿鸣
飘摇风雨中,睹物思故乡
可叹,落叶飘零
*/
#include<bits/stdc++.h>
#define mem(x,y) memset(x,y,sizeof(x))
#define si set <node> :: iterator
using namespace std;
int read(){
int s = 0,w = 1;
char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-')w = -1;ch = getchar();}
while(ch >= '0' && ch <= '9')s = s * 10 + ch - '0',ch = getchar();
return s * w;
}
struct node{
int w;
int id;
}q[10000010];
int n,Q,m;
int a[10000010];
int dp[10000010];
int l,r;
int ans[10000010];
signed main(){
cin>>n;
for(int i = 1;i <= n;i ++)a[i] = read();
cin>>Q;
while(Q --){
m = read();
if(ans[m]){
printf("%d\n",ans[m]);
continue;
}
l = 1,r = 0;
dp[1] = 0;
for(int i = 2;i <= n;i ++){
int v = dp[i - 1];
while(l <= r && q[r].w > v)r --;
while(l <= r && q[r].w == v && a[q[r].id] <= a[i - 1])r --;
q[++ r] = {v,i - 1};
while(l <= r && i - q[l].id > m)l ++;
int minn = q[l].w;
int pos = a[q[l].id];
if(l <= r){
for(int j = l;j <= r;j ++){
if(q[j].w != minn)break;
if(a[q[j].id] > pos){
pos = a[q[j].id];
l = j;
}
}
dp[i] = minn;
if(pos <= a[i])dp[i] ++;
}
}
ans[m] = dp[n];
printf("%d\n",dp[n]);
}
}