1. 程式人生 > >$Dynamic Planning Optimization$ 關於動態規劃的優化方案(%$color{red}{rqy}$)

$Dynamic Planning Optimization$ 關於動態規劃的優化方案(%$color{red}{rqy}$)

單位 位置 最大 向上 map pict 但是 -m 格子

關於動態規劃的優化方案(%\(\color{red}{rqy}\))

1.單調隊列

單調隊列是一種具有單調性的隊列,其中的元素全部按照遞增或者遞減的順序排列,就比如下面這個遞減隊列。

技術分享圖片

假如說我們要在隊尾加入一個\(5\),那麽我們入隊的步驟就是這樣的:

發現隊尾\(1\),(q[tail]),\(1<5\),則將1退出(tail--)

發現隊尾\(2\),(q[tail]),\(2<5\),則將2退出(tail--)

發現隊尾\(3\),(q[tail]),\(3<5\),則將3退出(tail--)

發現隊尾\(8\),(q[tail]),\(8>5\),停止退出隊尾,將\(5\)
入隊。

經過上述步驟之後隊列變為了{8,5},依然滿足遞減的單調性,而實際上這也就是單調隊列的基本操作。而維護遞增的方式也是一樣的。

#define MAXN 100010
int n,a[MAXN];
int q[MAXN],head=1,tail=1;
for(int i=1;i<=n;i++){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);//輸入 
    q[1]=a[1];//將第一個元素入隊 
    for(int i=2;i<=n;i++){
        while(head<=tail&&q[tail]<a[i])
        //如果隊列不為空並且隊尾元素小於a[i] 
            tail--;//彈出隊尾元素 
        q[++tail]=a[i];//入隊 
    }
}

【例題1】

我們現在有一個整數序列\(A(a[MAXN])\),長度為\(n\),又知兩個參數\(k\)\(m\),要求:從\(A\)序列中找出\(k\)個不相交的區間,每段區間長度\(len\)<=\(m\),要求所有k個區間的區間和最大。

考慮最基本的\(DP\),設\(dp[i][j]\)表示從前\(j\)個數裏面選出來\(i\)個長度不超過m的不相交區間的區間和最大值,然後我們再枚舉一個\(k\),指選擇\([k+1,j]\)這個子區間。然後我們創造一個前綴和數組\(sum[MAXN]\),那麽\([k+1,j]\)這個區間的區間和就是\(sum[j]-sum[k]\)。子問題分為兩塊:\(j\)
選入子區間,或者\(j\)不選入子區間,從\(j-m\)\(j\)範圍內枚舉一個\(k\)使得\(dp[i-][k]+sum[j]-sum[k]\)最大,然後與\(dp[i][j-1]\)取一個\(max\)可得答案。


for(int i=1;i<=n;i++)
    sum[i]=sum[i-1]+a[i];//前綴和數組
for(int i=1;i<=k;i++)
for(int j=1;j<=n;j++){
    int ans=-INF;
    for(int k=j-m;k<=j;k++){
        ans=max(ans,dp[i-1][k]+sum[j]-sum[j]);
    }
    dp[i][j]=max(ans,dp[i][j-1]);
}

這樣的\(DP\)時間復雜度為\(O(nmk)\),顯然太大,於是我們考慮優化。

我們可以看到\(DP\)的原式子是\(\color{red}{dp[i][j]=max(dp[i][j-1],max(f[i-1][k]+sum[j]-sum[k]));}\)

我們發現在裏面的\(k\)的最優化枚舉當中,sum[j]是不隨k的枚舉變化的,所以我們可以將sum[j]提出來變成:\(\color{red}{dp[i][j]=max(dp[i][j-1],sum[j]+max(f[i-1][k]-sum[k]));}\)

可以知道在整個式子裏面最耗時間的就是最後關於\(dp[i-1][k]-sum[k]\)最大值的枚舉,所以只要快速計算出來了\(dp[i-1][k]-sum[k]\)就可以快速計算整個式子。我們來看\(dp[i-1][k]-sum[k]\)的範圍是在\([0][0],[0][1],....[0][m-1],[1][m],[2][m+1],...,[n-m][n-1]\)這些區間上的最大值,也就是所有的\([j][i+j-1]\)的區間。

技術分享圖片

我們發現這些區間的左右端點都是單調遞增的,所以我們可以利用單調隊列在\(O(1)\)的時間內解決這些區間。然後我們就將時間優化到了\(O(nk)\)

一個\(n×m\)的矩形網格。你初始站在\((x,y)\)這。有些格子有障礙而有些沒有。有\(K\)個時間段。第\(i\)個時間段從\(s[i]\)持續到\(t[i]\)(包括兩端)這段時間內網格會向某個方向(上下左右之一)傾斜。所以每個時間段內的每個時間單位,你可以選擇在原地不動,或者向傾斜的方向走一格(當然你不能走到障礙上或是走出網格)。

求你最多能走多少格。

技術分享圖片

如上圖所示,黑色方塊為障礙,\(S\)為起始點。

按照最常的\(DP\)思路來看,我們設\(dp[k][i][j]\)為在k時間點,從\((x,y)\)節點走到了\((i,j)\)節點的時候最長走了多長。初始化\(dp[0][i][j]\)全部為\(?∞\),而\(dp[0][x1][y1]0\)=\(0\)(\(x1,y1\)為初始位置),考慮子問題就是:從那邊來?\(k\)時刻是從那個方向來還是不動?我們以第\(k\)時刻向右傾斜為例。

技術分享圖片

如果是向右傾斜,那麽上一層狀態就是在\((i,j-1)\)地點,那麽結合兩個子問題我們可以得出\(DP\)方程式:\(dp[k][i][j]=max(dp[k-1][i][j],dp[k-1][i][j-1]+1);\)

for(int k=1;k<=len;k++)
    for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
        dp[k][i][j]=max(dp[k-1][i][j],dp[k-1][i][j-1]+1);

那麽這樣的時間復雜度就是\(O(nm\sum_{i=1}^{K}(t[i]-s[i]+1))\),是無法通過這個題的全部數據的。然後我們緊接著考慮怎麽優化。關於位置的\(n^2\)枚舉我們沒有什麽辦法,但是關於\(K\)我們可以進行優化,時間點很多有\(\sum_1^K(t[i]-s[i]+1)\)個,但是時間段\(K\)卻<=\(200\),那麽我們可以將一段時間的轉移全部合並起來一起算,那麽就快得多了。

我們設\(dp[k][i][j]\)為在第\(k\)個時間段末尾,從\((x,y)\)走到了\((i,j)\)點,\(len[k]\)為第\(k\)個時間段的持續時間,可以算出是\(t[k]-s[k]+1\)

首先還是\(n^2\)的枚舉,和\(k\)時間段的枚舉,之後我們還有一個\(l\)的枚舉,這個\(l\)枚舉的是上一個狀態加上在當前這個\(k\)的時間段內一共走的步數對應傾斜方向的橫、豎坐標,如果我們繼續以右傾為例,那麽\(j-len[j]<=l<=j\),就是從完全不動到走了最多的\(len[k]\)步,那麽我們有了狀態轉移方程式:\(\color{red}{dp[k][i][j]=max_{j-len[k]<=l<=j}(dp[k-1][i][l]+j-l)}\),由於其中的+\(j\)與l的枚舉並無關聯,所以提出來就變成了\(\color{red}{dp[k][i][j]=max_{j-len[k]<=l<=j}(dp[k-1][i][l]-l)+j}\)。其實也就是枚舉這個時間段之前這個人的位置在哪,也就知道了當前的\(dp[k][i][j]\)是從哪裏轉移過來的。

之後,我們回過頭來看上一道題的最後的\(DP\)方程式:\(\color{red}{dp[i][j]=max(dp[i][j-1],sum[j]+max(f[i-1][k]-sum[k]));}\)

是不是發現格式非常的相似呢?,我們固定住\(i\)之後的狀態轉移方程式基本是和上題一樣的,所以一樣可以使用單調隊列優化到\(O(nmK)\)

下面針對一組樣例,我們進行一遍手動模擬,以幫助更好的理解。

就用洛谷的樣例吧。(第一行分別為n,m,x1,y1,k)

4 5 4 1 3

. . xx.

. . . . .

. . . x.

. . . . .

1 3 4

4 5 1

6 7 3

那麽畫完圖之後就是這個樣子:

技術分享圖片

\(1\)~\(3\)時刻的傾斜方向是右,那麽縱坐標是你不變的,我們枚舉縱坐標。

for(int i=1;i<=k;i++){
        int s; int t; int dir;
        scanf("%d%d%d",&s,&t,&dir);
        //註意要反著DP,也就是倒退
        int len=t-s+1;
        if(dir==1)  //北面(上)
            for(int j=1;j<=m;j++)//北面的話橫坐標不變,那麽我們枚舉縱坐標
                DP(i,n,j,dir,len);
        if(dir==2)  //南面(下)
            for(int j=1;j<=m;j++)//南面的話橫坐標不變,那麽我們枚舉縱坐標
                DP(i,1,j,dir,len);
        if(dir==3)  //西面(左)
            for(int j=1;j<=n;j++)//西面的話縱坐標不變,那麽我們枚舉橫坐標
                DP(i,j,m,dir,len);
        if(dir==4)  //東面(右)
            for(int j=1;j<=n;j++)//東面的話縱坐標不變,那麽我們枚舉橫坐標
                DP(i,j,1,dir,len);
    }

然後當我們的橫坐標x枚舉到1的時候,我們在DP函數裏面定義一個now,然後是\(while(x>=1\)&&\(x<=n\)&&\(y>=1\)&&\(y<=m)\),因為首先要保證不超過邊界。然後如果我們發現右面是可以走的,那麽我們就進行一個push操作。也就是關於dp[p-1][x][y]在單調隊列裏面的入隊操作。在最前面我們已經介紹了。

void push(int now,int value){
    if(value==-INF) return ;
    //如果壓根做不到這裏,那麽直接返回
    while(head<=tail&&value-now>=q[tail])
        tail--;//彈出隊尾
    q[++tail]=value-now;
    pos[tail]=now; 
    //pos記錄位置,用來判斷是不是可以滑
}

而至於為什麽要在\(while\)裏面減去一個\(now\),是因為(x,y)這個位置不一定是在當前方向的起點上,因為之後某一步的步數減去當前的步數得到的值就是(x,y)到那一步在的點的距離,相當於一個化簡~

由於\(dp[0][i][j]\)=-\(INF\),當前的\(p\)=\(1\)所以\(p\)-\(1\)的時候\(value\)就是-\(INF\),所以在第0個時間段到不了這個地方,我們直接返回。然後下面其實就沒什麽事了,所有的push全部直接返回,最後退出DP函數。就這樣進行到\(x\)(即\(j\))=\(3\)的時候,我們發現\(map[3][4]\)是一個障礙點,那麽也就是說我們之前進行的所有工作全部無效,然後我們將整個隊列清空,即\(head\)=\(1,tail\)=\(0\);

然後接著進行到\(x\)=\(4\),\(y\)=\(1\)(\(4\)\(1\)列)的時候,我們到了起始點,而起始點的dp[0][4][1]是0,所以\(value\)!=-\(INF\),我們終於將一個值\(value\)-\(now\)=-1入隊了,那麽我們當前的隊列是這個樣子的:

技術分享圖片

加上步數之後我們發現\(dp[p][x][y]=q[head]+now\)依然是\(0\),所以\(ans\)沒有被更新(廢話,你從起點走到起點需要更新\(ans\)嘛),所以我們繼續向下進行,因為每次\(now\)都會++,所以下面的\(dp[p][x][y]\)加上\(now\)之後就可以更新\(ans\)的值了。然後進行到\(x\)=\(4\),\(y\)=\(5\)的時候,我們發現\(now-pos[head]=4\),大於可以\(len\),也就是說超過了可以滑動的區間。(一共就三秒你怎麽滑第四塊啊~)那麽我們將隊首彈出,接下來我們就不能再更新ans的最大值了,\(x\)=\(4\)時完美結束。這個時候我們的行走路徑大概如下:

技術分享圖片(藍色方塊為當前方塊,黃色方塊為路徑)

也就是說從\(1\)~\(3s\)我們最多可以走3塊。(真是麻煩啊~)

\(i\)繼續走,我們進行到下一個時間段。\(4\)~\(5s\)的時候是向北傾斜的。那麽我們進行\(DP(i,n,j,dir,len)\),我們從\(n\)\(j\)列開始\(DP\),第一次將\(tail\)彈出後又入隊我們不管,因為\(j=1\)\(2\)的時候都不能更新\(ans\),然後到了\(j=3\)的時候,我們將\(dp[1][4][3]-now=1\)入隊了。

技術分享圖片

然後當\(now\)進行到第三次的時候我們就可以更改ans值為4了。

之後結束了第二個時間段。此時的路徑大概是這樣的:

技術分享圖片

最後在第三個時間段內,我們將路徑更改為如下:

技術分享圖片

那麽以上就是整個樣例的模擬,最終我們得到\(ans\)數為6.

關於單調隊列優化的一點總結

鑒於兩者之間的\(DP\)轉移方程的相似性,我們成功的利用單調隊列優化了問題,那麽回過頭來看看,什麽樣的問題可以利用單調隊列進行優化呢?我們最上面講的單調隊列是具有單調性的一種數據結構,他可以保證數據的單調性,自然也就可以留下數據的最大值或者最小值,利用了單調性,就是減少了一位枚舉,減去一維,直接獲得單調隊列裏面的最優解。並且DP可以使用單調隊列優化,當且僅當\(DP\)式的格式基本滿足\(\color{red}{dp[i]=a[i]+max_{l[i]<=j<=r[i]}b[j]}\)的時候。即“\(dp[i]\)=\(A(i)\)+\(B(j)\)中的最小/大值 \((i-k<=j<i,k\)為常數\()\)”,當你發現要求\(max\)而且求可能拓展的狀態有線性關系的時候,你就可以考慮單調隊列優化了。

$Dynamic Planning Optimization$ 關於動態規劃的優化方案(%$\color{red}{rqy}$)