# DP進階訓練:區間dp + 數位dp + 狀壓dp

DP進階訓練:區間dp + 數位dp + 狀壓dp

vj題單


A. Multiplication Puzzle (區間dp)

題意:

  • 首先這道題題意大概是:n個數字,每次你能拿走一個數字(除了兩邊的),貢獻是這個數字和兩邊兩個數字的成績。最後題目要求你按任意順序拿走n-2個數字,使得貢獻和最小。

分析:

  • 順序: 首先能想到是個dp題,因爲在思考時候就是在考慮最優的處理順序是什麼。注意,這裏提到了一個很關鍵的詞:順序,這表示一種策略,顯然不能想到合適的貪心思路,那就往dp方向想。
  • 性質: 我在最初思考時候,一般按照yxc dalao的閆式dp分析法,首先先認真分析這道題中存在的性質。(個人的一點看法:每個題不同的性質是解這道題的關鍵,如果你感覺沒有思路,可能是沒有抓住或挖掘出關鍵性質)。本題我認爲關鍵的性質是:三個相鄰的數要一同考慮,貢獻是a[i - 1] * a[i] * a[i + 1]。三個數考慮的結果是a[i]從數組中刪除。一個數組不斷刪除,會從長數組變成短數組,即若短數組最優解計算出來後,可以反着求出長數組的最優解。這個性質就已經指向了區間dp,因爲前面的性質是在區間間轉移。
  • 表示: 通過前面分析,你得到了解題思路是dp, 關鍵性質是區間間的轉移方式。接着就應該去思考如何通過dp數組來表示一個局面,或者說dp數組如何定義。誤區,再以前做dp題時候我有個誤區,就是沒有特別看重題目性質分析這一步,這導致我經常在嗯想dp表示和轉移。還有個誤區就是我總是希望dp數組能表示一個局面,然後再不同的局面間進行dp轉移。但事實上具體的局面很難表示這也是我做不出dp題的原因之一。如何去思考dp表示呢:一個很關鍵的詞就是————抽象。試想一下動態規劃問題就是在考慮一大堆子狀態局面間的轉移問題,但是狀態表示和狀態轉移都十分不好想。但是我們有時候看題解會發現他們中狀態轉移非常絲滑,原因就是狀態表示的好,使得兩個狀態間的轉移非常清晰。所以要練習自己抽象的能力,因爲好的狀態表示它不是表示出一個具體的局面,比如本題中具體局面就是一種剩餘數字的結果,如[10, 1, 50 ,5] 這種,我們會發現這怎麼表示呀?? 所以好的表示是把這道題涉及的子狀態局面劃分成多個抽象的集合,每個集合是許多小局面的組合,而這些集合間能有清晰的轉移。具體見下圖。本題相對簡單,因爲區間dp一般就是dp[l][r],本題中表示l到r這個區間處理結束的最優解。

  • 轉移: 有了好的表示轉移是不難的,更多時候表示和轉移是放在一塊考慮的,單考慮一個可能會讓另一個的工作變得難做。本題中是dp[l][r]表示l到r之間處理結束, 那子狀態如何貢獻給dp[l][r]呢?這裏有個關鍵詞:完全覆蓋 意思就是在考慮狀態轉移時要把所有能更新當前狀態的子局面都考慮到。所以本題考慮l,r之間的k節點是當前[l,r]數組最後一個更新的,dp[l][r] = min{dp[l][k] + dp[k][r] + a[l] * a[r] * a[k]} dp[l][k] + dp[k][r] 就表示l到k間數字和k到r間數字都刪除完的最優結果。我再一開始考慮時犯了個錯誤,我想的是dp[l][r] = min{dp[l][k] + dp[k + 1][r] + 更新最優值} 相當於我認爲先把[l, k] 區間刪到最後兩個值,再把[k+1,r]刪到最後兩個值。這裏其實犯的錯誤就是沒有完全覆蓋,因爲我這麼想相當於我把k+1留在了最後再考慮是否刪除,但是對於[l,r]數組k+1它不是邊界數字, 你甚至可以上來就把k+1這個數字刪除了, 但我這樣想相當於給加了一條限制(就是k+1這個數字留到最後刪除),使得原本更新[l,r]的子狀態一部分狀態因爲不滿足這個新加的限制,沒有用於更新[l,r]狀態,這就是沒有完全覆蓋。
  • 整理思路::dp[i][j]表示數組[i,j]之間刪除到只剩兩邊數字時的最優解,轉移方程dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + a[i] * a[k] * a[j]) 表示[i,k],[k,j]兩個數組都刪除到只剩i,k和k,j的最優解的和,然後在[i,k,j]三個數的數組中刪除k進行合併,貢獻爲a[i] * a[k] * a[j]。 初始化:[i,j]長度小於3的初始化爲0, 等於3的初始化爲 a[i] * a[i + 1] * a[i + 2], 大於3的初始化爲INF(因爲後面轉移時取min)。 結果: dp[0][n - 1]

代碼

# 省略頭文件
int main() {
    // freopen("../temp.in", "r", stdin);
    // freopen("../temp.out", "w", stdout);
    int n; scanf("%d",&n);
    vector<int> a(n, 0);
    for(int i = 0; i < n; ++ i) {
        cin >> a[i];
    }
    vector<vector<int> > dp(n, vector<int>(n, 0x3f3f3f3f));

    for(int len = 1; len <= n; ++ len) {
        for(int i = 0; i + len - 1 < n; ++ i) {
            int j = i + len - 1;
            if(len < 3) {
                dp[i][j] = 0;
                continue;
            }
            if(len == 3) {
                dp[i][j] = a[i] * a[i + 1] * a[j];
                continue;
            }
            for(int k = i + 1; k < j; ++ k) {
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + a[i] * a[k] * a[j]);
            }
        }
    }
    cout << dp[0][n - 1] << endl;
    return 0;
}

B. Cheapest Palindrome (區間dp)

題意:

  • 長度爲m的字符串,你能進行操作:給n個字符,在任意位置添加字符或者刪除任意字符。並給出了n個字符的添加和刪除費用。問把s串變成迴文串的最少花費。

分析:

  • 順序: 有一種先把一部分變成迴文,然後更新其他部分的感覺,還有貪心的感覺,但是貪心做不了,考慮dp

  • 性質: 我在分析性質時候還是太浮於表面,分析性質不能只是停留在歸納題目已知點上,要深入一些。比如本題在性質這裏最關鍵的是:兩種操作,變成迴文串。

  • 表示: dp[l][r]表示[l,r]部分變成迴文串

  • 轉移: 在想轉移的時候,不要想複雜了,更不要把所有子問題都考慮進去。你不是要解決dp[l][r]的問題,你只是進行一步轉移,不要把問題深入進去去考慮子問題怎麼做,那就弄複雜了。本題如果僅考慮轉移,只需要比較s[i]和s[j]就行,因爲子問題都已經處理完了,相當於已經是迴文的了。你只需要考慮變化s[i]或者s[j]使得[i,j]變成迴文就行。s[i]==s[j],dp[i][j] = dp[i + 1][j - 1]s[i] != s[j], dp[i][j] = min(dp[i + 1][j] + 處理s[i]最小花費, dp[i][j - 1] + 處理s[j]的最小花費)。這個處理最小花費可以預處理出來,cost[op] = min(x,y)。試想一下,如果[i,j]階段就去考慮如何複雜的實現,那就沒法做了。同時記住不要越級更新,比如[i+2,j]假如也可以通過更新使得[i,j]更新成爲迴文串,但是實際上在[i+1,j]中會把這個算進去,我們抽象地劃分集合進行轉移就是爲了讓集合間轉移更清晰,如果去越級去考慮,雖然不影響完全覆蓋,但是會讓轉移問題變得十分複雜,背離了我們抽象操作的初心。

  • 整理思路: 區間dp, dp[i][j] 表示[i,j]更新爲迴文串的最小花費。預處理出cost[op-'a'] = min(x,y)。轉移方程:s[i]==s[j]: dp[i][j] = dp[i+1][j-1]s[i] !=s[j]: dp[i][j] = min(dp[i+1][j]+cost[s[i]-'a'], dp[i][j-1]+cost[s[j]-'a'])。結果dp[0][n]

代碼:

# 省略頭文件
int main() {
    // freopen("../temp.in", "r", stdin);
    // freopen("../temp.out", "w", stdout);
    
    int n, m; cin >> n >> m;
    string s; cin >> s;
    vector<int> cost(26, 0);
    for(int i = 0; i < n; ++ i) {
        char op; cin >> op;
        int x, y; cin >> x >> y;
        cost[op - 'a'] = min(x, y);
    }

    vector<vector<int> > dp(m, vector<int>(m, 0));
    for(int len = 2; len <= m; ++ len) {
        for(int i = 0; i + len - 1 < m; ++ i) {
            int j = i + len - 1;
            if(s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1];
            else dp[i][j] = min(dp[i + 1][j] + cost[s[i] - 'a'], dp[i][j - 1] + cost[s[j] - 'a']);
        }
    }

    cout << dp[0][m - 1] << endl;
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章