数据结构优化dp
单调队列优化\(DP\)
例题:滑动窗口
思路
在求\(\max\)时考虑到当一个数大于前面的数时,前面的数将无论如何也不会被使用了。当时间超过时,前面的数也应当不被记录。能支持从前和从后操作的数据结构:双端队列。因为队列中前面的数总是大于后面的数,所以被称为单调队列。
双端队列内存放下标\(i\)是最优的操作,通过下标即可判断是否从前出队,再通过\(x[i]\)判断是否需要从后出队。
\(Code\)
#include<bits/stdc++.h>
using namespace std;
deque<int> minn, maxn;
int n, m;
int x[1000005];
template<typename T>
inline void read(T&x){
x = 0; char q; bool f = 1;
while(!isdigit(q = getchar())) if(q == '-') f = 0;
while(isdigit(q)){
x = (x<<1) + (x<<3) + (q^48);
q = getchar();
}
x = f?x:-x;
}
template<typename T>
inline void write(T x){
if(x < 0){
putchar('-');
x = -x;
}
if(x > 9) write(x/10);
putchar(x%10+'0');
}
int main(){
read(n), read(m);
for(register int i = 1; i <= n; ++i) read(x[i]);
for(register int i = 1; i <= n; ++i){
while(!minn.empty() && minn.front()+m <= i) minn.pop_front();
while(!minn.empty() && x[minn.back()] >= x[i]) minn.pop_back();
minn.push_back(i);
if(i >= m){
write(x[minn.front()]);
putchar(' ');
}
}
putchar('\n');
for(register int i = 1; i <= n; ++i){
while(!maxn.empty() && maxn.front()+m <= i) maxn.pop_front();
while(!maxn.empty() && x[maxn.back()] <= x[i]) maxn.pop_back();
maxn.push_back(i);
if(i >= m){
write(x[maxn.front()]);
putchar(' ');
}
}
return 0;
}
例题:琪露诺
思路
维护到当前点的最大值,那么\(dp[i]=\max(dp[j]+a[i]),j\in[i-r,i-l]\),只需要维护一个\(i+l\)到\(i+r\)的最大值即可,很容易想到类似于滑动窗口。
第\(1\)到\(l-1\)的都是无法到达的,从第\(l\)开始入队,有后入队且比前面大的前面肯定就无法更新内容了可以弹出,前面的下标加\(l\)也无法到达\(i\)了也可以弹出。
\(ans\)应该赋初值为\(-0x3f3f3f3f\),因为值可能为负,调了一天。
\(Code\)
#include<bits/stdc++.h>
using namespace std;
deque<int> maxn;
int n, l, r;
int a[2000005];
int dp[2000005];
int ans = -0x3f3f3f3f;
template<typename T>
inline void read(T&x){
x = 0; char q; bool f = 1;
while(!isdigit(q = getchar())) if(q == '-') f = 0;
while(isdigit(q)){
x = (x<<1) + (x<<3) + (q^48);
q = getchar();
}
x = f?x:-x;
}
template<typename T>
inline void write(T x){
if(x < 0){
putchar('-');
x = -x;
}
if(x > 9) write(x/10);
putchar(x%10^48);
}
int main(){
read(n), read(l), read(r);
for(register int i = 0; i <= n; ++i) read(a[i]);
dp[0] = 0;
for(register int i = 1; i < l; ++i) dp[i] = -0x3f3f3f3f;
for(int i = l; i <= n; ++i){
while(!maxn.empty() && dp[maxn.back()] <= dp[i-l]) maxn.pop_back();
maxn.push_back(i-l);
while(maxn.front()+r < i) maxn.pop_front();
dp[i] = dp[maxn.front()]+a[i];
}
for(register int i = n-r+1; i <= n; ++i) ans = max(ans, dp[i]);
write(ans);
return 0;
}
例题:[NOIP2017 普及组] 跳房子
思路
如果不考虑他可以使用金币改进的话这就是一个单调队列优化\(dp\)的版题,用\(O(n)\)的时间复杂度求出能得到的最多金币数。然而为了满足要求,还需要改进能跳的距离,也就是改变\(l\)与\(r\),用最少的金币满足要求,毫无疑问就是二分答案了。
\(Code\)
#include<bits/stdc++.h>
using namespace std;
const long long INF = 0x8080808080808080;
long long dp[500005];
int n, d, k, l, r, ans;
long long sum;
int pos[500005], num[500005];
deque<int> q;
template<typename T>
inline void read(T&x){
x = 0; char q; bool f = 1;
while(!isdigit(q = getchar())) if(q == '-') f = 0;
while(isdigit(q)){
x = (x<<1) + (x<<3) + (q^48);
q = getchar();
}
x = f?x:-x;
}
template<typename T>
inline void write(T x){
if(x < 0){
putchar('-');
x = -x;
}
if(x > 9) write(x/10);
putchar(x%10^48);
}
long long search(int l, int r) {
memset(dp, 0x80, sizeof(dp));
dp[0] = 0;
q.clear();
int j = 0;
for(register int i = 1; i <= n; ++i){
while(pos[i]-pos[j] >= l && j < i){
if(dp[j] != INF){
while(!q.empty() && dp[q.back()] <= dp[j]) q.pop_back();
q.push_back(j);
}
j++;
}
while(!q.empty() && pos[i]-pos[q.front()] > r) q.pop_front();
if(!q.empty())
dp[i] = dp[q.front()]+num[i];
}
long long num = INF;
for(register int i = 1; i <= n; ++i)
if(dp[i] > num)
num = dp[i];
return num;
}
int main(){
read(n), read(d), read(k);
for(register int i = 1; i <= n; ++i){
read(pos[i]);
read(num[i]);
if(num[i] > 0) sum += num[i];
}
if(sum < k){
write(-1);
return 0;
}
r = max(pos[n], d);
while(l <= r){
int mid = (l+r) >> 1;
int dl = max(1, d-mid), dr = d+mid;
long long num = search(dl, dr);
if(num < k) l = mid+1;
else{
ans = mid;
r = mid-1;
}
}
write(ans);
return 0;
}
例题:CF321E Ciel and Gondolas
单调队列优化dp
题目思路
很容易就可以看出来这道题是 \(dp\),如果设 \(dp[i][j]\) 表示 \(i\) 个人使用 \(j\) 条船的最优解,\(sum[i][j]\) 表示二维前缀和,很容易就可以得出式子:
\(k\) 的取值范围是考虑优化不能让前面的船空了,也不能让新来的船没人。
决策是具有单调性的,因此想到用单调队列优化dp。
如果 \(dp[i][k]>dp[j][k]\) 且 \(i<j\) 则决策 \(j\) 比 \(i\) 优,可以确定决策 \(i\)不会被用到了就直接出队,外层枚举船,内层枚举人,可以容易证明决策是无后效性的。对于每个人二分查找最优解,时间复杂度 \(O(nk\log n)\),可以通过。
\(Code\)
#include<bits/stdc++.h>
using namespace std;
int n, dp[4005][4005], k, a[4005][4005], sum[4005][4005], head, tail; //dp[i][j]表示前i个人用j条船的最优解
struct node{
int num, l, r;
}t;
deque<node> q;
template<typename T>
inline void read(T&x){
x = 0; char q; bool f = 1;
while(!isdigit(q = getchar())) if(q == '-') f = 0;
while(isdigit(q)){
x = (x<<1) + (x<<3) + (q^48);
q = getchar();
}
x = f?x:-x;
}
template<typename T>
inline void write(T x){
if(x < 0){
putchar('-');
x = -x;
}
if(x > 9) write(x/10);
putchar(x%10^48);
}
int val(int k, int u, int v){
return dp[u][k-1]+(sum[u][u]+sum[v][v]-sum[u][v]-sum[v][u])/2;
}
int main(){
read(n), read(k);
for(register int i = 1; i <= n; ++i)
for(register int j = 1; j <= n; ++j)
read(a[i][j]);
for(register int i = 1; i <= n; ++i)
for(register int j = 1; j <= n; ++j)
sum[i][j] = sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j]; //预处理前缀和
memset(dp, 0x3f, sizeof(dp));
for(register int i = 1; i <= n; ++i) dp[i][1] = sum[i][i]/2; //dp[i][1]表示前i个全在1条船上的情况
for(register int j = 2; j <= k; ++j){ //枚举每一条船
q.clear(); //单调队列初始化
t.num = 0, t.l = 1, t.r = n; //l,r记录人在船的区间,num记录最优人数
q.push_back(t);
for(register int i = 1; i <= n; ++i){ //枚举人数
while(q.size() > 1 && q.front().r < i) q.pop_front(); //当还存在不止一个解且最优人数区间小于需求人数i时直接出队
q.front().l = i; //最小人数为i
dp[i][j] = val(j, q.front().num, i); //k条船i个人最优解等于k-1条船预处理最优人数+剩余人数在一条船上的值
while(q.size() > 1 && val(j, i, q.back().l) <= val(j, q.back().num, q.back().l)) q.pop_back();
int l = q.back().l, r = q.back().r;
while(l < r){ //二分查找最优长度
int mid = (l+r) >> 1;
if(val(j, q.back().num, mid) >= val(j, i, mid)) r = mid; //队尾的值长度与二分长度判断
else l = mid+1;
}
if(val(j, q.back().num, q.back().r) <= val(j, i, q.back().r)) l++;
if(l != q.back().l) q.back().r = l-1; //若边界不在队中,则修改队尾
else q.pop_back(); //否则将队尾弹出
if(l <= n){ //将第p条船的信息加入队中
t.num = i, t.l = l, t.r = n;
q.push_back(t);
}
}
}
write(dp[n][k]);
return 0;
}