1. 程式人生 > 其它 >【LeetCode 刷題筆記】 02 LeetCode 熱題 HOT 100 中的動態規劃

【LeetCode 刷題筆記】 02 LeetCode 熱題 HOT 100 中的動態規劃

部落格都快被荒廢了,今天才突然想起還有這麼個東西,決定將自己最近刷到的LeetCode熱題榜單中的動態規劃的內容總結一下,與大家分享。

乾貨可能比較多。本人有點懶,就不一題一題地發部落格了XD

LeetCode 演算法100題總結-dp

To be yourself in a world that is constantly trying to make you something else is the greatest accomplishment.

——Ralph W. Emerson

動態規劃

由於是動態規劃專題,所以有的題目可能有更好的解法沒有被寫在裡面,這篇部落格只聚焦於動態規劃解法。

有幾種常見的一維dp陣列,第一種是記錄前 i 個元素【中】(最大/最長的)長度,第二種是記錄以第 i 個元素【為結尾】的(最長)長度,第三種是記錄前 i 個元素在某種性質上的 true 和 false

動態規劃有單狀態和多狀態的

多狀態dp要注意同時更新的問題,即要先將所有狀態儲存在臨時變數中,再用臨時變數對所有狀態進行更新

多狀態:

152題(最大和最小)

5. Longest Palindromic Substring

\(dp[i][j]\) 表示從下標 \(i\) 到下標 \(j\) 的子串是否是迴文串

關鍵:

  1. 遞推式?
  2. 二維陣列的斜向遍歷如何寫?(可以記模板,實在不行就把下標寫出來然後找規律)

時間和空間:\(O(n^2)\)

程式碼:

class Solution {
public:
    string longestPalindrome(string s) {
        int l = s.size();
        if (l == 0 || l == 1)
            return s;
        vector<vector<bool>> dp(l, vector<bool>(l, false));
        for (int i = 0; i < l; i++) {
            dp[i][i] = true;
            if (i > 0)
                dp[i][i-1] = true;
        }
        int i, j;
        int start = 0, end = 0;
        // 關鍵!!!
        for (int k = 1; k < l; k++) {
            for (i = 0; i < l-k; i++) {
                j = i+k;
                if (dp[i+1][j-1] && s[i] == s[j]) {
                    dp[i][j] = true;
                    start = i;
                    end = j;
                }
                else
                    dp[i][j] = false;
            }
        }
        return s.substr(start, end-start+1);
    }
};

10. Regular Expression Matching

  • 正則表示式匹配,應該要用到前面的狀態來推出當前狀態會比較省時間 ---> 動態規劃
  • 要理解題意,模板 ba* 可以匹配字串 b

遞推式(分3種情況來考慮):

  1. 模板的第 \(j\) 個元素是字母
  2. 是 '.'
  3. 是 '*'(關鍵)

時間和空間:\(O(mn)\)

程式碼:

class Solution {
public:
    inline bool is_letter(const char &c) {
        return 'a' <= c && c <= 'z';
    }

    bool isMatch(string s, string p) {
        vector<vector<bool>> dp(32, vector<bool>(32, false));
        dp[0][0] = true;
        int m = s.size(), n = p.size();
        for (int i = 1; i <= m; i++)
            dp[i][0] = false;
        for (int i = 0; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (is_letter(p[j-1])) {
                    if (i > 0 && s[i-1] == p[j-1])
                        dp[i][j] = dp[i-1][j-1];
                }
                else if (p[j-1] == '.') {
                    if (i > 0)
                        dp[i][j] = dp[i-1][j-1];
                }
                else {
                    if (i > 0 && (s[i-1] == p[j-2] || p[j-2] == '.'))
                        dp[i][j] = (dp[i][j-2] || dp[i-1][j]);
                    else
                        dp[i][j] = dp[i][j-2];
                }
            }
        }
        return dp[m][n];
    }
};

22. Generate Parentheses

這個遞推式確實比較難想。

怎麼說呢,見一題積累一題吧

時間和空間:\(O(\frac{4^n}{\sqrt{n}})\)

程式碼:

class Solution {
public:
    vector<string> generateParenthesis(int n) {
        vector<vector<string>> dp(n+1);
        dp[0] = { "" };
        dp[1] = { "()" };
        string temp;
        for (int i = 2; i <= n; i++) {
            for (int j = 0; j <= i-1; j++) {
                for (string &x : dp[j]) {
                    for (string &y : dp[i-1-j]) {
                        dp[i].push_back("(" + x + ")" + y);
                    }
                }
            }
        }
        return dp[n];
    }
};

32. Longest Valid Parentheses

一開始以為要用一個二維陣列記錄 i 到 j 的子串是否是有效括號,但實際上解決不了問題。想複雜了,應該先考慮一維陣列能否解決問題。

正確做法:dp陣列記錄以第 i 個字元結尾的最長的有效括號的長度。

欲求dp[i],首先要看s[i](實際下標可能有微小變化),再看s[i-1],進行分類討論。關鍵是以 )) 結尾的情況要思考清楚。

已經見過幾種一維dp陣列了,第一種是記錄前 i 個元素中最大/最長的長度,第二種是記錄以第 i 個元素為結尾的最長長度,第三種是記錄前 i 個元素的 true 和 false

時間和空間:\(O(n)\)

程式碼:

class Solution {
public:
    int longestValidParentheses(string s) {
        int l = s.size();
        if (l == 0)
            return 0;
        vector<int> dp(l+1, 0);
        dp[0] = dp[1] = 0;
        int ans = 0;
        for (int i = 2; i <= l; i++) {
            if (s[i-1] == '(')
                dp[i] = 0;
            else {
                if (s[i-2] == '(')
                    dp[i] = dp[i-2] + 2;
                else {
                    if (i-dp[i-1]-2 >= 0 && s[i-dp[i-1]-2] == '(')
                        dp[i] = dp[i-dp[i-1]-2] + dp[i-1] + 2;
                    else
                        dp[i] = 0;
                }
            }
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};

42. Trapping Rain Water

利用 dp 陣列求出每個位置左邊最高的柱子高度和右邊最高的柱子高度(非常簡單),兩者的最小值減去該位置的高度即為該位置能接收的雨水量,再掃描一遍整個陣列進行求和即可。

時間和空間:\(O(n)\)

程式碼:

class Solution {
public:
    int trap(vector<int>& height) {
        int l = height.size();
        if (l <= 2)
            return 0;
        vector<int> leftmax(l, 0), rightmax(l, 0);
        leftmax[0] = rightmax[l-1] = 0;
        for (int i = 1; i < l; i++) {
            leftmax[i] = max(height[i-1], leftmax[i-1]);
            rightmax[l-i-1] = max(height[l-i], rightmax[l-i]);
        }
        int res = 0, temp = 0;
        for (int k = 1; k < l-1; k++) {
            temp = min(leftmax[k], rightmax[k]) - height[k];
            res += (temp > 0 ? temp : 0);
        }
        return res;
    }
};

53. Maximum Subarray

沒什麼好說的了,最經典的題目了

這裡的dp陣列記錄的是以i為結尾的陣列的最大和,求dp[l-1]即可(可優化空間至 \(O(1)\)

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int cur_max = nums[0];
        int ans = cur_max;
        for (int i = 1; i < nums.size(); i++) {
            if (cur_max >= 0)
                cur_max += nums[i];
            else
                cur_max = nums[i];
            ans = max(ans, cur_max);
        }
        return ans;
    }
};

55. Jump Game

dp[i] 表示從第i個下標數字開始能否抵達終點

dp[l-1] = true

從後往前遍歷,如果當前數字後面的範圍中有一個為true,則dp[i] = true

時間:\(O(n^2)\)

空間:\(O(n)\)

程式碼:

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int l = nums.size();
        vector<int> dp(l, false);
        dp[l-1] = true;
        for (int i = l-2; i >= 0; i--) {
            for (int j = i+1; j <= min(l-1, i+nums[i]); j++) {
                if (dp[j]) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[0];
    }
};

62. Unique Paths

1) DP

dp[i] [j] 表示到 (i, j) 的道路總數

\[dp[i][j]=dp[i-1][j]+dp[i][j-1] \]

(可優化空間)

時間:\(O(n^2)\)

空間:\(O(n)\)

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<int> dp(n, 1);
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[j] += dp[j-1];
            }
        }
        return dp[n-1];
    }
};

2) 數學方法

組合問題,一下子出結果

時間:\(O(n)\)

空間:\(O(1)\)

class Solution {
public:
    int uniquePaths(int m, int n) {
        long long ans = 1;
        for (int x = n, y = 1; y < m; ++x, ++y) {
            ans = ans * x / y;
        }
        return ans;
    }
};

64. Minimum Path Sum

dp[i] [j] 表示從(i,j)抵達終點的最小代價,結果即為dp[0] [0]

\[dp[i][j]=grid[i][j]+min(dp[i+1][j], dp[i][j+1]) \]
  • 可優化空間複雜度
  • 注意邊界條件的取值

時間:\(O(n^2)\)

空間:\(O(n)\)

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        vector<int> dp(n, 0);
        int i, j;
        dp[n-1] = grid[m-1][n-1];
        for (j = n-2; j >= 0; j--)
            dp[j] = dp[j+1] + grid[m-1][j];
        for (i = m-2; i >= 0; i--) {
            dp[n-1] += grid[i][n-1];
            for (j = n-2; j >= 0; j--)
                dp[j] = grid[i][j] + min(dp[j+1], dp[j]);
        }
        return dp[0];
    }
};

70. Climbing Stairs

1) DP

斐波那契數列,不解釋了

時間:\(O(n)\)

空間:\(O(1)\)

2) 矩陣快速冪

快速冪可以降低時間複雜度

時間:\(O(\log{n})\)

空間:\(O(1)\)

Matrix qpow(Matrix a, int n) {
    Matrix ans(1, 0, 0, 1);
    while (n) {
        if (n & 1)
            ans = ans * a;
        a = a * a;
        n >>= 1;
    }
    return ans;
}

long long climbStairs(int n) {
    Matrix a(1, 1, 1, 0);
    Matrix B = qpow(a, n);
    return B.a21 + B.a22;
}

3) 數學方法

時間和空間:\(O(1)\)

72. Edit Distance

dp[i] [j]為字串1的前i個字元和字串2的前j個字元之間的編輯距離

遞推關係:抓住word1[i-1]和word2[j-1]是否相等來進行分類討論!

時間和空間:\(O(mn)\)

空間可以優化為 \(O(\max\{m, n\})\)

class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.size(), n = word2.size();
        vector<vector<int> > dp(m+1, vector<int>(n+1, 0));
        for (int i = 0; i <= m; i++)
            dp[i][0] = i;
        for (int j = 0; j <= n; j++)
            dp[0][j] = j;
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (word1[i-1] == word2[j-1])
                    dp[i][j] = dp[i-1][j-1];
                else {
                    dp[i][j] = 1 + min(dp[i-1][j], min(dp[i][j-1], dp[i-1][j-1]));
                }
            }
        }
        return dp[m][n];
    }
};

85. Maximal Rectangle

此題從84題進化而來,一定要先弄懂84題的方法!

將每行的高度累加,就能看成很多個84題疊加起來了

DP方法維護每個下標左側第一個比自己小的高度的下標以及右側第一個比自己小的高度的下標

利用boundary變數,在從一行進入到下一行的時候根據當前數字是1還是0來動態維護leftlessmin和rightlessmin陣列

時間:\(O(mn)\)

空間:\(O(n)\)

class Solution {
public:
    int maximalRectangle(vector<vector<char>>& matrix) {
        int m = matrix.size(), n = matrix[0].size();
        vector<int> height(n, 0), leftless(n, -1), rightless(n, n);
        int res = 0, area = 0;
        int boundary = 0;
        for (int i = 0; i < m; i++) {
            // 累加高度
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '0')
                    height[j] = 0;
                else
                    ++height[j];
            }

            // 更新
            boundary = -1;
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '0') {
                    leftless[j] = -1;
                    boundary = j;
                }
                else {
                    leftless[j] = max(leftless[j], boundary);
                }
            }
            boundary = n;
            for (int j = n-1; j >= 0; j--) {
                if (matrix[i][j] == '0') {
                    rightless[j] = n;
                    boundary = j;
                }
                else {
                    rightless[j] = min(rightless[j], boundary);
                }
            }

            // 計算每個下標處的最大面積
            for (int j = 0; j < n; j++) {
                area = height[j] * (rightless[j]-leftless[j]-1);
                res = max(res, area);
            }
        }
        return res;
    }
};

96. Unique Binary Search Trees

dp[i]代表從1到i這i個節點能構成多少棵二叉樹

實際上只要把每個節點都當成根節點一次再相加就能夠遞推了

時間:\(O(n^2)\)

空間:\(O(n)\)

class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n+1, 0);
        dp[0] = dp[1] = 1;
        for (int i = 2; i <= n; i++) {
            dp[i] = 0;
            for (int j = 0; j < i; j++) {
                dp[i] += (dp[j] * dp[i-j-1]);
            }
        }
        return dp[n];
    }
};

121. Best Time to Buy and Sell Stock

dp[i] 表示前 i 天所能得到的最大收益

則根據第 i 天的情況分類討論

  • 空間複雜度可以優化

時間:\(O(n)\)

空間:\(O(1)\)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int minprice = prices[0], maxprofit = 0;
        for (int i = 1; i < prices.size(); i++) {
            if (prices[i] > minprice)
                maxprofit = max(maxprofit, prices[i]-minprice);
            else
                minprice = prices[i];
        }
        return maxprofit;
    }
};

122(*). Best Time to Buy and Sell Stock II

兩狀態DP

dp0[i] 表示第 i 天不持有股票的情況下,前 i 天的最大收益

dp1[i] 表示第 i 天持有股票的情況下,前 i 天的最大收益

最後返回 dp0[n-1] 即可

時間和空間:\(O(n)\)

空間複雜度可以優化為 \(O(1)\)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int yesmax = -prices[0], nomax = 0;
        int ty = 0, tn = 0;
        for (int i = 1; i < prices.size(); i++) {
            ty = yesmax;
            tn = nomax;
            yesmax = max(ty, tn-prices[i]);
            nomax = max(tn, ty+prices[i]);
        }
        return nomax;
    }
};

123(*). Best Time to Buy and Sell Stock III

188(*). Best Time to Buy and Sell Stock IV

dp[i] [j] 表示前 j 天完成了 (i+1) 次操作(包含買賣)的情況下的最大收益

空間可以優化為 \(O(1)\)

答案顯然為 dp[1] [l-1],dp[3] [l-1]... 的最大值

139. Word Break

dp[i]表示字串s[0...i]能否被分割

則dp[i]可以由前(i-1)個狀態來推斷出來

時間:\(O(n^2)\)

空間:\(O(n)\)

  • 剪枝
    1. 統計出詞典中最長和最短的詞的長度
    2. 只記錄能被分割的下標

剪枝程式碼:

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> str_set;
        int minlength = 0, maxlength = 0;
        int l = 0;
        for (const auto str: wordDict) {
            if (str_set.count(str))
                continue;
            str_set.insert(str);
            l = str.size();
            minlength = min(minlength, l);
            maxlength = max(maxlength, l);
        }
        
        int ls = s.size();
        vector<int> idx_arr;
        idx_arr.push_back(-1);
        for (int i = 0; i < ls; i++) {
            for (int j = 0; j < idx_arr.size(); j++) {
                if (i-idx_arr[j] > maxlength && j == idx_arr.size()-1)
                    return false;
                if (i-idx_arr[j] < minlength || i-idx_arr[j] > maxlength)
                    continue;
                if (str_set.count(s.substr(idx_arr[j]+1, i-idx_arr[j]))) {
                    idx_arr.push_back(i);
                    break;
                }
            }
        }
        
        if (idx_arr[idx_arr.size()-1] == ls-1)
            return true;
        else
            return false;
    }
};

一般的dp陣列程式碼:

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        if (s.size() == 0)
            return true;
        if (wordDict.size() == 0)
            return false;
        int minlen, maxlen;
        minlen = maxlen = wordDict[0].size();
        unordered_set<string> pool;
        for (const auto str: wordDict) {
            minlen = min(minlen, (int)str.size());
            maxlen = max(maxlen, (int)str.size());
            pool.insert(str);
        }
        int l = s.size();
        if (l < minlen)
            return false;
        vector<bool> dp(l+1, false);  // dp[i]表示s[0...i-1]是否能被拆分
        dp[0] = true;
        int i, j;
        int begin;
        for (i = 1; i <= l; i++) {
            if (i < minlen) {
                dp[i] = false;
                continue;
            }
            begin = max(0, i-maxlen);
            for (j = i-minlen; j >= begin; j--) {
                if (pool.count(s.substr(j, i-j)) && dp[j]) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[l];
    }
};

152. Maximum Product Subarray

多狀態dp(此處為2狀態,最大和最小)

類似於53題最大連續子序列和

由於有負數出現,這題要維護2個狀態,一個是最大狀態,一個是最小狀態

dp[i]表示以i結尾的最大/最小子陣列乘積

時間和空間:\(O(n)\)

空間可以優化為 \(O(1)\)

未優化程式碼:

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int l = nums.size();
        vector<int> maxdp(l, 0), mindp(l, 0);
        maxdp[0] = mindp[0] = nums[0];
        int ans = maxdp[0];
        for (int i = 1; i < l; i++) {
            if (nums[i] == 0)
                maxdp[i] = mindp[i] = 0;
            else if (nums[i] > 0) {
                maxdp[i] = max(nums[i], maxdp[i-1] * nums[i]);
                mindp[i] = min(nums[i], mindp[i-1] * nums[i]);
            }
            else {
                maxdp[i] = max(nums[i], mindp[i-1] * nums[i]);
                mindp[i] = min(nums[i], maxdp[i-1] * nums[i]);
            }
            ans = max(ans, maxdp[i]);
        }
        return ans;
    }
};

官方程式碼:

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int maxvalue = nums[0], minvalue = nums[0];
        int maxres = maxvalue;
        int mx, mn;
        for (int i = 1; i < nums.size(); i++) {
            mx = maxvalue;
            mn = minvalue;
            maxvalue = max(mx * nums[i], max(mn * nums[i], nums[i]));
            minvalue = min(mx * nums[i], min(mn * nums[i], nums[i]));
            maxres = max(maxres, maxvalue);
        }
        return maxres;
    }
};

188(*). Best Time to Buy and Sell Stock IV

見123題

198. House Robber

不解釋了,經典題之一

dp[i]:前 i 個……最大

時間和空間:\(O(n)\)

空間可以優化為 \(O(1)\)

class Solution {
public:
    int rob(vector<int>& nums) {
        int l = nums.size();
        if (l == 1)
            return nums[0];
        vector<int> maxvalue(l);
        maxvalue[0] = nums[0];
        maxvalue[1] = max(nums[0], nums[1]);
        for (int i = 2; i < l; i++)
            maxvalue[i] = max(maxvalue[i - 1], maxvalue[i - 2] + nums[i]);
        return maxvalue[l - 1];
    }
};

221. Maximal Square

思路比較新奇

dp[i] [j] 表示以 (i, j) 為右下角的最大矩形的邊長

if matrix[i] [j] == '1':

\[dp[i][j]=1+\min\{dp[i-1][j],dp[i][j-1],dp[i-1][j-1]\} \]

else:

\[dp[i][j]=0 \]

時間和空間:\(O(mn)\)

空間可優化為 \(O(n)\)

未優化程式碼:

class Solution {
public:
    int maximalSquare(vector<vector<char>>& matrix) {
        int m = matrix.size(), n = matrix[0].size();
        vector<vector<int> > dp(m+1, vector<int>(n+1, 0));
        int res = 0;
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (matrix[i-1][j-1] == '1') {
                    dp[i][j] = 1 + min(dp[i-1][j-1], min(dp[i][j-1], dp[i-1][j]));
                    res = max(res, dp[i][j]);
                }
            }
        }
        return res * res;
    }
};

優化程式碼:

class Solution {
public:
    int maximalSquare(vector<vector<char>>& matrix) {
        int m = matrix.size(), n = matrix[0].size();
        vector<vector<int> > dp(2, vector<int>(n+1, 0));
        int res = 0;
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (matrix[i-1][j-1] == '1') {
                    dp[i%2][j] = 1 + min(dp[(i+1)%2][j-1], min(dp[i%2][j-1], dp[(i+1)%2][j]));
                    res = max(res, dp[i%2][j]);
                }
                else
                    dp[i%2][j] = 0;
            }
        }
        return res * res;
    }
};

279. Perfect Squares

dp[i] 表示數字 i 最少表示為x個數字的平方和

正向傳遞即可

時間:\(O(n \sqrt{n})\)

空間:\(O(n)\)

class Solution {
public:
    int numSquares(int n) {
        vector<int> dp(n+1);  // dp[0] = 0
        for (int i = 1; i <= n; i++) {
            dp[i] = 1 + dp[i-1];
            for (int j = 2; j*j <= i; j++) {
                dp[i] = min(dp[i], 1+dp[i-j*j]);
            }
        }
        return dp[n];
    }
};

300. Longest Increasing Subsequence

dp[i] 表示以 i 結尾的最長上升子序列的長度

要得到 dp[i] ,只需要從0到i-1逐個比較即可

時間:\(O(n^2)\)

空間:\(O(n)\)

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int l = nums.size();
        vector<int> dp(l, 1);
        dp[0] = 1;
        int res = 1;
        for (int i = 1; i < l; i++) {
            dp[i] = 1;
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = max(dp[i], dp[j] + 1);
                    res = max(res, dp[i]);
                }
            }
        }
        return res;
    }
};

309. Best Time to Buy and Sell Stock with Cooldown

三狀態同時 dp

多狀態dp要注意同時更新的問題,即要先將所有狀態儲存在臨時變數中,再用臨時變數對所有狀態進行更新

用一個二維陣列 dp[3] [l] 來進行維護

dp[0] [i] 表示第 i 天持有股票的情況下,前 i 天的最大收益

dp[1] [i] 表示第 i 天不持有股票且不是冷凍期的情況下,前 i 天的最大收益

dp[2] [i] 表示第 i 天不持有股票且是冷凍期的情況下,前 i 天的最大收益

時間和空間:\(O(n)\)

空間複雜度可以優化為 \(O(1)\)

未優化:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int l = prices.size();
        vector<vector<int> > dp(3, vector<int>(l, 0));
        dp[0][0] = -prices[0];
        dp[1][0] = dp[2][0] = 0;
        for (int j = 1; j < l; j++) {
            dp[0][j] = max(dp[0][j-1], dp[1][j-1]-prices[j]);
            dp[1][j] = max(dp[1][j-1], dp[2][j-1]);
            dp[2][j] = dp[0][j-1] + prices[j];
        }
        return max(dp[1][l-1], dp[2][l-1]);
    }
};

優化:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int l = prices.size();
        vector<int> dp(3);
        dp[0] = -prices[0];
        dp[1] = dp[2] = 0;
        vector<int> t(3, 0);
        for (int i = 1; i < l; i++) {
            for (int k = 0; k < 3; k++)
                t[k] = dp[k];
            dp[0] = max(t[0], t[1]-prices[i]);
            dp[1] = max(t[1], t[2]);
            dp[2] = t[0] + prices[i];
        }
        return max(dp[1], dp[2]);
    }
};

312. Burst Balloons

dp[i] [j] 表示在開區間 (i, j) 中填滿數字所能得到的最大值

遞推式:

需要反向思考:區間中最後一個被戳破的氣球是哪個?

時間:\(O(n^2 \times n)=O(n^3)\)

空間:\(O(n^2)\)

自底向上進行dp:

class Solution {
public:
    int maxCoins(vector<int>& nums) {
        int l = nums.size();
        vector<int> new_nums(l+2, 1);
        for (int i = 1; i <= l; i++)
            new_nums[i] = nums[i-1];
        vector<vector<int> > dp(l+2, vector<int>(l+2, 0));
        int i = 0, j = 0, k = 0;
        int adder = 0;
        for (int u = 2; u < l+2; u++) {
            for (i = 0, j = i+u; j < l+2; i++, j++) {
                for (k = i+1; k <= j-1; k++) {
                    adder = new_nums[i] * new_nums[k] * new_nums[j];
                    dp[i][j] = max(dp[i][j], adder + dp[i][k] + dp[k][j]);
                }
            }
        }
        return dp[0][l+1];
    }
};

自頂向下進行記憶化搜尋:

class Solution {
public:
    int solve(vector<vector<int> > &dp, vector<int> &a, int left, int right) {
        if (left >= right-1)
            return 0;
        if (dp[left][right] != -1)
            return dp[left][right];
        int adder = 0;
        for (int k = left+1; k <= right-1; k++) {
            adder = a[left] * a[k] * a[right];
            dp[left][right] = max(dp[left][right], solve(dp, a, left, k)+adder+solve(dp, a, k, right));
        }
        return dp[left][right];
    }

    int maxCoins(vector<int>& nums) {
        int l = nums.size();
        vector<vector<int> > dp(l+2, vector<int>(l+2, -1));
        vector<int> new_nums(l+2, 1);
        for (int i = 0; i < l; i++)
            new_nums[i+1] = nums[i];
        return solve(dp, new_nums, 0, l+1);
    }
};

322. Coin Change

完全揹包問題的變式,要求揹包被恰好填滿,且所用物品的總數最少

dp[i] [j] 表示放入前 i 件物品的情況下,填滿容量為 j 的揹包所需要的最少硬幣

dp[m-1] [amount] 即為所求結果

時間和空間:\(O(m \cdot amount)\)

空間可優化\(O(amount)\)

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount+1, -1);
        dp[0] = 0;
        int m = coins.size();
        int i, j;
        for (i = 0; i < m; i++) {
            for (j = coins[i]; j <= amount; j++) {
                if (dp[j-coins[i]] != -1) {
                    if (dp[j] == -1)
                        dp[j] = dp[j-coins[i]] + 1;
                    else {
                        dp[j] = min(dp[j], dp[j-coins[i]] + 1);
                    }
                }
            }
        }
        return dp[amount];
    }
};

337. House Robber III

對某棵子樹維護兩個值,一個是選取了根節點後的最大值,另一個是不選取根節點的最大值

最後的結果是根節點的兩個值取最大

時間和空間:\(O(n)\)

空間可以被優化為 \(O(1)\)

雜湊表:

class Solution {
public:
    void robhelp(unordered_map<TreeNode *, int> &picked, unordered_map<TreeNode *, int> &unpicked, TreeNode* subroot) {
        if (subroot == nullptr)
            return;
        robhelp(picked, unpicked, subroot->left);
        robhelp(picked, unpicked, subroot->right);
        picked[subroot] = subroot->val + unpicked[subroot->left] + unpicked[subroot->right];
        unpicked[subroot] = max(picked[subroot->left], unpicked[subroot->left]) + max(picked[subroot->right], unpicked[subroot->right]);
    }

    int rob(TreeNode* root) {
        unordered_map<TreeNode *, int> picked, unpicked;
        picked[nullptr] = unpicked[nullptr] = 0;
        robhelp(picked, unpicked, root);
        return max(picked[root], unpicked[root]);
    }
};

使用struct進行空間優化(由於沒有采用紅黑樹實現的unordered_map,所以時間也有所優化):

class Solution {
public:
    struct Result {
        int choosen, unchoosen;
    };

    Result robhelp(TreeNode *subroot) {
        if (!subroot)
            return {0, 0};
        Result lbranch = robhelp(subroot->left);
        Result rbranch = robhelp(subroot->right);
        Result ans;
        ans.choosen = subroot->val + lbranch.unchoosen + rbranch.unchoosen;
        ans.unchoosen = max(lbranch.choosen, lbranch.unchoosen) + max(rbranch.choosen, rbranch.unchoosen);
        return ans;
    }

    int rob(TreeNode* root) {
        Result res = robhelp(root);
        return max(res.choosen, res.unchoosen);
    }
};

338. Counting Bits

第 i 個數字包含的 1 相當於最後一位的1的個數加上其右移1位得到的數字的1的個數之和

時間和空間:\(O(n)\)

class Solution {
public:
    vector<int> countBits(int n) {
        vector<int> dp(n+1, 0);
        dp[0] = 0;
        for (int i = 1; i <= n; i++)
            dp[i] = (i&1) + dp[i>>1];
        return dp;
    }
};

416. Partition Equal Subset Sum

0-1 揹包問題的變式

dp[i] [j] 表示前i個物體(取或不取)能否恰好填滿容量為 j 的揹包

時間和空間:\(O(l \cdot half)\)

空間可優化\(O(half)\)

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int l = nums.size();
        if (l == 1)
            return false;
        int sum = 0;
        for (int i = 0; i < l; i++)
            sum += nums[i];
        if (sum & 1)
            return false;
        int half = sum / 2, t = 0;
        vector<bool> dp(half+1, false);
        dp[0] = true;
        for (int i = 0; i < l; i++) {
            for (int j = half; j >= nums[i]; j--)
                dp[j] = (dp[j] || dp[j-nums[i]]);
        }
        return dp[half];
    }
};

494. Target Sum

法1:

dp[i] [j] 表示使用了前 i 個物品後“容量” j 的可能性總量(以1000為對稱中心,即1000代表0)

時間和空間:\(O(l \cdot total)\)

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int l = nums.size();
        vector<vector<int> > dp(l+1, vector<int>(2001, 0));
        dp[0][1000] = 1;
        for (int i = 1; i <= l; i++) {
            for (int j = 0; j <= 2000; j++) {
                if (dp[i-1][j] > 0) {
                    dp[i][j-nums[i-1]] += dp[i-1][j];
                    dp[i][j+nums[i-1]] += dp[i-1][j];
                }
            }
        }
        return dp[l][1000+target];
    }
};

法2:

變為0-1揹包問題的變式了

時間和空間:\(O(l \cdot neg)\)

空間可優化\(O(neg)\)

class Solution {
public:
   int findTargetSumWays(vector<int>& nums, int target) {
       int l = nums.size();
       int sum = 0;
       for (int i = 0; i < l; i++) {
           sum += nums[i];
       }
       if (sum - target < 0 || (sum-target)%2 == 1)
           return 0;
       int neg = (sum-target) / 2;
       vector<int> dp(neg+1, 0);
       dp[0] = 1;
       for (int i = 0; i < l; i++) {
           for (int j = neg; j >= nums[i]; j--) {
               dp[j] += dp[j-nums[i]];
           }
       }
       return dp[neg];
   }
};

647. Palindromic Substrings

類似於第5題,這裡不講了