单调队列优化DP

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



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值