力扣题记之动态规划

More contentLeetCode hot100@动态规划&多维动态规划

DP是由前一个状态推导而出,而贪心是在局部直接选择最优

DP五部曲:

  1. 确定dp数组以及下标的含义
  2. 确定递推公式(状态转移公式)
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

01背包模板:

for(int i = 0; i < weight.size(); i++) { //遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { //遍历背包容量(为了确保每个物品最多被添加一次,所以要从大到小去遍历)        
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); //求背包容纳的最大价值(添加,不添加)
        dp[j] += dp[j - nums[i]]; //求装满背包的不同组合数
        //cout << "i = " << i << ", j = " << j << ", dp[" << j << "] = " << dp[j] << endl;
    }
}

完全背包模板:

//常规模板
for(int i = 0; i < weight.size(); i++) { //遍历物品
    for(int j = weight[i]; j <= bagWeight; j++) { //遍历背包容量(每个物品都可以多次添加,因此从小到大去遍历)
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); //添加or不添加
        //cout << "i = " << i << ", j = " << j << ", dp[" << j << "] = " << dp[j] << endl;
    }
}
//求组合种数
for(int i = 0; i < nums.size(); i++) { //遍历物品
    for(int j = nums[i]; j <= bagWeight; j++) { //遍历背包容量
        dp[j] += dp[j - nums[i]]; //求装满背包的不同组合数
    }
}
//求排列种数    
for(int i = 0; i <= bagWeight; i++) { //先遍历背包容量
    for(int j = 0; j < nums.size(); j++) { //再遍历物品
        if(i - nums[j] >= 0) dp[i] += dp[i - nums[j]]; //求装满背包的不同排列数
    }
}

ps:DP的一些题目也能用递归回溯搜索,但往往会超时,毕竟类似于暴力

刷题大纲


基础题目

62. 不同路径

Solution1:递归回溯搜索(一开始超内存,后精简变量,但超时,只能过样例,遂放弃)(原因:回溯搜索本质上是暴力)

Solution2:动态规划(详见注释)

class Solution1 {
private:
    // vector<string> res; //其实只定义一个变量来记录方案数就可以
    // string path; //这样定义变量是为了方便debug
    int res;
    vector<vector<int>> map;
public:
    void backTracking(int row, int col, int m, int n) {
        if(row == m-1 && col == n-1) {
            // res.push_back(path);
            res++;
            return ;
        }

        //搜题目给的两个方向
        if(row != m-1 && !map[row+1][col]) { //下
            map[row+1][col] = 1;
            // path += "D";
            backTracking(row+1, col, m, n);
            map[row+1][col] = 0;
            // path.erase(path.size()-1, 1);
        }

        if(col != n-1 && !map[row][col+1]) { //右
            map[row][col+1] = 1;
            // path += "R";
            backTracking(row, col+1, m, n);
            map[row][col+1] = 0;
            // path.erase(path.size()-1, 1);
        }

        //最后返回
        return ;
    }
    int uniquePaths(int m, int n) {
        map = vector<vector<int>>(m, vector<int>(n, 0));
        map[0][0] = 1;
        res = 0;

        backTracking(0, 0, m, n);
        
        // for(const auto &i : res) cout << i << endl;

        // return res.size();
        return res;
    }
};
class Solution2 {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m, vector<int>(n, 0)); //表示走到坐标为(i, j)的位置有dp[i][j]种不同路径
        
        //初始化
        for(int i = 0; i < m; i++) dp[i][0] = 1;
        for(int i = 0; i < n; i++) dp[0][i] = 1;

        for(int i = 1; i < m; i++) {
            for(int j = 1; j < n; j++) {
                dp[i][j] = dp[i-1][j] + dp[i][j-1]; //要不从上一格过来 要不从左一格过来
                cout << "dp[" << i << "][" << j << "] = " << dp[i][j] << endl;
            }
        }

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

63. 不同路径 II

搜索会超时,依然动态规划

与上题类似,只是初始化复杂一点,再把障碍位置的dp[i][j]设为0即可

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();

        vector<vector<int>> dp(m, vector<int>(n, 0)); //表示走到坐标为(i, j)的位置有dp[i][j]种不同路径
        
        //初始化
        int firstCol_pos = m;
        int firstRow_pos = n;
        for(int i = 0; i < m; i++) { //寻找第一列的第一个障碍位置
            if(obstacleGrid[i][0]) {
                firstCol_pos = i; 
                break;
            }
        }
        for(int i = 0; i < n; i++) { //寻找第一行的第一个障碍位置
            if(obstacleGrid[0][i]) {
                firstRow_pos = i; 
                break;
            }
        }

        for(int i = 0; i < firstCol_pos; i++) dp[i][0] = 1;
        for(int i = 0; i < firstRow_pos; i++) dp[0][i] = 1;

        for(int i = 1; i < m; i++) {
            for(int j = 1; j < n; j++) {
                if(obstacleGrid[i][j]) dp[i][j] = 0;
                else dp[i][j] = dp[i-1][j] + dp[i][j-1]; //要不从上一格过来 要不从左一格过来
                cout << "dp[" << i << "][" << j << "] = " << dp[i][j] << endl;
            }
        }

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

70. 爬楼梯

dp数组下标代表当前在第几阶,数组value表示到这一阶的方法数(即爬到第i层楼梯,有dp[i]种方法)

遍历顺序从前往后 但是一开始没想明白为什么状态转移公式是dp[i] = dp[i-1] + dp[i-2] 只是找规律推出来的

class Solution {
public:
    int climbStairs(int n) {
        vector<int> dp(50); //dp数组下标代表当前在第几阶,数组value表示到这一阶的方法数
        dp[1] = 1;
        dp[2] = 2;
        if(n < 3) return dp[n];
        for(int i = 3; i <= n; i++) dp[i] = dp[i-1] + dp[i-2];

        return dp[n];
    }
};

96. 不同的二叉搜索树

想不出状态转移公式

class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n + 1); //表示i个节点组成的不相同BST有dp[i]种
        /*
        dp[0] = 
        dp[1] = 1
        dp[2] = 2
        dp[3] = 5
        */

        dp[0] = 1;
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= i; j++) {
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        
        return dp[n];
    }
};

343. 整数拆分

Solution1:第一反应是回溯搜索,不出所料超时了,只能过样例

Solution2:DP(详见注释)

class Solution1 {
private:
    int res = 0;
    int mul = 1;
public:
    void backTracking(int n, int depth) {
        // if(n < 0) return ;
        if(!n) {
            res = max(res, mul);
            cout << "mul = " << mul << endl;
            return ;
        }
        //第一层不能取到n 因为0*n为0
        for(int i = 1; i <= n; i++) {
            if(depth == 1 && i == n) continue;
            mul *= i;
            backTracking(n - i, depth + 1);
            mul /= i;
        }
    }
    int integerBreak(int n) {
        backTracking(n, 1);

        return res;
    }
};
class Solution2 {    
public:
    int integerBreak(int n) {
        vector<int> dp(n + 1); //表示n为i时的最大乘积是dp[i]

        dp[2] = 1;
        // dp[3] = 2;
        // dp[4] = 4
      
        for(int i = 3; i <= n; i++) {
            for(int j = 1; j < i; j++) {
                //对于遍历的j,要么把i拆分为两份,要么继续往前拆
                //即j是拆分i的第一个数
                dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));
            }
        }

        return dp[n];
    }
};

509. 斐波那契数

状态转移公式给了,初始化给了,遍历从前往后即可

class Solution {
public:
    int fib(int n) {
        int f[35];
        f[0] = 0;
        f[1] = 1;

        for(int i = 2; i <= n; i++) f[i] = f[i-1] + f[i-2];

        return f[n];
    } 
};

746. 使用最小花费爬楼梯

爬上第i阶楼梯的最小费用为dp[i]

状态转移公式为dp[i] = min(cost[i-1] + dp[i-1], cost[i-2] + dp[i-2]) 因为要不从上一阶爬上来,要不从上两阶爬上来,只要支付了当时那阶的费用即可,因此从两种选择中找花费较小的

初始化: dp[0] = 0; dp[1] = 0;

遍历顺序从前往后

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        vector<int> dp(1005); //爬上第i阶楼梯的最小费用为dp[i]

        dp[0] = 0;
        dp[1] = 0;
        for(int i = 2 ; i <= cost.size(); i++) {
            dp[i] = min(cost[i-1] + dp[i-1], cost[i-2] + dp[i-2]);
            cout << "i = " << i << ", dp[i] = " << dp[i] << endl;
        }
  
        return dp[cost.size()];

    }
};

背包问题

416. 分割等和子集

01背包应用题:一个元素只能选一次,重量数组和价值数组相同,如果能用集合中的元素正好填满容量为sum/2的背包,即dp[j] = j,则返回true

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for(const auto &i : nums) sum += i;
        if(sum % 2) return false;
        sum /= 2;

        vector<int> dp(sum + 1);
        dp[0] = 0;

        for(int i = 0; i < nums.size(); i++) {
            for(int j = sum; j >= nums[i]; j--) {
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
                if(i == nums.size()-1) cout << "dp[" << j << "] = " << dp[j] << endl;
            }
        }

        if(dp[sum] == sum) return true;
        else return false;
    }
};

474. 一和零

两个维度的01背包

最多有i个0和j个1的strs的最大子集的大小为dp[i][j]

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0)); 
        for(const auto &str : strs) { //遍历物品
            int oneNum = 0, zeroNum = 0;
            for(const auto &c : str) {
                if(c == '0') zeroNum++;
                else oneNum++;
            }
            for(int i = m; i >= zeroNum; i--) { //遍历背包容量且从后向前遍历
                for(int j = n; j >= oneNum; j--) {
                    dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
                    // cout << "str = " << str << " dp[" << i << "][" << j << "] = " << dp[i][j] << endl;
                }
            }
        }

        return dp[m][n];
    }
};

494. 目标和

不是求背包里最多能装多少了,而是背包装满有多少种方法

详见题解:https://programmercarl.com/0494.%E7%9B%AE%E6%A0%87%E5%92%8C.html#%E6%80%9D%E8%B7%AF

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
       int sum = 0;
       for(const auto &i : nums) sum += i;
       int bag = (target + sum) / 2;

       if((target + sum) % 2 == 1) return 0;
       if(abs(target) > sum) return 0;

       vector<int> dp(bag + 1, 0);
       dp[0] = 1;
       for(int i = 0; i < nums.size(); i++) {
            for(int j = bag; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
       }

       return dp[bag];
    }
};

1049. 最后一块石头的重量 II

不知道怎么应用到01背包

石头的重量和价值等同,dp[j]表示当背包容量为j时所能容纳的石头总重量,把背包容量定为sum/2,则一堆石头和另一堆石头的重量分别是dp[sum/2]和sum – dp[sum/2]

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = 0;
        for(const auto &i : stones) sum += i;
        int bag = sum / 2;
        cout << "sum = " << sum << endl;
        vector<int> dp(bag+1, 0);

        for(int i = 0; i < stones.size(); i++) {
            for(int j = bag; j >= stones[i]; j--) {
                dp[j] = max(dp[j], dp[j-stones[i]] + stones[i]);
                if(i == stones.size() - 1) cout << "dp[" << j << "] = " << dp[j] << endl;
            }
        }

        return sum - dp[bag] - dp[bag];
    }
};

57. 爬楼梯(第八期模拟笔试)

377. 组合总和 Ⅳ同理,一次迈一阶、两阶、n阶看做是不同的物品,每个物品可以取多次,最后求排列数


139. 单词拆分

单词可以重复使用 想到完全背包

数组含义:拼出目标字符串前i个字符所需要的最少单词数为dp[i]

为了能多次选到重复的单词(本质上是排列问题:aba和aab不同),因此必须先遍历背包容量,再遍历物品

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        vector<uint64_t> dp(s.size() + 1, INT_MAX);
        dp[0] = 0; 

        for(int i = 0; i <= s.size(); i++) {
            for(int j = 0; j < wordDict.size(); j++) {
                if(i < wordDict[j].size()) continue;
                if(s.substr(i - wordDict[j].size(), wordDict[j].size()) == wordDict[j]) {
                    dp[i] = min(dp[i], dp[i - wordDict[j].size()] + 1);
                }
                // cout << "i = " << i << ", j = " << j << ", dp[" << j << "] = " << dp[j] << endl;

            }
        }

        if(dp[s.size()] == INT_MAX) return false;
        return true;
    }
};

279. 完全平方数

每个完全平方数都可以重复用 想到完全背包

求最少数量 跟322. 零钱兑换很相似 只需要把物品从硬币换成完全平方数即可

class Solution {
public:
    int numSquares(int n) {
        //当n为i时,凑满n需要的最少完全平方数为dp[i]
        vector<int> dp(n + 1, INT_MAX);
        dp[0] = 0;

        //因为1 <= n <= 10000,值不大,因此可以事先准备一个完全平方数数组
        vector<int> nums;
        for(int i = 1; i * i <= n; i++) nums.push_back(i * i);

        for(int i = 0; i < nums.size(); i++) {
            for(int j = nums[i]; j <= n; j++) {
                dp[j] = min(dp[j], dp[j - nums[i]] + 1);
            }
        }

        return dp[n];
    }
};

322. 零钱兑换

关键词:每种硬币数量无限 想到完全背包 但是要变式

当总金额为i时,凑满总金额需要的最少硬币个数为dp[i]

dp数组定义 递推公式 dp[0]初始化都对了 但是dp其他元素的初始化错了 应该是一个最大的数

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<uint64_t> dp(amount + 1, INT_MAX);
        dp[0] = 0;

        for(int i = 0; i < coins.size(); i++) {
            for(int j = coins[i]; j <= amount; j++) {
                dp[j] = min(dp[j], dp[j - coins[i]] + 1);
                // cout << "i = " << i << ", j = " << j << ", dp[" << j << "] = " << dp[j] << endl;
            }
        }

        if(dp[amount] == INT_MAX) return -1;
        return dp[amount];
    }
};

377. 组合总和 Ⅳ

之前的组合总和123都能用回溯搜索处理 但这个会超时 因此动态规划

每个元素都可以取无数次 而且最终求的是排列种数 因此直接套完全背包求排列种数模板

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<uint64_t> dp(target + 1, 0);
        dp[0] = 1;

        for(int i = 0; i <= target; i++) {
            for(int j = 0; j < nums.size(); j++) {
                if(i - nums[j] >= 0) dp[i] += dp[i - nums[j]];
            }
        }
     
        return dp[target];
    }
};

518. 零钱兑换 II

完全背包问题:硬币的weight等同value,当背包容量为i时,装满背包的不同组合数为dp[i]

uint64_t表示64位无符号整型

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<uint64_t> dp(amount + 1, 0); //无符号64位整型  

        dp[0] = 1;
        for(int i = 0; i < coins.size(); i++) {
            for(int j = coins[i]; j <= amount; j++) {
                dp[j] += dp[j - coins[i]];
            }
        }

        return dp[amount];
    }
};

打家劫舍

198. 打家劫舍

dp数组含义:前i间房屋能偷的最高金额为dp[i]

初始化:dp[0]=nums[0](下标为0也就是第一间房屋,能偷的最高金额自然为nums[0]),dp[1] = max(nums[0], nums[1])(相邻的两间房只能偷一个,因此哪个金额高就偷哪个)

class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.size() == 1) return nums[0];

        vector<uint64_t> dp(nums.size() + 1, 0);
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);

        for(int i = 2; i < nums.size(); i++) {
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]); //第i家偷or不偷
            // cout << "dp[" << i << "] = " << dp[i] << endl;
        }

        return dp[nums.size()-1];   
    }
};

213. 打家劫舍 II

不知道怎么初始化dp数组

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

        for(int i = start + 2; i <= end; i++) {
            dp[i] = max(dp[i-1], dp[i-2] + nums[i]);
        }
        return dp[end];
    }
};

337. 打家劫舍 III

简单地想成了层序遍历,以层为单位偷,但实际上在一层中可以偷其中几个,而不是全偷或全不偷

树形DP 需要递归后序遍历 使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱

class Solution {
public:
    int rob(TreeNode* root) {
        vector<int> res = robTree(root);
        return max(res[0], res[1]);
    }
    //长度为2的数组:下标0代表不偷 1代表偷
    vector<int> robTree(TreeNode* cur) {
        if(!cur) return vector<int>{0, 0};
        vector<int> left = robTree(cur->left);
        vector<int> right = robTree(cur->right);
        //偷cur 那么就不能偷左右节点 
        int val1 = cur->val + left[0] + right[0];
        //不偷cur 那么可以偷也可以不偷左右节点 取较大的情况
        int val2 = max(left[0], left[1]) + max(right[0], right[1]);
        return {val2, val1};
    }
};

股票问题

121. 买卖股票的最佳时机

只能买卖一次

Solution1:贪心 遍历维护左边最小值与右边最大值

Solution2:DP 不会定义此类题的数组含义

数组含义:dp[i][0] 表示第i天持有股票所得最多现金 dp[i][1] 表示第i天不持有股票所得最多现金

递推公式:

  • dp[i][0] = max(dp[i-1][0], – prices[i]); //i-1天的时候就已经持有 or 第i天买入
  • dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]); //i-1天的时候就已经不持有 or 第i天卖出
class Solution1 {
public:
    int maxProfit(vector<int>& prices) {
        int low = INT_MAX;
        int res = 0;
        for(int i = 0; i < prices.size(); i++) {
            low = min(low, prices[i]);
            res = max(res, prices[i] - low);
        }

        return res;
    }
};
class Solution2 {
public:
    int maxProfit(vector<int>& prices) {
        vector<vector<int>> dp(prices.size() + 1, vector<int>(2, 0));
        //dp[i][0] 表示第i天持有股票所得最多现金 
        //dp[i][1] 表示第i天不持有股票所得最多现金
        dp[0][0] = 0 - prices[0];
        dp[0][1] = 0;
        cout << "dp[0][0] = " << dp[0][0] << " dp[0][1] = " << dp[0][1] << endl;
        for(int i = 1; i < prices.size(); i++) {
            dp[i][0] = max(dp[i-1][0], - prices[i]); //i-1天的时候就已经持有 or 第i天买入
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]); //i-1天的时候就已经不持有 or 第i天卖出
            // cout << "dp[" << i << "][0] = " << dp[i][0] << " dp[" << i << "][1] = " << dp[i][1] << endl;
        }

        return dp[prices.size()-1][1];
    }
};

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

可以买卖任意次

Solution1:贪心(只要后一天比当前价高,就买了再卖)

Solution1:DP(类似上题,但注意在dp[i][0]的递推公式上略有不同,因为上题只能买卖一次,没积累现金)

class Solution1 {  
public:
    int maxProfit(vector<int>& prices) {
        int res = 0;
        if(prices.size() == 1) return 0;
        for(int i = 0; i < prices.size() - 1; i++) {
            if(prices[i] < prices[i+1]) {
                res += prices[i+1] - prices[i];
            }
        }
        return res;
    }
};
class Solution2 { 
public:
    int maxProfit(vector<int>& prices) {
        vector<vector<int>> dp(prices.size()+1, vector<int>(2, 0));
        //dp[i][0]表示第i天持有股票所得最多现金 
        //dp[i][1]表示第i天不持有股票所得最多现金
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        
        for(int i = 1; i < prices.size(); i++) {
            //i-1天的时候就已经持有 or 第i天买入(与上题有差异,因为上题没有积累现金)
            dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]);
            //i-1天的时候就已经不持有 or 第i天卖出
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);
        }

        return dp[prices.size()-1][1];
    }
};

123. 买卖股票的最佳时机 III

难点在于最多买卖两次,因此dp[i][0]、dp[i][1]两个状态不够了

dp数组含义:i表示第i天,j为0~3四个状态,dp[i][j]表示第i天状态j所剩最大现金

  1. 第一次持有股票
  2. 第一次不持有股票
  3. 第二次持有股票
  4. 第二次不持有股票
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        vector<vector<int>> dp(prices.size()+1, vector<int>(4, 0));
        //i表示第i天,j为0~3四个状态,dp[i][j]表示第i天状态j所剩最大现金
        //状态0表示第一次持有股票
        //状态1表示第一次不持有股票
        //状态2表示第二次持有股票
        //状态3表示第二次不持有股票
        dp[0][0] = -prices[0]; 
        dp[0][2] = -prices[0]; 

        for(int i = 1; i < prices.size(); i++) {
            dp[i][0] = max(dp[i-1][0], -prices[i]);
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);
            dp[i][2] = max(dp[i-1][2], dp[i-1][1] - prices[i]);
            dp[i][3] = max(dp[i-1][3], dp[i-1][2] + prices[i]);
        }

        return dp[prices.size()-1][3];
    }
};

188. 买卖股票的最佳时机 IV


309. 买卖股票的最佳时机含冷冻期


714. 买卖股票的最佳时机含手续费


子序列问题

300. 最长递增子序列

dp数组含义:dp[i]表示i之前(包括i)以nums[i]结尾的最长递增子序列的长度

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int res = 1;
        vector<int> dp(nums.size()+1, 1); //下标从0到i的元素的最长递增子序列长度为dp[i]
        
        for(int i = 1; i < nums.size(); i++) {
            for(int j = 0; j < i; j++) {
                if(nums[i] > nums[j]) {
                    dp[i] = max(dp[i], dp[j] + 1); 
                }
            }
            res = max(res, dp[i]);
        }

        return res;
    }
};

1035. 不相交的线


1143. 最长公共子序列


53. 最大子数组和


674. 最长连续递增序列

求最长的连续递增子序列,自然想到滑动窗口

但是DP更简单,相当于300. 最长递增子序列的简单版

也可以用贪心,更简单

class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        int res = 1;
        vector<int> dp(nums.size()+1, 1); //以下标i的元素结尾的最长递增序列长度为dp[i]
        
        for(int i = 1; i < nums.size(); i++) {
            if(nums[i] > nums[i-1]) {
                dp[i] = dp[i-1] + 1; 
                
            }
            cout << "dp[" << i << "] = " << dp[i] << endl;
            res = max(res, dp[i]);
        }

        return res;
    }
};

718. 最长重复子数组

用二维数组记录两个字符串的所有比较情况

dp数组含义:以下标i – 1为结尾的A,和以下标j – 1为结尾的B,最长重复子数组长度为dp[i][j]

ps:为什么用i-1而不是i?这样dp[i][0] 和dp[0][j]都没有意义,直接初始化为0,而不用额外初始化

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        //以下标i-1为结尾的A,和以下标j-1为结尾的B,最长重复子数组长度为dp[i][j]
        vector<vector<int>> dp(nums1.size()+1, vector<int>(nums2.size()+1, 0));
        int res = 0;
        for(int i = 1; i <= nums1.size(); i++) {
            for(int j = 1; j <= nums2.size(); j++) {
                if(nums1[i-1] == nums2[j-1]) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                }
                // cout << "dp[" << i << "][" << j << "] = " << dp[i][j] << endl;
                res = max(res, dp[i][j]);
            }
        }

        return res;
    }
};

72. 编辑距离

dp数组含义:以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]

或者可以换个说法:word1中前i个字母组成的字符串,和word2中前j个字母组成的字符串,最近编辑距离为dp[i][j](因为比如word=”abc”,当i=2时,即代表以下标i-1为结尾的字符串“ab”,也就是前i个字母)

初始化:dp[i][0]:以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0],即i;dp[0][j]同理

class Solution {
public:
    int minDistance(string word1, string word2) {
        //以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]
        //即word1中前i个字母组成的字符串,和word2中前j个字母组成的字符串,最近编辑距离为dp[i][j]
        vector<vector<int>> dp(word1.size()+1, vector<int>(word2.size()+1, 0));
        //dp[i][0]:以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0],即i 
        for(int i = 0; i <= word1.size(); i++) dp[i][0] = i;
        for(int j = 0; j <= word2.size(); j++) dp[0][j] = j;

        for(int i = 1; i <= word1.size(); i++) {
            for(int j = 1; j <= word2.size(); j++) {
                if(word1[i-1] == word2[j-1]) {
                    dp[i][j] = dp[i-1][j-1];
                } else {
                    //word1删除一个元素 or word2删除一个元素 or 替换一个元素
                    dp[i][j] = min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}) + 1;
                }
            }
        }

        return dp[word1.size()][word2.size()];
    }
};

115. 不同的子序列


392. 判断子序列

Solution1:简单循环判断(本质双指针)

Solution2:看成编辑距离的简化版,只需要计算删除的情况,不用考虑增加和替换

dp数组含义:以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]

class Solution1 {
public:
    bool isSubsequence(string s, string t) {
        if(s.size() == 0) return true;
        if(t.size() == 0) return false;
        int k = 0;
        for(int i = 0; i < t.size(); i++) {
            if(t[i] == s[k]) k++;
            if(k == s.size()) return true;
        }

        return false;
    }
};
class Solution2 {
public:
    bool isSubsequence(string s, string t) {
        //以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]
        vector<vector<int>> dp(s.size()+1, vector<int>(t.size()+1, 0));

        for(int i = 1; i <= s.size(); i++) {
            for(int j = 1; j <= t.size(); j++) {
                if(s[i-1] == t[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
                else dp[i][j] = dp[i][j-1];
            }
        }

        if(dp[s.size()][t.size()] == s.size()) return true;
        return false;
    }
};

583. 两个字符串的删除操作


516. 最长回文子序列


647. 回文子串

Solution1:DP

dp数组含义:表示区间范围[i,j]的子串是否是回文子串,如果是,则dp[i][j]为true,否则为false

遍历顺序:从下到上,从左到右(详见代码随想录)

Solution2:双指针

遍历中心点的,注意中心点有两种情况:一个元素可以作为中心点,两个元素也可以作为中心点

class Solution1 {
public:
    int countSubstrings(string s) {
        vector<vector<bool>> dp(s.size() + 1, vector<bool>(s.size() + 1, false));
        int cnt = 0;
        for(int i = s.size() - 1; i >= 0; i--) {
            for(int j = i; j < s.size(); j++) {
                if(s[i] == s[j]) {
                    if(j - i <= 1) {
                        dp[i][j] = true;
                        cnt++;
                    } else if(dp[i+1][j-1]){
                        dp[i][j] = true;
                        cnt++;
                    }
                }
            }
        }

        return cnt;
    }
};
class Solution2 {
private:
    int extend(string &s, int l, int r) {
        int num = 0;
        while(l >= 0 && r < s.size() && s[l] == s[r]) {
            num++;
            l--;
            r++;
        }
        return num;
    }
public:
    int countSubstrings(string s) {
        int cnt = 0;
        for(int i = 0; i < s.size(); i++) {
            cnt += extend(s, i, i);     //以一个元素为中心点
            cnt += extend(s, i, i + 1); //以两个元素为中心点
        }

        return cnt;
    }
};


不准投币喔 👆

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇