有關動態規劃的相關優化思想

PaperHammer發表於2022-04-29

【以下內容僅為本人在學習中的所感所想,本人水平有限目前尚處學習階段,如有錯誤及不妥之處還請各位大佬指正,請諒解,謝謝!】

引言

前一篇文章(有關動態規劃 - PaperHammer - 部落格園 (cnblogs.com))我們探討了動態規劃及其分析方法,但在做題或面試時往往會需要我們對空間或時間進行優化,尤其是對空間的優化尤為常見。動態規劃本就難度較大,對於尚處學習階段的同學,能寫出來就算很不錯了。在此,本人將分享我在做題時的想法與見解,並和大家一起學習新的知識,如有錯誤或更好的思想還請各位大佬留言指正,謝謝!

複雜度分析

複雜度分為時間複雜度與空間複雜度,其不代表程式正真執行時間與所佔空間,只反映在執行時間或佔用空間隨資料規模增大的變化趨勢,以最高階的變化趨勢來分別代表整個程式的時空複雜度。

【注:本文重點非複雜度的介紹,相關詳細內容將在其他文章中介解釋】

(一)空間

空間複雜度取決於我們用來記錄之前資料的儲存結構。一般地,在DP中常見的空間複雜度有O(N)與O(N2)或者更大,但一般不超過O(N3),通常我們需要對其指數至少減小1或降階,以達到相應的需求。減少的關鍵思想在於,判斷我們所儲存過的資料是否會被再次訪問,是否可以不用一直儲存。通常採用畫表的方式進行判斷。

(二)時間

時間複雜度取決於迴圈結構,而迴圈結構又取決於推匯出的狀態轉移方程。一般地,常見的時間複雜度為O(N),O(N2)或者更大,但一般不超過O(N3),通常我們需要對其指數至少減小1或降階。其關鍵思想在於,一是對某些特殊情況進行判斷,使得直接在迴圈中不進行操作,通常採用邏輯推理方式進行;二是利用某些資料結構和數學理論進行優化。一般地,當我們寫出狀態轉移方程後,就可得出時間複雜度。

優化思路

空間(減少儲存結構規模)

(1)   減少變數總數

(2)   減少不必要的資料的儲存

時間(減少迴圈次數)

(1)優化變換狀態(從變數的角度,排除無意義或可省去的部分)

2)選擇適當的規劃方向(從情況的角度,多種方法對比)

3)四邊形不等式與優化決策單調

 

第一部分 空間優化

上一篇文章中我們講到了定變數的過程,我們採用陣列的方式儲存資訊,每個維度的索引代表一個變數,不難發現:變數的數目往往決定了我們儲存結構的規模且規模呈次方級增長,雖然空間與時間比起來不那麼“寶貴”,現在的裝置運存至少也有10 G,但必要的優化還是需要,我們不能因為足夠就開始浪費。

題:01揹包

【注:在此不重複該題目及方程推導過程,如有需要,請轉至上一篇文章有關動態規劃 - PaperHammer - 部落格園 (cnblogs.com)

方程:f [ i ][ v ] = max(f [ i ][ v ], f [ i-1 ][ v-w[i] ] + c[i]);

f [ i ] [ j ] = max(f [ i-1 ][ j ], f [ i-1] [ j-w[i] ] + c[i]);

常見的有上述兩種形式的方程,其原理是一致的。第一個方程的解釋不再贅述;第二個方程中i表示前i件物品,j表示當前揹包的容量,f[ i ][ j ]表示在前i件物品中不超過容量j時的最大價值。

例:

輸入格式

第一行兩個整數 N,V 用空格隔開,分別表示物品數量和揹包容積。

接下來有兩行,第一行為 wi,第二行為ci 用空格隔開,分別表示第 i 件物品所佔的空間和價值。

輸入樣例

4 20
8 9 5 2
5 6 7 3

輸出樣例

16

 

1)減少不必要的資料的儲存

針對第二個方程(第一個類似),我們給出利用上面的例子列出f[ i ][ j ]所每個索引下所對應的值。可以發現i只來源於i-1,即當前狀態下的i僅前一次有關,與前兩次、前三次均無關。

 

從表中的資料也可以看出,第i行的資料只依賴於第i-1行的值,因此我們只需用兩個一維陣列儲存第i-1行和第i行這兩行的值即可,其中f1[ i ]代表f[ i-1 ][ j ],f2[ i ]表示f[ i ][ j ]。

所以第二個方程可以轉化為:f2[ j ] = max(f2[ j ], f1[ j-w[ i ] ] + c[ i ]);

此時,空間複雜度從原來的O(N2)降為了O(N)。

for(int i = 1; i <= n; i++){
    for(int j = 1; j <= v; j++){
        f2[j] = f1[j];
        if(j-c[i] >= 0)
            f2[j] = max(f2[j], f1[j-w[i]]+c[i]);
    }
    for(int k = 1; k <= v; k++)
        f1[k] = f2[k];
}
cout << f2[v] << endl;

 

2)減少變數數

但在許多面試題中往往需要我們進行原地修改,那就意味著我們只能使用一個一維陣列。同樣這道題也可以進行原地修改。但現在的迴圈順序是容量從小到大的情況,如果原地修改會出現覆蓋資料的情況

 

我們來分析一下原因:

當我們使用二維陣列時,紅色代表i-1行儲存的資訊,藍色代表當前第i行的值,該值受到所對應的上一行值的影響。如果我們換成一維陣列,從幾何意義上其可以反映為:

1.初始狀態:整行都為上一次所儲存的值

 

2.開始更新:根據方程可知,每一個新值受上一個值的影響,即需要上一個值的存在。如果我們依舊從小到大進行更新,那麼首先就覆蓋了第一個值。之後就會導致我們並沒有按照方程原本的意思:根據上一個值計算結果,反而是用新值計算結果,故結果不正確。

 

所以我們需要將原本正向的更新改為反向,這樣就能避免該問題。

for(int i = 1 ;i<=n; i++) {
    for(int j = v; j>=0 ;j--)
        if(j-w[i]>=0)
            f[j] = max(f[j],f[j-w[i]]+c[i]);
}
cout << f[v] << endl;

 

第二部分 時間優化

時間往往是我們在程式設計中更看中的一點,不論是競賽還是面試,很多情況下都需要對時間的把控。一般來說動態規劃在暴力的基礎上已經對時間有了很好的優化,但依然可以再優化下面還是通過例子來講述這兩種方法。

題:傳紙條

      來源:P1006 [NOIP2008 提高組] 傳紙條 - 洛谷 | 電腦科學教育新生態 (luogu.com.cn)

將題目抽象化後可翻譯為:從點(1,1)走到(m,n)再從(m,n)回到(1,1)方向只能向下(上)或向右(左),即起點在左上角,終點在右下角。且每個點只能走一次,每個點上有一個正數,返回所有在兩次行走的可行路徑中的數值的最大和。

往返路徑且往返過程等價,不妨將其看為從同一點出發的兩條路徑。利用上一篇文章的方法步驟,把每次選擇怎麼走作為一個子問題,並且總是以最大和為基礎進行操作,每次選擇處理相同

對於第一條路上的點(i,j)有兩種選擇方式;

對於第二條路上的點(k,l)有兩種選擇方式;

選擇方式的兩種理解:(從左邊到達,從上邊到達)/(向右走,向下走)

由於二者等價所以合併後對於點(i,j,k,l)有四種選擇方式

不難推出方程:f[ i ][ j ][ k ][ l ] = max( f[ i ][ j-1 ][ k-1 ][ l ] , f[ i-1 ][ j ][ k ][ l-1 ] , f[ i ][ j-1 ][ k ][ l-1 ] , f[ i-1 ][ j ][ k-1 ][ l ] ) + value[ i ][ j ] + value[ k ][ l ];為了避免重複每次特判即可,詳細內容見文末附錄。

根據上述方程可以得到此時的時間複雜度為O(N2*M2),理論上只能承受N,M<=50的資料量,甚至更小。

for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            for (int p = 1; p <= n; p++)
                for (int q = 1; q <= m; q++) {
                    f[i][j][p][q] = max(max(f[i - 1][j][p - 1][q], f[i - 1][j][p][q - 1]), max(f[i][j - 1][p - 1][q], f[i][j - 1][p][q - 1])) + g[i][j] + g[p][q];
                    if (i == p && j == q)f[i][j][p][q] -= g[i][j];
                    //去重,只需判斷第二次走的點是否被第一次所走過,因為兩條路經本身不會經過自己走過的點,只可能兩條路相交 
                }

 

1)優化狀態(重新構建變數的意義,改進狀態表示)

三維DP剛剛提到,兩個路徑的行進是完全等價的,每條路徑在每個點只有兩種選擇,且它們是同步進行的,所以可以得到當前總步數steps = i + j = k + l;利用這一點,我們列舉當前走過的步數,同時列舉第一條路徑和第二條路徑當前的橫座標或者縱座標,f[k][i][j]表示走了k步,第一條路徑走到第i行,第二條路徑走到第j行的最大價值,於是方程轉變為:f[ k ][ i ][ j ] = max ( f[ k-1 ][ i ][ j ], f[ k-1 ][ i-1 ][ j-1 ], f[ k-1 ][ i ][ j-1 ], f[ k-1 ][ i-1 ][ j ] ) + value[ i ][ k-i+1 ] + a[ j ][ k-j+1 ];

此時,方程時間複雜度為O(N2 * (N+M))

for (int k = 1; k <= n + m-1; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++) {
                if ( k-i + 1 < 1 || k - j + 1 < 1) //判斷縱座標的合法性,不合法就跳過
                    continue;
                 f[k][i][j] = max(max(f[k - 1][i][j], f[k - 1][i - 1][j - 1]), max(f[k - 1][i][j - 1], f[k - 1][i - 1][j])) + g[i][k - i + 1] + g[j][k - j + 1];
                if (i == j) f[k][i][j] -= g[i][k - i + 1];
            }

 

二維DP沿用01揹包的優化思想,當前狀態與前一個狀態有關,所以我們原地修改,反向更新,即可再降一維。

for(int k=3;k<=n+m;k++)
        for(int i=n;i>=1;i--)
            for(int p=n;p>i;p--)
                f[i][p]=max(max(f[i][p],f[i-1][p-1]),max(f[i-1][p],f[i][p-1]))+g[i][k-i]+g[p][k-p];

 

2)選擇適當的規劃方向

動態規劃的規劃方向主要有兩種:順推和逆推。有些情況下,不同的規劃方向,時間效率有所不同。一般地,若初始狀態確定,目標狀態不確定,“從因到果”,則應考慮採用順推;反之,若目標狀態確定,而初始狀態不確定,“從果到因”,就應該考慮採用逆推。若初始狀態和目標狀態都已確定,一般情況下順推和逆推都可以選用。但是,我們還可以使用類似雙指標一樣的思想,進行雙向規劃,即雙執行緒

題:傳紙條——三維DP

其實,剛才的三維DP已經體現了雙執行緒的思想,即同時進行,本質就是對四維DP的優化

 

【注:下方內容,即第(3)點在此僅作為補充說明,本人尚未對其深入瞭解與研究,僅供參考】

 3)四邊形不等式與優化決策單調

該方法實際上是基於數學理論推導而來。

有關四邊形不等式,它可以針對形如f[ i, j ] = min/max{ m( i, k-1 ), m( k, j) } + w( i, j ) (i ≤ k ≤ j)的方程進行降階優化。

補充說明:

  1. a.    區間包含的單調性:對於i ≤ i' < j ≤ j',有w( i' , j ) ≤ w( i , j' ),稱w具有區間包含的單調性,即保號性,區間小則對應值就小。
  2. b.    四邊形不等式:對於i ≤ i' < j ≤ j',有w( i , j ) + w( i' , j' ) ≤ w( i' , j ) + w( i , j' ),稱函式w滿足四邊形不等式,即小區間與大區間之和<=交錯區間和。
  3. c.     定理一:如果上述w同時滿足區間包含單調性和四邊形不等式,那麼m也滿足四邊形不等式性質。
  4. d.    定理二:如m滿足四邊形不等式,則s( i , j )單調,即s( i , j ) ≤ s( i , j+1 ) ≤ s( i+1 , j+1 )。其中,s表示m去最優值時的下標。

【注:相關證明過程在此不提供】

由此,我們可以得到:s[ i , j-1 ] ≤ s[ i , j ] ≤ s[ i+1 , j ];

所以上述方程轉變為:f[ i , j ] = min/max{ m[ i , k ] + m[ k , j ]} (s[ i , j-1 ] ≤ k ≤ s[ i+1 , j ]);

此時,時間複雜度從O(N3)減小為O(N2)

 

總結

動態規劃的優化還有許多型別,本人目前僅瞭解以上內容,每一種優化思想都有獨特的地方,但這些思想往往十分複雜讓人望而生畏,只有通過平時一道題一道題的磨礪,才能逐漸領悟。眾所周知,寫出程式碼不難,難的是如何對現有程式碼進行優化,當我們能夠對我們所寫過的程式碼提供一份優化方案時,相信我們的水平一定上了一個臺階,讓我們一起努力,加油!

【感謝您可以抽出時間閱讀到這裡,內容可能會有許多不妥之處;受限於水平,許多地方可能存在錯誤,還請各位大佬留言指正,請見諒,謝謝!】

 

#附文中所提到的2個題目的程式碼(僅提供文中提到的部分方案,可滿足絕大多數時的要求)

(1)01揹包

#include <bits/stdc++.h>
using namespace std;
int n,v,c[102],w[102],f[100000];

int main() {
    cin >> n >> v;    
    for(int i = 1; i <= n; i++)
        cin >> w[i];    
    for(int i = 1; i <= n; i++)
        cin >> c[i];
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= v; j++) {
            if(j - w[i] >= 0)
                f[j] = max(f[j], f[j - w[i]] + c[i]);
        }    
    }    
    cout<<f[v]<<endl; 
    return 0;
}

 

(2)傳紙條(三維DP)

#include<bits/stdc++.h>
using namespace std;

int n, m, g[54][54], f[108][54][54];
int main()
{
    ios::sync_with_stdio(false);
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            cin >> g[i][j];
    for (int k = 1; k <= n + m-1; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++) {
                if ( k-i + 1 < 1 || k - j + 1 < 1) //判斷縱座標的合法性,不合法就跳過
                    continue;
                 f[k][i][j] = max(max(f[k - 1][i][j], f[k - 1][i - 1][j - 1]), max(f[k - 1][i][j - 1], f[k - 1][i - 1][j])) + g[i][k - i + 1] + g[j][k - j + 1];
                if (i == j) f[k][i][j] -= g[i][k - i + 1];
            }
    cout << f[n + m - 1][n][n] << endl;
    return 0;
}

 

(3)傳紙條(二維DP)

#include<bits/stdc++.h>
using namespace std;

int n, m, g[54][54], f[108][108];
int main()
{
    ios::sync_with_stdio(false);
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            cin >> g[i][j];
    for(int k=3;k<=n+m;k++)
        for(int i=n;i>=1;i--)
            for(int p=n;p>i;p--)
                f[i][p]=max(max(f[i][p],f[i-1][p-1]),max(f[i-1][p],f[i][p-1]))+g[i][k-i]+g[p][k-p];
    cout << f[n - 1][n] << endl;
    return 0;
}

相關文章