动态规划

203 阅读26分钟

image.png 额外知识点: c++中不支持动态数组,例如int a[n] 不允许将变量声明为数组长度,这是为了执行效率。如果固定了长度,可以在编译的时候就存放在静态方法区。

动态规划的思想:

动态规划是一种分治的思想,把一个问题递归地分化成一个个小问题。把子问题整合成为整问题的方法是结题的关键,又称作状态转移方程。 总结: 解决动态规划问题最难的地方有两点:(1)如何定义f(n)f(n);(2)如何利用f0,f1,...,fn1f_0,f_1,...,f_{n-1}推导出fnf_n.

01 爬楼梯

Problem: 70. 爬楼梯

解题方法

每一级楼梯只能从它的前面一阶或者是前面两阶而来,所以d[i]=d[i-1]+d[i-2];这是一个斐波那契数列。

Code

class Solution {
    public int climbStairs(int n) {
        int[] dp=new int[ n+1];
        if(n<=1){
            return 1;
        }


        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3;i < n+1;i ++){
            dp[ i ] = dp[ i-2 ]+dp[ i -1 ];
        }
    return dp[n];

    }
}

02最小花费爬楼梯

Problem: 746. 使用最小花费爬楼梯

方法

dp数组定义:dp[i]表示到达i台阶的最小花费。
初始化:可以选择从0或者1起跳,到达第0阶和第一阶的值都是0,dp[0]=dp[1]=0;dp[2]=Math.min(dp[0]+cost[0],dp[1]+cost[1])
递推公式:dp[i] = Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2);表示到达第i台阶的花费是从i-1阶跳一格上来与从i-2跳两个上来的最小值。

Code

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int[] dp = new int[cost.length+1];//dp[i]表示到达i阶所需要的最小花费
        dp[0]=0;//到达第0台阶不需要花费
        dp[1] = 0;
        for(int i =2;i<cost.length+1;i++){
            dp[i] = Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[cost.length];

    }
}

03机器人寻路

方法

dp数组:dp[i][j]表示从0,0到达i,j的道路数量
初始化:第一行和第一列均为1(初始化不一定只有一个数)
递推公式:d[i][j] = d[i-1][j]+d[i][j-1]
返回值:d[m-1][n-1]

Code

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int[][] dp = new int[obstacleGrid.length][obstacleGrid[0].length];
        for(int i =0;i< obstacleGrid.length;i++){
            if(obstacleGrid[i][0]!=1)dp[i][0] =1;
            else{

            }
        }
        int i = 0;
        int j = 0;
        while(i<obstacleGrid.length && obstacleGrid[i][0]!=1 )dp[i++][0] =1;
        while(i<obstacleGrid.length)dp[i++][0] =0;
        while(j<obstacleGrid[0].length && obstacleGrid[0][j]!=1 )dp[0][j++] =1;
        while(j<obstacleGrid[0].length)dp[0][j++] =0;

        for(i =1;i< obstacleGrid.length;i++){
            for (j =1;j < obstacleGrid[0].length;j++){
                if( obstacleGrid[i][j] == 1){
                    dp[i][j] = 0;
                }
                else{
                    dp[i][j] = dp[i-1][j]+dp[i][j-1];
                }
            }
        }
        return dp[obstacleGrid.length-1][obstacleGrid[0].length-1];

    }
}

机器人寻路变体

思路

遇到障碍物让dp[i][j]=0;

Code

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        for(int i =0;i<m;i++)dp[i][0]=1;
        for(int j = 0;j<n;j++)dp[0][j]=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 ];
            }
        }
        return dp[m-1][n-1];
    }
}

背包问题

01背包问题

weight[n] value[n] 定义:01背包问题的定义是在规定容量的背包内,尽可能装价值最大的物品。每个物品只有01两种选择。f(i,j)表示在背包重量为j的情况下,背包的最高价值为.这个价值的来源有两种路径:

  • 假如使用了第i个元素 那么他的价值等于f(i,j) =f(i-1,j-weight[i])+value[i]
  • 如果没有使用 那么他的价值就是f(i,j)=f(i-1,j);
  • 二者取最大值 这个方法下一行都会使用上一行左上角的元素,因此可以使用滚动数组进行优化记得需要遍历n次就行了 递推:

9af4cdad-9fab-44f8-b7ab-64e74edd9846.jpg

Code

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        // 读取 N
        Scanner scanner = new Scanner(System.in);
        int M = scanner.nextInt();
        int N = scanner.nextInt();
        int[] costs = new int[M];
        int[] values = new int[M];

        for (int i = 0; i < M; i++) {
            costs[i] = scanner.nextInt();
        }
        for (int j = 0; j < M; j++) {
            values[j] = scanner.nextInt();
        }

        // 创建一个动态规划数组dp,初始值为0
        int[] dp = new int[N + 1];

        // 外层循环遍历每个类型的研究材料
        for (int i = 0; i < M; ++i) {
            // 内层循环从 N 空间逐渐减少到当前研究材料所占空间
            for (int j = N; j >= costs[i]; --j) {
                // 考虑当前研究材料选择和不选择的情况,选择最大值
                dp[j] = Math.max(dp[j], dp[j - costs[i]] + values[i]);
            }
        }

        // 输出dp[N],即在给定 N 行李空间可以携带的研究材料最大价值
        System.out.println(dp[N]);
    }
}
变体
  1. 输入一堆石头,两两相撞,大石头减去小石头会剩下一块小石头。问最小剩下的重量是多少?
  2. 将一个集合分割成两个和相等的集合
  3. 计算装满背包有多少种方法。例如目标和问题,可以通过划分为两个子集:加法集合与减法集合,sum-sub =target;也即转换成了背包容量为sum的背包问题,但是是求装满背包的方式数量。 d[j]=d[j]+d[j-weight[i]]

01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。

一零和

474. 一和零 - 力扣(LeetCode)

这个题可以先用三维数组分析,对于一个状态f(i,m,n)f(i,m,n)它是由两种状态转移而来,一种是不使用str[i],这种情况下f(i,m,n)=f(i1,m,n)f(i,m,n)=f(i-1,m,n)

对于另一种情况,如果使用了str[i],那么f(i,m,n)=f(i1,mcount0,ncount1)+1f(i,m,n) = f(i-1,m-count0,n-count1)+1

对于两种情况取最大值即可得到最优解max(f(i1,mcount0,ncount1)+1,f(i1,m,n))max(f(i-1,m-count0,n-count1)+1,f(i-1,m,n))但要注意当j小于count0或者k小于count1的时候 ,要对f[i][j][k]赋值,不能够直接跳过,否则会造成解漏掉的情况。(如果使用滚动数组则可以不用赋值。)

要注意当j小于count0或者k小于count1的时候,要对f[i][j][k]赋值,不能够直接跳过,否则会造成解漏掉的情况。(如果使用滚动数组则可以不用赋值。)\color{red} \mathbf{要注意当j小于count0或者k小于count1的时候,} \\ \mathbf{要对f[i][j][k]赋值,} \mathbf{不能够直接跳过,否则会造成解}\\ \mathbf{漏掉的情况。(如果使用滚动数组则可以不用赋值。)}

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int len = strs.length;

        int[][][] f = new int[len+1][m + 1][n + 1];
        // f[][0][0] = 0;
        //外层遍历物品
        for (int i = 1; i <= strs.length; i++) {
            // 计算字符数量
            int n0 = 0;
            int n1 = 0;
            for (char c : strs[i - 1].toCharArray()) {
                if (c == '0')
                    n0++;
                if (c == '1')
                    n1++;
            }
            //内层遍历背包
            for (int j = m; j >= 0; j--) {
                for (int k = n; k >= 0; k--) {
                    if(j<n0||k<n1){
                    f[i][j][k] =  f[i-1][j][k];
                    }else{
                        f[i][j][k] = Math.max(f[i-1][j - n0][k - n1] + 1, f[i-1][j][k]);
                    }                 
                }
            }

        }
        return f[len][m][n];

    }
}
//滚动数组的解法
class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int len = strs.length;

        int[][] f = new int[m + 1][n + 1];
        // f[0][0] = 0;
        for (int i = 1; i <= strs.length; i++) {
            // 计算字符数量
            int n0 = 0;
            int n1 = 0;
            for (char c : strs[i - 1].toCharArray()) {
                if (c == '0')
                    n0++;
                if (c == '1')
                    n1++;
            }
            for (int j = m; j >= 0; j--) {
                for (int k = n; k >= 0; k--) {

                        f[j][k] = Math.max(f[j - n0][k - n1] + 1, f[j][k]);
                                   
                }
            }

        }
        return f[m][n];

    }
}
目标和

示例:

  • 输入:nums: [1, 1, 1, 1, 1], S: 3
  • 输出:5

解释:

  • -1+1+1+1+1 = 3
  • +1-1+1+1+1 = 3
  • +1+1-1+1+1 = 3
  • +1+1+1-1+1 = 3
  • +1+1+1+1-1 = 3 思路:分成AB两部分,A-B=target 又有A+B=sum 则有2A=sum+target 选和为A的数即可 代码:
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.size(); i++) sum += nums[i];
        if (abs(target) > sum) return 0; // 此时没有方案
        if ((target + sum) % 2 == 1) return 0; // 此时没有方案
        int bagSize = (target + sum) / 2;
        vector<int> dp(bagSize + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < nums.size(); i++) {
            for (int j = bagSize; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[bagSize];
    }
};
分割等和子集

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 思路:转化为bagSize = sum/2的01背包问题

for(i=0;i<
最后一块石头重量

石头两两相碰消失,最后剩下一块石头,求石头最小的质量 选择任意数量的石头,尽量接近总质量的一半

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        vector<int> dp(15001, 0);
        int sum = 0;
        for (int i = 0; i < stones.size(); i++) sum += stones[i];
        int target = sum / 2;
        for (int i = 0; i < stones.size(); i++) { // 遍历物品
            for (int j = target; j >= stones[i]; j--) { // 遍历背包
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum - dp[target] - dp[target];
    }
};

完全背包问题

当物品可以取用无数次的时候,则是完全背包问题. f(i,j)表示只考虑前i个物品,可以恰装满j的组合数。 f(i,j)=f(i1,j)+f(i1,jxi)f(i,j)=f(i-1,j)+f(i-1,j-xi)如果不使用则是继承i-1 如果使用则是f(i-1,j-xi)

注意第一行需要初始化,d[0][j]=d[0][jx0]从左往右初始化\color{red} \mathbf{注意第一行需要初始化,d[0][j]=d[0][j-x_0]从左往右初始化} 从小到大遍历target,每一个target都由前边的target+num[i]转化而来,用一个一维数组保存target的方案数量即可。 注意第一个元素的初始化为1

零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

示例 1:

  • 输入:coins = [1, 2, 5], amount = 11
  • 输出:3
  • 解释:11 = 5 + 5 + 1

思路:目标和问题,元素可以重复选,归属于完全背包类。从小到大遍历target,每一个target都由前边的target+num[i]转化而来,用一个一维数组保存target的方案数量即可。 而且硬币不会出现相同的元素,故不需要去重


class Solution {
    public int change(int amount, int[] coins) {
        //完全背包问题
        int[] d = new int[amount + 1];
        d[0] = 1;
        // 求组合数,外层遍历物品;如果是求排列数,则外层遍历背包
        for (int i = 1; i <=coins.length; i++) {
            int xi = coins[i-1];
            for (int j = xi; j <= amount; j++) {
                
                d[j] +=  d[j - xi];
            }
        }
        return d[amount];
    }
}

组合总和

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

  • nums = [1, 2, 3]
  • target = 4

所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) 思路:对于每一个背包,遍历物品来获得得到该重量的方案,由于本题只考虑方案数量,所以使用f0行来存储总方案数量,如果不用累加的话,需要使用三层for循环来实现手动累加相对更加麻烦

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<vector<long>> f(nums.size()+1,vector<long>(target+1));
        f[0][0]=1;
        for(int j=1;j<target+1;j++){//对于每一个背包重量
            for(int i =1;i<nums.size()+1;i++){//可能是由任意一个nums[i]来的,所以需要遍历所有物品
            //取所有遍历物品之和
                if(j-nums[i-1]>=0&&f[0][j]<INT_MAX -f[0][j-nums[i-1]])
                f[0][j]+=f[0][j-nums[i-1]];
            }
        }
        return f[0][target];

    }
};
进阶爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。 思路:n是背包的target 而每次可以选择m以内的物品,是典型的完全背包问题

完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。

思路:先求根号n,为了避免重复计算平方可以设置一个平方数组s[n],由于每个target只用保存一个最优结果,所以只用一维数组就行,每次迭代都更新暂时的最优结果。 从小到大遍历target,对于每一个target遍历物品i,都选择f(target) = min(f(target-s[i]))

这题用先遍历物品也可以做,区别在于先遍历背包是通过对于target状态的扣除,倒推出之前的状态。而遍历物品则是通过物品从0到1的堆砌形成状态的更新,一步步堆到target。

class Solution {
public:
    int numSquares(int n) {
        vector<int> dp(n + 1, INT_MAX);
        dp[0] = 0;
        for (int i = 0; i <= n; i++) { // 遍历背包
            for (int j = 1; j * j <= i; j++) { // 遍历物品
                dp[i] = min(dp[i - j * j] + 1, dp[i]);
            }
        }
        return dp[n];
    }
};
单词拆分

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。 思路:d[i]表示i之前的字符串能不能用字典生成,d[0]=1,从i开始往前倒len个字符,如果是字典里面的数,那么它就是可以被字典组成的。用一个wordLen存储字符长度,可以减少很多冗余计算

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {

        vector<int> d =
            vector<int>(s.length() + 1, 0); // di表示i之前的是否可以表示
        d[0] = 1; // 空字符串是连接运算的单位元
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        unordered_set<int> wordLen;
        for (int i = 0; i < wordDict.size(); i++) {
            wordLen.insert(wordDict[i].size()); // 建立字典中长度的set
        }

        for (int i = 1; i <= s.length(); i++) {
            for (auto len : wordLen) {
                if (i >= len) {
                    string word = s.substr(i - len, len);//(0,4)表示从索引0开始取4个字符
                    if (wordSet.find(word) != wordSet.end() && d[i - len]) {
                        d[i] = true;
                    }
                }
            }
        }
        return d[s.length()];
    }
};

打家劫舍

低阶

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

  • 示例 1:
  • 输入:[1,2,3,1]
  • 输出:4

解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。   偷窃到的最高金额 = 1 + 3 = 4。 思路:假设d[i]表示包括d[i]在内偷到的最高金额。那么d[i]有两种来源

  • 如果偷了num[i]那么d[i]=d[i-2]+num[i]
  • 如果偷的是num[i-1]那么d[i]=d[i-1]
  • 初始值为第一个房屋的价格
class Solution {
public:
    int rob(vector<int>& nums) {
        if (nums.size() == 0) return 0;
        if (nums.size() == 1) return nums[0];
        vector<int> dp(nums.size());
        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]);
        }
        return dp[nums.size() - 1];
    }
};
环形

房屋如果是环形的需要排除第一个元素和最后一个元素同时选中的情况,0-n-1 与1-n两种方案取最大值

class Solution {
public:
    int rob(vector<int>& nums) {
        if (nums.size() == 0) return 0;
        if (nums.size() == 1) return nums[0];
        int result1 = robRange(nums, 0, nums.size() - 2); // 情况二
        int result2 = robRange(nums, 1, nums.size() - 1); // 情况三
        return max(result1, result2);
    }
    // 198.打家劫舍的逻辑
    int robRange(vector<int>& nums, int start, int end) {
        if (end == start) return nums[start];
        vector<int> dp(nums.size());
        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 - 2] + nums[i], dp[i - 1]);
        }
        return dp[end];
    }
};

股票问题

单买单卖

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。 思路:每一天的股票都有三种状态,所以需要一个(day,3)的二维数组

  • 静止状态,本题中只能卖出一次,所以卖出之后就不能回到初始状态了,故静止状态只能继承
  • 买入状态:继承或者从前一天的初始状态转换而来,max(前一天初始状态-今天价格,前一天的买入状态)
  • 卖出状态:继承或者从前一天的买入状态继承而来,max(前一天的买入状态+今天价格,前一天的卖出状态)

初始资金为0,且第一天不能卖出,最优值的流向是从左上角流向右下角

class Solution {
public:
    int maxProfit(vector<int>& prices) {
    //  设天数为n,新建一个数组为dp[n,2]
    int day = prices.size();
    vector< vector<int>> dp(day,vector<int>(3,0));//横坐标表示天数,纵坐标表示状态,值表示净利润
    dp[0][0]= 0;//维护最佳销售额
    dp[0][1]= -prices[0];//第一天购入的最佳花费
    dp[0][2]= 0;//第一天无法卖出
    for(int i =1;i<day;i++){
        dp[i][0] =dp[i-1][0];//静止状态只能由不买也不卖的状态来维持,因为题目中只允许买卖一次,所以卖出之后不能再转换成静止
        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]);//卖出状态可以从买入状态转换而来,也可以从卖出状态维持,由于不能在买,所以卖出状态就是最终状态
    }
    return dp[day-1][2];

    }
};
k次买卖(hard)

你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

  • 示例 1:
  • 输入: [7,1,5,3,6,4]
  • 输出: 7
    解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
class Solution {
public:
    int maxProfit(int max_k, vector<int>& prices) {
        vector<vector<vector<int>>> dp(prices.size(),vector<vector<int>>(max_k+1,vector<int>(2,0)));
        
        for(int i =0;i<prices.size();i++){
            for(int k =1;k<=max_k;k++){
                if(i==0){
                    dp[i][k][0]=0;//第一天不持有
                    dp[i][k][1]=-prices[i];//第一天买入
                    continue;
                }
                 dp[i][k][0]=max(dp[i-1][k][0],dp[i-1][k][1]+prices[i]);//不持有状态,从前一天不持有状态保持,或者是从前一天持有状态卖出
                 dp[i][k][1]=max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i]);//持有状态,从前一天不持有状态买入此时买入次数+1,或者是保持持有
            }
        }
        return dp[prices.size()-1][max_k][0];
    }
};
冷静期与手续费问题

冷静期clamdown需要从i-2的不持有状态转变成持有状态,手续费是在售出操作时-fee

// 同时考虑交易次数的限制、冷冻期和手续费
int maxProfit_all_in_one(int max_k, vector<int>& prices, int cooldown, int fee) {
    int n = prices.size();
    if (n <= 0) {
        return 0;
    }

    vector<vector<vector<int>>> dp(n, vector<vector<int>>(max_k + 1, vector<int>(2)));
    // k = 0 时的 base case
    for (int i = 0; i < n; i++) {
        dp[i][0][1] = INT_MIN;
        dp[i][0][0] = 0;
    }

    for (int i = 0; i < n; i++) {
        for (int k = max_k; k >= 1; k--) {
            if (i - 1 == -1) {
                // base case 1
                dp[i][k][0] = 0;
                dp[i][k][1] = -prices[i] - fee;
                continue;
            }

            // 包含 cooldown 的 base case
            if (i - cooldown - 1 < 0) {
                // base case 2
                dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
                // 别忘了减 fee
                dp[i][k][1] = max(dp[i-1][k][1], -prices[i] - fee);
                continue;
            }
            dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
            // 同时考虑 cooldown 和 fee
            dp[i][k][1] = max(dp[i-1][k][1], dp[i-cooldown-1][k-1][0] - prices[i] - fee);     
        }
    }
    return dp[n - 1][max_k][0];
}

子序列问题

最长上升子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列是可以跳着选,子数组不能够跳着选 思路:对于单串问题,定义一个dp数组记录最优状态即可,一般长度为n+1;d[i]={比num[i]稍小的一个数d[j]+1}; 本题和导弹下降系统是同一类题,上升子序列的要求是后一个序列最起码存在一个元素大于前一个序列的任意一个元素.

class Solution {
public:
    int lengthOfLIS_2(vector<int>& nums) {
        vector<int> dp(nums.size(),1);//对于任意一个元素而言,起码存在长度为1的递增子序列
        
        for(int i=1;i<nums.size();i++){
            for(int j =i-1;j>=0;j--){
                if(nums[j]<nums[i]){
                   dp[i]=max(dp[i], dp[j]+1);
                }  
            }
        }
        int res=1;
        for(int i =0;i<nums.size();i++){
            if(dp[i]>res)res = dp[i];
        }
        return res;
        

    }
        int lengthOfLIS(vector<int>& nums) {
        vector<int> f(nums.size(),0);//fn是一个最长递增子序列暂存值,左边是最长子序列最小的元素,右边是目前最大的值
        int len =1;
        f[0]=nums[0];
        for(int i =1;i<nums.size();i++){//如果后一个元素比当前递增子序列最后一个元素大,则直接加到后面
            if(nums[i]>f[len-1]){
                f[len++]=nums[i];
            }else{
                int j=0;
                while(f[j]<nums[i]){//找到第一个比它大的一丢丢的元素,用nums[i]替换这个数,对于当前最长子序列的长度不会有影响
                    j++;
                }
                f[j]=nums[i];
            }
        }

        return len;
        

    }
};
最长连续递增序列

例如:12537 结果是3 思路:如果后一个数比前一个数大,就dp+1

class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        vector<int> dp(nums.size(), 1);
        for (int i = 1; i < nums.size(); i++) {
            if (nums[i] > nums[i - 1]) {
                dp[i] = dp[i - 1] + 1;
            }
        }
        int max_dp = 0;
        for (int i = 0; i < nums.size(); i++) {
            if (dp[i] > max_dp) {
                max_dp = dp[i];
            }
        }
        return max_dp;
    }
};
最长重复子序列

力扣题目链接(opens new window)

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。

示例:

输入:

  • A: [1,2,3,2,1]
  • B: [3,2,1,4,7]
  • 输出:3
  • 解释:长度最长的公共子数组是 [3, 2, 1] 。 思路:1.要求连续的最长相同子数组2.要对比和计算两个序列,考虑使用二维dp表 3.递推关系:如果下一个字符相同,那么长度就会+1,如果不同,则长度为0,同时用一个变量存储最大值

image.png

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        vector<vector<int>> dp(nums1.size()+1,vector<int>(nums2.size()+1,0));//y引入空串
        int result =0;//dp[i][j]表示以i-1结尾的A串与以j-1结尾的B串的子数组长度,当最后两个字符不一样时,结果为0
        //例如串4123与123最长为3,串41237与123的长度就为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;
                }
                if(result<dp[i][j])result
                =dp[i][j];
                
                
            }
        }
        return result;


    }
};
最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。 思路:

  1. 定位为双串dp问题
  2. 关系:最后一个字符相同,dp+1,如abc与abdc。字符不同,则继承前边最长的dp abcd与abcf
  3. dp[i][j]表示以ij结尾的字符串的最优值
  4. 这个题需要从左上和左边继承,最好是设置成(m+1,n+1)的表
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        vector<vector<int>> dp(text1.size() + 1,
                               vector<int>(text2.size() + 1, 0));

        for (int i = 1; i < text1.size() + 1; i++) {
            for (int j = 1; j < text2.size() + 1; j++) {
                if (text1[i - 1] == text2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] =
                        max(dp[i - 1][j], dp[i][j - 1]); // 从左边或者上边继承
                }
            }
        }
        return dp[text1.size()][text2.size()];
    }
};
不相交的线

在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:

  •  nums1[i] == nums2[j]
  • 且绘制的直线不与任何其他连线(非水平线)相交。 思路:这题是子序列的裸题,只是换一种描述方式
最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

  • 输入: [-2,1,-3,4,-1,2,1,-5,4]
  • 输出: 6
  • 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

思路:1. dp[i]定义为以i结尾的最大和 2. 由于最优结果不会继承到末尾,所以需要单独设置一个变量来统计最大值

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        vector<int> dp(nums.begin(),nums.end());
        int res = nums[0];//为了解决只有一个值的情况
        for(int i =1;i<nums.size();i++){
            dp[i] =max(dp[i-1]+nums[i],dp[i]);
            if(dp[i]>res)res = dp[i];
        }
        return res;
    }
};
判断子序列

判断s是不是t的子序列,即t删掉一些元素是否可以变成s 思路:

  1. 双串问题,二维dp
  2. 关系:字符相同,等价于dp[i-1][j-1],字符不同,如果是t多了,则左继承,如果是s多了,那么就是false(初始化就默认是false)
class Solution {
public:
    bool isSubsequence(string s, string t) {
        vector<vector<bool>> f(s.size()+1,vector<bool>(t.size()+1,0));
        //初始化
        for(int j =0;j<t.size()+1;j++){
            f[0][j]=true;
        }
        for(int i=1;i<s.size()+1;i++){
            for(int j =i;j<t.size()+1;j++){
                if(s[i-1]==t[j-1]){
                    //斜方向继承
                    f[i][j] = f[i-1][j-1];
                }else{
                    //否则的话就左继承
                     f[i][j] = f[i][j-1];
                }
            }
        }
        return f[s.size()][t.size()];

    }
};
不同的子序列个数(hard)

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)

image.png

class Solution {
public:
    int numDistinct(string s, string t) {
         vector<vector<uint64_t>> f(s.size()+1,vector<uint64_t>(t.size()+1,0));
         for (int i = 0; i < s.size(); i++) f[i][0] = 1;
         for(int i =1;i<s.size()+1;i++){
            for(int j =1;j<t.size()+1;j++){
                if(s[i-1]==t[j-1]){
                    f[i][j] = f[i-1][j-1]+f[i-1][j];//如果使用si则等价于去掉最后一个字母,如果不食用则往前倒一位
                }else{
                    f[i][j] =f[i-1][j];
                }
            }
         }
         return f[s.size()][t.size()];
    }
};
字符串删除操作

给定两个单词 word1 和 word2 ,返回使得 word1 和  word2 **相同所需的最小步数

思路:1.二维dp 对于每一个s[i]==t[j] 有f(i,j)=f(i-1,j-1);如果s[i]!=s[j]那么就f(i,j)=min(f(i-1,j)+1,f(i,j-1)+1) 2.同时删去i,j的情况已经被继承到了i-1,j的情况,所以不需要再单独写

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

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符 sea 变为seat
  • 删除一个字符 seaf 变成sea
  • 替换一个字符 set 变成sea

思路: 1.二维dp 最好的结果落在右下角 2.对三种方式的操作数递推:

  • 替换:f(i,j)=f(i-1,j-1)+1
  • 插入和删除是同样的:f(i,j)=min(f(i-1,j),f(i,j-1))+1 对于i,j相同的情况=f(i-1,j-1) 3.初始化:把一个字符串变成空串需要的操作数
class Solution {
public:
    int minDistance(string word1, string word2) {
        vector<vector<int>> f(word1.size()+1, vector<int>(word2.size()+1, 0));
        for(int i =1;i<word1.size()+1;i++){
            f[i][0]=i;
        }
        for(int i =1;i<word2.size()+1;i++){
            f[0][i]=i;
        }
        for (int i = 1; i < word1.size() + 1; i++) {
            for (int j = 1; j < word2.size() + 1; j++) {
                if (word1[i - 1] == word2[j - 1]) {
                    f[i][j] = f[i - 1][j - 1];
                } else {
                    f[i][j] = min(f[i][j-1]+1,f[i-1][j]+1);
                }
            }
        }
        return f[word1.size()][word2.size()];
    }
};
回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

输入: s = "aaa"

输出: 6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

思路:

  1. 二维dp 每一个ij表示是或者不是,用一个result累加回文串数量
  2. 首先需要判断一个字符串是不是回文子串,而且最好建立在递归判断的基础上,想到当i==j的时候,如果i+1到j-1是回文子串 那么ij就是回文子串。
  3. 最小的回文子串是一个字符 因此对角线全为1 ;同时需要考虑两个连续字符aa,也是最小的递归基
class Solution {
public:
    int countSubstrings(string s) {
        vector<vector<int>> f(s.size(),vector<int>(s.size(),0));
        int res=0;
        for(int i =0;i<s.size();i++){
            f[i][i]=1;//长度为1
            res++;
            if(i&&s[i]==s[i-1]){//长度为2
                f[i-1][i]=1;
                res++;
            }
        }

        for(int i =s.size()-1;i>=0;i--){
            for(int j=i+2;j<s.size();j++){
                if(s[i]==s[j]){
                    f[i][j]=f[i+1][j-1];//会用到左下角的结果,因此遍历方向是从下往上遍历
                    res=res+f[i][j];
                }
            }
        }
        return res;

    }
};
最长回文子序列

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。 例如:bbbab最长子序列是bbbb为4

思路:

  1. 单个字符的长度是1,空字符串的长度是0,所有情况都可以从这两种基本情况演变而来
  2. s[i]==s[j]那么最长回文子序列长度等价于里面那一串的最大长度+2,即f(i,j)=f(i+1,j-1)+2;
  3. 如果不相等,那么分成用i和j两种情况讨论,可能是f(i+1,j)的长度最大,也可能是f(i,j-1)的长度最大
class Solution {
public:
    int longestPalindromeSubseq(string s) {
        vector<vector<int>> f(s.size(), vector<int>(s.size(), 0));
        int res = 1;
        for (int i = 0; i < s.size(); i++) {
            f[i][i] = 1;                 // 长度为1
            if (i) { // 长度为2
            if(s[i] == s[i - 1]){
                f[i - 1][i] = 2;
            }else{
                 f[i - 1][i] =1;
            }
                
            }
        } // 对角线上的元素已经填写好了
        for (int i = s.size() - 1; i >= 0; i--) {
            for (int j = i + 1; j < s.size(); j++) {
                if (s[i] == s[j]){
                    f[i][j]=f[i+1][j-1]+2;
                }else{
                    f[i][j] = max(f[i+1][j],f[i][j-1]);
                }
            }
        }
        return f[0][s.size()-1];
    }
};

总结:动态规划大概可以分成单串和双串两种类型,其中单串如果需要考虑子序列或者子串又可以视为双串问题。第二点是要明确递推关系,值一般都是从相邻的单元格传递过来,从左上角是最常见的,这个可以用于优化空间复杂度。股票问题是多状态问题,可以设置多个状态变量,这些状态之间可以发生从上到下的转变和继承,最优值会落在最下边一排