Leetcode 1320 (一維DP)

題意

給定一個英文單詞,用兩隻手指去碼字。每隻手指移動所需要的代價爲鍵盤上兩字母之間的距離,求碼完整個單詞所需要的最小代價。

解法

最直觀的想法是,運用dp[idx][pos1][pos2]dp[idx][pos1][pos2]來表示輸入到第idxidx個字母,並且第一隻手指在pos1pos1,第二隻手指在pos2pos2時的最小代價。這時狀態轉移方程爲:

  • 移動左手:dp[idx+1][cur][other]=min(dp[idx+1][cur][other],dp[idx][prev][other]+cost);dp[idx+1][cur][other] = min(dp[idx+1][cur][other], dp[idx][prev][other] + cost);
  • 移動右手:dp[idx+1][other][cur]=min(dp[idx+1][other][cur],dp[idx][other][prev]+cost);dp[idx+1][other][cur] = min(dp[idx+1][other][cur], dp[idx][other][prev] + cost);

由於每個狀態都是從上個狀態轉移過來的,通過優化,我們可以把第一維度優化掉,變成dp[2][pos1][pos2]dp[2][pos1][pos2]這個形式。這種方法的時間複雜度沒變,爲O(wordLength2626)O(wordLength*26*26)

仔細觀察後,我們發現區分左右手的其實是沒有必要的,比如dp[2][pos1][pos2]dp[2][pos1][pos2]dp[2][pos2][pos1]dp[2][pos2][pos1]是一樣的,因爲我們可以想象隨時左右手交換,結果也不會改變。同時,一個更重要的發現是,由於我們是按順序碼字,因此每次總有一隻手停留在上一個剛碼字母的那個字母處。

有了這個信息,我們可以只記錄後手所在的位置(因爲先手一定落在剛剛敲擊完字母的那個位置),來進行動態規劃。我們用dp[idx][x]dp[idx][x]表示先手在剛剛敲擊完word[idx]word[idx],後手停留在字母xx處的最小代價。於是狀態轉移方程變爲:

  • 移動先手:dp[idx][t]=min(dp[idx][t],dp[idx1][t]+cost[a1][a2]);dp[idx][t] = min(dp[idx][t], dp[idx-1][t] + cost[a1][a2]);
  • 移動後手:dp[idx][a1]=min(dp[idx][a1],dp[idx1][t]+cost[t][a2]);dp[idx][a1] = min(dp[idx][a1], dp[idx-1][t] + cost[t][a2]);

這樣一來,時間複雜度便可優化到O(wordLength26)O(wordLength*26)。同理,我們可以優化掉第一維空間,使得最終的空間複雜度爲常數級別。

思考

對於此類題型,我們如果發現以下兩個特徵:

  • 多維DP數組填不滿或者有冗餘(左右順序交換不太重要,比如dp[pos1][pos2]dp[pos1][pos2]等同於dp[pos2][pos1]dp[pos2][pos1]
  • 多維DP數組裏不同維度的值加起來等於一個恆定值(比如pos1+pos2=idxpos1 + pos2 = idx

這種情況,可以去想着通過優化空間得到更優解。

代碼

const int INF = 0x3f3f3f3f;
class Solution {
public:
    int dp[2][27];
    vector<vector<int>> cost;
    
    pair<int, int> getPos(int x) {
        return make_pair(x/6, x%6);
    }
    
    int calculateDis(pair<int, int> pos1, pair<int, int> pos2) {
        return abs(pos1.first-pos2.first) + abs(pos1.second-pos2.second);
    }
    
    int minimumDistance(string word) {
        cost = vector<vector<int>>(30, vector<int>(30, 0));
        for (int i=0; i<26; i++)
            for (int j=0; j<=i; j++) {
                pair<int, int> pos1 = getPos(i);
                pair<int, int> pos2 = getPos(j);
                cost[i][j] = cost[j][i] = calculateDis(pos1, pos2);
            }
        memset(dp, 0x3f, sizeof(dp));
        for (int i=0; i<26; i++)
            dp[0][i] = 0;
        int len = word.length();
        for (int idx=1; idx<len; idx++) {
            int cur = idx&1, prev = !cur;
            int a1 = word[idx-1] - 'A', a2 = word[idx] - 'A';
            for (int t=0; t<26; t++) if (dp[prev][t] != INF) {
                dp[cur][t] = min(dp[cur][t], dp[prev][t] + cost[a1][a2]);
                dp[cur][a1] = min(dp[cur][a1], dp[prev][t] + cost[t][a2]);
            }
            for (int i=0; i<26; i++)
                dp[prev][i] = INF;
        }
        int ans = INF;
        for (int i=0; i<26; i++)
            ans = min(ans, dp[(len-1)&1][i]);
        return ans;
    }
};

相關題型

題目:
給定一個類似雙端隊列的數組arrarr,每次取元素只能從兩頭取,但是讀此數組中的元素可以讀任意位置上的。再給定一個數KK,以及長度爲K的mulmul數組,要求從arrarr裏取KK個元素,與這個mulmul的元素從左到右依次相乘,求乘積和最大爲多少?

思路:
這個題還是可以開一個dp[step][pos1][pos2]dp[step][pos1][pos2]的數組,代表總共取了step個數字,其中數組左邊取了pos1pos1個元素,數組右邊取了pos2pos2個元素時的最優乘積。

然而,注意到我們可以利用類似上個題空間優化的思想,用dp[step][x]表示 目前已經取了stepstep個數字,並且這stepstep個數字裏包含左端xx個數字且上一個數字取的是左手邊第x個數字的最優乘積。得到以下狀態轉移方程:

dp[step][x]=max(dp[step1][x1]+arrRight[x]mul[step],dp[step1][x]+arrLeft[stepx]mul[step]))dp[step][x] = max(dp[step-1][x-1] + arrRight[x]*mul[step], dp[step-1][x] + arrLeft[step-x]*mul[step]))

接着同理,優化掉第一維度數組,自底向上DP求解即可。這裏時間複雜度沒變,爲O(K2)O(K^2),但空間複雜度優化到了O(K)O(K)

參考文獻

https://leetcode.com/problems/minimum-distance-to-type-a-word-using-two-fingers/discuss/477652/JavaC%2B%2BPython-1D-DP-O(1)-Space

發佈了41 篇原創文章 · 獲贊 44 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章