动态规划相关题目总结

221.最大正方形

设dp[i][j]为以点(i, j)为右下角的正方形最大边长,多画画图模拟模拟可以发现递推式dp[i][j] = min(dp[i][j-1], dp[i-1][j-1], dp[i-1][j])+1。

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

53.最大子数组和

经典dp,设dp[i]为以nums[i]结尾的最大子数组和,考虑是否与nums[i-1]结尾的最大子数组结合,如果dp[i-1] < 0那肯定不结合,反之应该结合。

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

300.最长递增子序列

两种做法,第一种就是普通的dp,设dp[i]为以nums[i]结尾的最长递增子序列长度,然后向前枚举子序列前一个数字,时间复杂度是O(n^2)。第二种做法利用二分查找,时间复杂度为O(nlogn)。

//O(n^2)写法
class Solution {
public:
    int lengthOfLIS(vector& nums) {
        vector dp(nums.size());
        int res = 0;
        for(int i = 0; i < nums.size(); 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;
    }
};
//O(nlogn)写法
class Solution {
public:
    int lengthOfLIS(vector& nums) {
        vector a(nums.size()+1, 0x3f3f3f3f);
        for(int i = 0; i < nums.size(); i++){
            int idx = lower_bound(a.begin(), a.end(), nums[i])-a.begin();
            a[idx] = nums[i];
        }
        return lower_bound(a.begin(), a.end(), 0x3f3f3f3f)-a.begin();
    }
};

32.最长有效括号

设dp[i]为以第i个括号结尾的最长有效括号串,如果第i个括号是左括号那dp[i]肯定是0,如果如果第i个括号是右括号那需要根据上一个括号分类讨论,若s[i-1]是左括号,那s[i-1]和s[i]正好凑成一对,此时dp[i] = dp[i-2]+2,若s[i-1]是右括号,那dp[i-1]就是该右括号最长有效括号长度,i-dp[i-1]就是该有效括号串的左端点,i-dp[i-1]-1的位置如果是个右括号,那它不能与i位置的右括号匹配,此时dp[i] = 0,如果i-dp[i-1]-1的位置是左括号,那它可以与i位置的右括号匹配,此时dp[i] = dp[i-1]+2+dp[i-dp[i-1]-2]。

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

322.零钱兑换

设dp[i][j]表示只使用前i种货币,恰好凑到j元所需要的最少货币数。状态转移的时候考虑是否使用第i种货币,不使用的话就是dp[i-1][j],使用的话就是dp[i][j-coins[i]]+1,二者取最小值就行了。

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

1143.最长公共子序列

设dp[i][j]表示考虑串1前i位以及串2前j位的最长公共子序列,如果串1的第i位和串2的第j位恰好相等,那就dp[i][j] = dp[i-1][j-1]+1,否则dp[i][j] = max(dp[i-1][j], dp[i][j-1])。

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int n = text1.size(), m = text2.size();
        vector> dp(n, vector(m));
        bool flag = false;
        for(int i = 0; i < m; i++){
            if(text1[0] == text2[i]) flag = true;
            if(flag) dp[0][i] = 1;
        }
        flag = false;
        for(int i = 0; i < n; i++){
            if(text1[i] == text2[0]) flag = true;
            if(flag) dp[i][0] = 1;
        }
        for(int i = 1; i < n; i++)
            for(int j = 1; j < m; j++){
                if(text1[i] == text2[j]) dp[i][j] = dp[i-1][j-1]+1;
                dp[i][j] = max(dp[i][j], dp[i-1][j]);
                dp[i][j] = max(dp[i][j], dp[i][j-1]);
            }
        return dp[n-1][m-1];
    }
};

121.买卖股票的最佳时机

维护一个前缀最小值或者后缀最大值就行了。

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

72.编辑距离

设dp[i][j]表示将串1的前i位变为串2前j位所需最少操作数,状态转移就是考虑最后一次执行的是什么操作,如果最后一次是删除,那就对应dp[i][j] = dp[i-1][j]+1,如果最后一次是添加,那就对应dp[i][j] = dp[i][j-1]+1,如果最后一次是修改,那就是dp[i][j] = dp[i-1][j-1]+1,当然如果本身第i位和第j位就相等,那dp[i][j] = dp[i-1][j-1],这四种情况取最小值。

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

70.爬楼梯

经典入门dp题目。

class Solution {
public:
    int climbStairs(int n) {
        vector dp(n+5);
        dp[1] = 1, dp[2] = 2;
        for(int i = 3; i <= n; i++)
            dp[i] = dp[i-1]+dp[i-2];
        return dp[n];
    }
};

122.买卖股票的最佳时机 II

设dp[i][0]表示在第i天手上没有股票时的钱,dp[i][1]表示在第i天手上有股票时的钱,状态转移的话如果当天没有股票那说明可能是今天卖出去了,也可能是昨天手上就没股票继承过来了,所以dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]),然后如果当天有股票那说明可能是今天刚买,也可能是昨天手上就有股票继承过来了,所以dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])。

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

198.打家劫舍

设dp[i]表示前i个位置拿到的钱,状态转移的时候考虑是否拿第i个位置的钱,如果拿的话那dp[i] = dp[i-2]+nums[i],如果不拿的话dp[i] = dp[i-1],二者取一个最大值就行了。

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

139.单词拆分

一开始想的是把单词都存到哈希表里,然后遍历字符串,过程中把字符累加到临时字符串中,只要临时字符串在哈希表中出现就置空,看最后临时字符串是否为空。但是这个方法没法通过样例s = "aaaaaaa", wordDict = ["aaa", "aaaa"]。

设dp[i]表示前i个字符能否凑出来,状态转移就是枚举最后一个单词,时间复杂度接近O(n^2)。

class Solution {
public:
    bool wordBreak(string s, vector& wordDict) {
        s = " "+s;
        int n = s.size();
        vector dp(n);
        dp[0] = true;
        for(int i = 0; i < n; i++){
            for(int j = 0; j < wordDict.size(); j++){
                if(i >= wordDict[j].size() && s.substr(i-wordDict[j].size()+1, wordDict[j].size()) == wordDict[j])
                    dp[i] = max(dp[i], dp[i-wordDict[j].size()]);

            }
        }
        return dp[n-1];
    }
};

64.最小路径和

对于(i, j)要么从(i-1, j)过来,要么从(i, j-1)过来,根据这个进行状态转移。

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

718.最长重复子数组

设dp[i][j]表示串1前i位和串2前j位最长公共子串长度,如果对应的两个位置值不同那就置0,否则dp[i][j] = dp[i-1][j-1]+1。

class Solution {
public:
    int findLength(vector& nums1, vector& nums2) {
        int n = nums1.size(), m = nums2.size();
        vector> dp(n, vector(m));
        int mx = 0;
        for(int i = 0; i < m; i++){
            dp[0][i] = nums1[0]==nums2[i];
            mx = max(mx, dp[0][i]);
        }
        for(int i = 0; i < n; i++){
            dp[i][0] = nums1[i]==nums2[0];
            mx = max(mx, dp[i][0]);
        }
        for(int i = 1; i < n; i++)
            for(int j = 1; j < m; j++){
                if(nums1[i] == nums2[j]) dp[i][j] = dp[i-1][j-1]+1;
                mx = max(mx, dp[i][j]);
            }
        return mx;
    }
};

128.最长连续序列

两种做法,第一种是用哈希+并查集,首先把数组中的数字都保存在哈希表中,之后遍历一遍数组,检查nums[i]-1是否在哈希表中,如果也存在的话那就用并查集把两个集合合并,同时可以开一个数组维护一下每个连通块大小,最后答案就是最大连通块的大小。

第二种做法是哈希+遍历,思路更简单一些,还是把数组中的数字都保存在哈希表中,然后遍历一遍原数组,当某个元素是所在连续序列中最小的一个时,直接从它的数值递增来求该序列长度。

//哈希+并查集写法
class Solution {
public:
    vector fa;
    int find(int x){
        if(fa[x] == x) return x;
        return fa[x] = find(fa[x]);
    }
    int longestConsecutive(vector& nums) {
        fa.resize(nums.size());
        vector sz(nums.size());
        unordered_map mp;
        for(int i = 0; i < nums.size(); i++)
            fa[i] = i, sz[i] = 1, mp[nums[i]] = i;
        for(int i = 0; i < nums.size(); i++){
            if(mp.count(nums[i]-1)){
                int r1 = find(mp[nums[i]]), r2 = find(mp[nums[i]-1]);
                if(r1 == r2) continue;
                if(sz[r1] > sz[r2]){
                    sz[r1] += sz[r2];
                    fa[r2] = r1;
                }
                else{
                    sz[r2] += sz[r1];
                    fa[r1] = r2;
                }
            }
        }
        int res = 0;
        for(int i = 0; i < nums.size(); i++)
            res = max(sz[i], res);
        return res;
    }
};
//哈希+遍历写法
class Solution {
public:
    int longestConsecutive(vector& nums) {
        unordered_map mp;
        for(int i = 0; i < nums.size(); i++)
            mp[nums[i]] = 1;
        int res = 0;
        for(auto x:mp){
            if(!mp.count(x.first-1)){
                int len = 0;
                while(mp.count(x.first+len)) len++;
                res = max(res, len);
            }
        }
        return res;
    }
};

62.不同路径

设dp[i][j]为从起点到(i, j)的路径数,状态转移根据上一步的位置,dp[i][j] = dp[i-1][j]+dp[i][j-1]。

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

你可能感兴趣的:(动态规划,算法)