【C++】單調隊列優化動態規劃

引入

可用單調隊列優化的動規有一大類題型,它們多半都有一個特徵:
可以化歸爲序列中定長區間的最值問題。注意這裏必須是定長區間,否則應用RMQ算法。
下面舉一個例子:

  • 輸入:第一行兩個正整數N(N<=600000),M。接下來一行N個數。

  • 輸出:對於每個區間[i,i-M+1],輸出其中的最小值

  • 思路:很顯然這道題數據大到不允許利用RMQ的各種O(NlogN)的算法,想到每一次找最小值都只是將上一個區間後移一個數,即這個區間的答案很有可能可從上個區間獲得,於是保持一個單調隊列,區間每後移一次,就將num[i]插入隊尾,若隊尾的數Q[k]大於等於當前的數(num[i]),就說明Q[k]在以後就不可能是一個可行解,刪去它,這樣循環操作,直至Q[k]<num[i]爲止。
    對於每一個區間的最小值,只需輸出當前隊最前面的且在num中的下標號大於i-M+1的值即可。這樣複雜度就從O(NlogN)飛躍到了O(N)。
    此外,有很多問題可以化歸爲此問題進行解決,如求序列中長度不大於定值的最大連續子序列和等問題,並且此方法可以套用於滿足上述性質的非動歸題目,應用範圍極廣。

例題1 Diving the Path

POJ-2373
鏈接:http://poj.org/problem?id=2373
https://vjudge.net/problem/POJ-2373

在這裏插入圖片描述

題目大意

有一個長度爲L(1000000)的區間,L是偶數,噴水的機器半徑是a到b,
限制:1區間不能重複覆蓋,2部分區域一定只能被一個機器覆蓋。
求最少需要多少機器。

思路

f[i]表示長度爲i的最少需要幾個設備。
f[i]=1+min(f[i-2a]…f[i-2b]);
把f[i-2a]到f[i-2b]看成一個滑動窗口,維護一個從小到大的隊列,左邊可以出去,右邊也可以出去,右邊出去的條件是當前新加入的數小於等於隊尾,就可以把隊尾刪除。

同時注意,由於某些區域只能由一個設備覆蓋,所以對於[s,t]的這種區域,我們對於所有的s<i<t,都讓f[i]=無窮大+1;
而且只有當i是偶數的時候,f[i]纔有解。
q數組裏面記錄的是下標.

代碼

#include <iostream>
#include <cstdio>

#define R          register int
#define re(i,a,b)  for(R i=a; i<=b; i++)
 
using namespace std;

int const inf=(int)1e7;
int const N=1000005;

int a,b,n,l;
int f[N],q[N];

int main() {
    scanf("%d%d%d%d",&n,&l,&a,&b);
    re(i,1,l) f[i]=inf;
    re(i,0,n-1) {
        int x,y;
        scanf("%d%d",&x,&y);
        re(j,x+1,y-1) f[j]=inf+1;
    }
    f[0]=0;
    int ft=0,lt=-1;
    for(int i=2; i<=l; i+=2) {
        while(ft<=lt && q[ft]<i-2*b) ft++;
        if(i-2*a>=0) {
            while(ft<=lt && f[q[lt]]>=f[i-2*a]) lt--;
            q[++lt]=i-2*a;
        }
        if(f[i]==inf+1) continue;
        if(ft<=lt) f[i]=1+f[q[ft]];
    }
    if(f[l]>=inf) f[l]=-1;
    printf("%d\n",f[l]);
    return 0;
}

例題2 最大子段和

最大子段和(最大子序和)
鏈接:
Luogu:https://www.luogu.com.cn/problem/P1115
51Nod:https://www.51nod.com/Challenge/Problem.html#problemId=1049
vjudge:https://vjudge.net/problem/51Nod-1049
在這裏插入圖片描述

思路

f[i]表示以i爲結尾的最大子序和
f[i]=sum[i]-min(sum[i-1],sum[i-2]…sum[i-m])
維護一個單調隊列即可。

代碼

#include <stdio.h>
#include <algorithm>

using namespace std;

typedef long long ll;

int const N=50005;

ll n;
ll a[N],f[N];

int main() {
    scanf("%lld",&n);
    for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
    for(int i=1; i<=n; i++) if(f[i-1]>0) f[i]=f[i-1]+a[i];
        else f[i]=a[i];
    ll ans=0;
    for(int i=1; i<=n; i++) ans=max(f[i],ans);
    printf("%lld\n",ans);
    return 0;
}

例題3 綠色通道

CodeVS-3342(可惜CodeVS死了)

題目描述

《思遠高考綠色通道》(Green Passage, GP)是唐山一中常用的練習冊之一,其題量之大深受lsz等許多oiers的痛恨,其中又以數學綠色通道爲最。2007年某月某日,soon-if (數學課代表),又一次宣佈收這本作業,而lsz還一點也沒有寫……

高二數學《綠色通道》總共有n道題目要寫(其實是抄),編號1…n,抄每道題所花時間不一樣,抄第i題要花a[i]分鐘。由於lsz還要準備NOIP,顯然不能成天寫綠色通道。lsz決定只用不超過t分鐘時間抄這個,因此必然有空着的題。每道題要麼不寫,要麼抄完,不能寫一半。一段連續的空題稱爲一個空題段,它的長度就是所包含的題目數。這樣應付自然會引起馬老師的憤怒。馬老師發怒的程度(簡稱發怒度)等於最長的空題段長度。

現在,lsz想知道他在這t分鐘內寫哪些題,才能夠儘量降低馬老師的發怒度。由於lsz很聰明,你只要告訴他發怒度的數值就可以了,不需輸出方案。(快樂融化:那麼lsz怎麼不自己寫程序?lsz:我還在抄別的科目的作業……)

輸入描述

第一行爲兩個整數n,t,代表共有n道題目,t分鐘時間。
以下一行,爲n個整數,依次爲a[1], a[2],… a[n],意義如上所述。

輸出描述

僅一行,一個整數w,爲最低的發怒度。

樣例輸入

17 11
6 4 5 2 5 3 4 5 2 3 4 5 2 3 6 3 5

樣例輸出

3

數據範圍

60%數據 n<=2000
100%數據 0 < n <=50000,0 < a[i] <=3000,0< t<=100000000

思路

先二分枚舉答案 枚舉一個最大的空題區間mid,然後dp判斷是否可以達到
F[i]表示第i題一定要做的情況下,前i題滿足條件,最少需要花費的時間
F[i]=min(f[j])+a[i]; i-mid-1<=j<=i-1 單調隊列

代碼

CodeVS沒了,代碼也沒了。

例題4

POJ-1276
鏈接:http://poj.org/problem?id=1276
https://vjudge.net/problem/POJ-1276

在這裏插入圖片描述

思路

用單調隊列優化多重揹包
多重揹包狀態轉移方程:
f[i][j]=max(f[i-1][j-kw[i]]+kv[i])
0<=k<=min(num[i],j/w[i]);
有三重循環
二進制可以優化一下
通過單調隊列可以優化到O(nm)
求f[i][0…m]
設餘數爲r,我們來求解f[i][r+kw[i]];
r=0 f[i][0] ,f[i][w[i]],f[i][2
w[i]],f[i][kw[i]]
r=1
f[i][1],f[i][1+w[i]],f[i][1+2
w[i]],…f[i][1+kw[i]]
f[i][r],f[i][r+w[i]],f[i][r+2
w[i]]…f[i][r+kw[i]]
f[i][j]=max(f[i-1][r]+x
v[i],f[i-1][r+w[i]]+(x- 1)v[i],f[i-1][r+2w[i]]+(x-2)v[i],…f[i- 1][r+xw[i]]+0v[i]);
f[i][j]=max(f[i-1][j+k
w[i]]+(x-k)v[i]);
f[i][j]=max(f[i-1][j+k
w[i]]-kv[i])+xv[i];
f[i-1][j+kw[i]]-kv[i]放到單調隊列裏面
k表示在x個裏面少取了k個
0<=k<=min(x,num[i])
j mod w[i]=r
j div w[i]=x

代碼

#include <cstdio>
#include <cstring>

using namespace std;

int const N=13;
int const M=100005;

int n,m;
int f[M],a[N],b[N],q[M],v[M];

int main() {
    while(scanf("%d",&m)!=EOF) {
        scanf("%d",&n);
        for(int i=1; i<=n; i++) scanf("%d%d",&b[i],&a[i]);
        memset(f,0,sizeof(f));
        for(int i=1; i<=n; i++) for(int r=0; r<a[i]; r++) {
            int st=0,ed=-1,k=0;
            for(int j=r; j<=m; j+=a[i],k++) {
                while(st<=ed && q[st]<k-b[i]) st++;
                while(st<=ed && v[ed]<=f[j]-k*a[i]) ed--;
                q[++ed]=k,v[ed]=f[j]-k*a[i];
                f[j]=v[st]+k*a[i];
            }
        }
        printf("%d\n",f[m]);
    }
    return 0;
}

其他練習

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章