DP作为一种结果一定正确的算法,在推出了正确方程的情况下,最难解决的就是效率问题。
先来看一道题:洛谷P1725琪露诺
对于这道题,我们可以很轻松的得出下面这段代码:
for(int i = L; i <= n; i++)
for(int j = max(0, i - R); j <= i - L; j++)
f[i] = max(f[j] + a[i], f[i]);
这个方程很好推,但是效率为O(n * (R - L - 1)),看看数据范围,很明显是不够的(实测60分)
那么我们该怎么优化呢?
很明显a[i]是不变的,所以我们的方程等价于f[i] = max(f[j]) + a[i] (i - R <= j <= i - L)。
到了这一步可能有人就会说:这不是线段树优化DP吗?
其实也可以,就是效率多一个log,在这里献上我的同学神犇NSH的线段树AC码:
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
int L,R,a[200001],n;
int dp[200001];
int nxt[200001];
struct DP{
int val,loc;
};
DP Max(DP x,DP y)
{
return (x.val>y.val)?x:y;
}
struct note{
int l,r;
DP va;
}rt[2000001];
int num=0;
void build(int x,int L,int R)
{
if(L==R)
{
rt[x].va.val=-2100000000;
return;
}
int mid=(L+R)/2;
rt[x].l=++num;
build(num,L,mid);
rt[x].r=++num;
build(num,mid+1,R);
rt[x].va=Max(rt[rt[x].r].va,rt[rt[x].l].va);
}
void Insert(int x,int L,int R,int aim,int v,int lo)
{
if(L==R)
{
rt[x].va.val=v;
rt[x].va.loc=lo;
return;
}
int mid=(L+R)/2;
if(aim<=mid)
{
Insert(rt[x].l,L,mid,aim,v,lo);
}
else
{
Insert(rt[x].r,mid+1,R,aim,v,lo);
}
rt[x].va=Max(rt[rt[x].l].va,rt[rt[x].r].va);
}
DP Query(int x,int L,int R,int al,int ar)
{
if(R<al||ar<L)
{
DP tmp;tmp.val=-2100000000;
return tmp;
}
if(al<=L&&R<=ar)
{
return rt[x].va;
}
int mid=(L+R)/2;
return Max(Query(rt[x].l,L,mid,al,ar),Query(rt[x].r,mid+1,R,al,ar));
}
int main()
{
scanf("%d%d%d",&n,&L,&R);
for(int i=0;i<=n;i++)
{
scanf("%d",a+i);
}
memset(dp,-127,sizeof(dp));
num++;
build(1,0,n);
dp[n]=a[n];
Insert(1,0,n,n,dp[n],n);
for(int i=n-1;i>=0;i--)
{
DP temp;
if(i+R>n)
{
if(dp[i]<a[i])
{
dp[i]=a[i];
}
if(i+L<=n)
{
temp=Query(1,0,n,i+L,n);
if(dp[temp.loc]+a[i]>dp[i])
{
dp[i]=dp[temp.loc]+a[i];
nxt[i]=temp.loc;
}
}
}
if(i+R<=n)
{
temp=Query(1,0,n,i+L,i+R);
if(dp[temp.loc]+a[i]>dp[i])
{
dp[i]=dp[temp.loc]+a[i];
nxt[i]=temp.loc;
}
}
Insert(1,0,n,i,dp[i],i);
//cout<<dp[i]<<endl;
}
printf("%d\n",dp[0]);
return 0;
}
但是这份码在洛谷上跑了544ms,有没有什么更快的方法呢?
单调队列!
很明显这道题的维护目标是单调的,而且是单变量的(j)
那么考虑维护一个队列,其中队列头部的元素是最大的,我们需要的最优解也就是队头元素了
但是这样有几个问题:如何维护单调性?如何维护合法性?
考虑单调性,我们的初始代码为什么慢?因为有很多没有必要的比较,如果a>b,在之后的过程中我们只需要让新来的c和a比较,因为我们只取较大的那个,但是你的程序会让c和b再比一次,这就是无意义的比较。而我们可以将一个新的元素插在队尾,在插入之前先把队尾比它小的元素删除,那么整个队列就维持了单调性,免除了无意义的比较。
再考虑合法性,在这个问题中,所谓的合法性就是指的可供转移的状态必须在i - R ~ i - L之间,我们注意到队列中的元素最初都是从队尾插入的,所以这个队列不仅满足所维护的值单调,也满足时间单调性,所以队头元素不仅是最优的,也是最老的(雾),那么在更新之前,我们检查一下队头元素是否有“过期”,是就删除,否则这就是你要的最大值。
献上本人丑陋的代码(40ms):
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cstdlib>
#include <iostream>
using namespace std;
const int MAXN = 300040;
typedef int Array[MAXN];
Array a, f, p, p2;
int n, L, R, ans, pos, cnt;
inline int read()
{
int ch; int x = 0; int f = 1; ch = getchar();
while(ch != '-' && (ch < '0' || ch > '9')) ch = getchar();
ch == '-' ? f = -1 : x = ch - '0', ch = getchar();
while(ch >= '0' && ch <= '9')
{x = x * 10 + ch - '0'; ch = getchar();}
return x * f;
}
namespace DP {
Array Q;int h, t;
void init() {
h = 0; t = 1;
memset(Q, 0, sizeof(Q));
}
void dp() {
for(int i = L; i <= n; i++) {
while(h <= t && f[Q[t]] <= f[i - L]) t--;//删除比它小的
Q[++t] = i - L;//插入(顺带维护了j <= i - L)
while(h <= t && Q[h] < i - R) h++;//销毁过期产品
f[i] = f[Q[h]] + a[i];//更新
}
for(int i = n - R + 1; i <= n; i++)
if(ans < f[i])
ans = f[i], pos = i;
printf("%d\n", ans);
}
}
signed main() {
n = read(), L = read(), R = read();
for(int i = 0; i <= n; i++) a[i] = read();
for(int i = 0; i < L; i++) f[i] = 0;
DP::init();
DP::dp();
return 0;
}
再来一题:洛谷P2627修剪草坪,或bzoj2442(同一题)
这道题同样有一个比较好推的方程,不能出现连续的k个,也就等同于每k个都必须有一个断点,所以一个枚举断点的DP方程就和明显了,不取断点j,从j-1继承,然后加上a[j + 1]到a[i]:f[i] = max(f[i], f[j] + sum[i] - sum[j]);其中sum数组为前缀和数组
先献上一份70分(LG)的代码:
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <iostream>
#define LL long long
using namespace std;
int n, k;
const int MAXN = 100100;
inline int read()
{
int ch; int x = 0; int f = 1; ch = getchar();
while(ch != '-' && (ch < '0' || ch > '9')) ch = getchar();
ch == '-' ? f = -1 : x = ch - '0', ch = getchar();
while(ch >= '0' && ch <= '9')
{x = x * 10 + ch - '0'; ch = getchar();}
return x * f;
}
typedef long long Array[MAXN];
Array f, sum;
signed main() {
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; i++) sum[i] = sum[i - 1] + (LL) read();
for(int i = 1; i <= n; i++)
for(int j = max(i - k, 0); j <= i; j++)
f[i] = max(f[i], f[j - 1] - sum[j] + sum[i]);
printf("%lld\n", f[n]);
return 0;
}
然后同样的,sum[i]是不变的,所以可以将sum[i]提出来,那么我们需要维护的就是f[j - 1] - sum[j],单调队列的维护同上题:
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cstdlib>
#include <cmath>
#define LL long long
using namespace std;
int n, k;
const int MAXN = 100100;
inline int read()
{
int ch; int x = 0; int f = 1; ch = getchar();
while(ch != '-' && (ch < '0' || ch > '9')) ch = getchar();
ch == '-' ? f = -1 : x = ch - '0', ch = getchar();
while(ch >= '0' && ch <= '9')
{x = x * 10 + ch - '0'; ch = getchar();}
return x * f;
}
typedef long long Array[MAXN];
Array f, sum;
namespace DP {
Array Q; int h, t;
void init() {
h = 0; t = 1;
}
bool check(int x, int y) {
return f[x - 1] - sum[x] < f[y - 1] - sum[y];
}
void dp() {
for(int i = 1; i <= n; i++) {
while(h <= t && Q[h] < i - k) h++;
int j = (int) Q[h];
f[i] = f[j - 1] - sum[j] + sum[i];
while(h <= t && check((int)Q[t], i)) t--;
Q[++t] = (LL)i;
}
printf("%lld\n", f[n]);
}
}
signed main() {
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; i++) sum[i] = sum[i - 1] + (LL) read();
DP::init();
DP::dp();
return 0;
}
单调队列同样可以用在斜率优化上
留坑,Thanks for your watching