leetcode中动态规划

 

链接:https://leetcode.cn/studyplan/dynamic-programming/

斐波那契类型

打家劫舍

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

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

示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400

分析:
我们假设dp[i]指的是前i家取得的最大值。

  1. 对于第i家房屋,如果不偷的话,那么我们就去前i-1的最大值,即dp[i-1]
  2. 对于第i家房屋,如果偷的话,那么就是找到前i-2家的最大值,然后加上nums[i],就是max(dp[1]....dp[i-2])+nums[i]
    然后对于这两种情况取一个最大值
    最后状态转移方程就是dp[i]=max(dp[i-1],max(dp[1]....dp[i-2])+nums[i])
class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.size()==1){
            return nums[0];
        }
        //dp[i]指的是前i个房子最大收获
        int n=nums.size();
        int dp[n+2];
        dp[0]=nums[0];
        dp[1]=max(nums[1],nums[0]);
        for(int i=2;i<n;i++){
            int ma=0;
            for(int j=0;j<i-1;j++){
                ma=max(ma,dp[j]);
            }
            dp[i]=max(ma+nums[i],dp[i-1]);
        }
        return dp[n-1];
    }
};

删除并获得点数

给你一个整数数组nums,你可以对它进行一些操作。
每次操作中,选择任意一个nums[i],删除它并获得nums[i]的点数。之后,你必须删除 所有 等于nums[i] - 1nums[i] + 1的元素。开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。

示例 1:
输入:nums = [3,4,2]
输出:6
解释:
删除 4 获得 4 个点数,因此 3 也被删除。
之后,删除 2 获得 2 个点数。总共获得 6 个点数。

示例 2:
输入:nums = [2,2,3,3,3,4]
输出:9
解释:
删除 3 获得 3 个点数,接着要删除两个 2 和 4 。
之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。
总共获得 9 个点数。

提示:
1<=nums.length<=2104
1<=nums[i]<=104
解题思路:

解题思路
状态表示:
dp[i] 表示删除元素i所能获得的最大点数 c[i] 表示元素i的个数

状态转移:
元素i的选择有删除和不删除两种状态:
如果删除i,获得的点数是 dp[i] = dp[i - 2] + i * c[i]
如果不删i,获得的点数是 dp[i - 1]
因此转移方程为:dp[i]=max(dp[i−1],dp[i−2]+i∗c[i])dp[i] = max(dp[i - 1],dp[i - 2] + i * c[i])dp[i]=max(dp[i−1],dp[i−2]+i∗c[i])
class Solution {
public:
    int c[10005] {} ,dp[10005] {};
    int deleteAndEarn(vector<int>& nums) {
        for(auto x : nums) c[x]++;
        int ans = 0;
        dp[1] = 1 * c[1]; //初始化
        for(int i = 2; i <= 10000; i++){
            dp[i] = max(dp[i - 2] + i * c[i],dp[i - 1]);
            ans = max(ans,dp[i]); 
        }
        return ans;
    }
};

然后这个就是我做的虽然也过了,但是显得很呆:

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

不同路径

一个机器人位于一个m×n网格的左上角 (起始点在下图中标记为"Start")。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为"Finish")。问总共有多少条不同的路径?

示例 1:
输入:m = 3, n = 7
输出:28

示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。

  1. 向右 -> 向下 -> 向下
  2. 向下 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向下

示例 3:
输入:m = 7, n = 3
输出:28

示例 4:
输入:m = 3, n = 3
输出:6

提示:

1 <= m, n <= 100
题目数据保证答案小于等于2109

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

最小路径和

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步

示例 1:
image
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12

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

矩阵

不同路径 II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 1 和 0 来表示。

示例 1:
image
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1.向右 -> 向右 -> 向下 -> 向下
2.向下 -> 向下 -> 向右 -> 向右

示例 2:
image
输入:obstacleGrid = [[0,1],[0,0]]
输出:1

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));
        for (int i = 0; i < m && obstacleGrid[i][0] != 1; i++) dp[i][0] = 1;
        for (int i = 0; i < n && obstacleGrid[0][i] != 1; i++) dp[0][i] = 1;

        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (obstacleGrid[i][j] != 1) {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                }
            }
        }
        return dp.back().back();
    }
};

三角形最小路径和

给定一个三角形 triangle ,找出自顶向下的最小路径和。

每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。

示例 1:
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

示例 2:
输入:triangle = [[-10]]
输出:-10

提示:
1<=triangle.length<=200
triangle[0].length==1
triangle[i].length==triangle[i1].length+1
104<=triangle[i][j]<=104

这个就是答案在最后一行中。

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        int n=triangle.size();
        int dp[n+4][n+4];
        for(int i=0;i<=n;i++){
            for(int j=0;j<=n;j++){
                dp[i][j]=1e9;
            }
        }
        dp[0][0]=triangle[0][0];
        for(int i=1;i<n;i++){
            for(int j=0;j<=i;j++){
                if(j==0){
                    dp[i][j]=dp[i-1][j]+triangle[i][j];
                }
                else{
                    dp[i][j]=min(dp[i-1][j],dp[i-1][j-1])+triangle[i][j];
                }
            }
        }
        int ma=1e9;
        for(int i=0;i<n;i++){
            ma=min(ma,dp[n-1][i]);
        }
        return ma;
    }
};

下降路径最小和

给你一个n×n的 方形 整数数组 matrix ,请你找出并返回通过matrix 的下降路径 的 最小和

下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置(row,col)的下一个元素应当是(row+1,col1)(row+1,col)或者(row+1,col+1)

示例 1:
image
输入:matrix = [[2,1,3],[6,5,4],[7,8,9]]
输出:13
解释:如图所示,为和最小的两条下降路径

示例 2:
image
输入:matrix = [[-19,57],[-40,-5]]
输出:-59
解释:如图所示,为和最小的下降路径

提示:

n == matrix.length == matrix[i].length
1 <= n <= 100
-100 <= matrix[i][j] <= 100

class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& matrix) {
        int n=matrix.size();
        int m=matrix[0].size();
        int dp[n+3][m+3];
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                dp[i][j]=1e8+100;
            }
        }
        for(int i=0;i<m;i++){
            dp[0][i]=matrix[0][i];
        }
        cout<<endl;
        for(int i=1;i<n;i++){
            for(int j=0;j<m;j++){
                if(j==0){
                    dp[i][j]=min(dp[i-1][j],dp[i-1][j+1])+matrix[i][j];
                }else if(j==m-1){
                    dp[i][j]=min(dp[i-1][j],dp[i-1][j-1])+matrix[i][j];
                }
                else{
                    dp[i][j]=min(dp[i-1][j],min(dp[i-1][j-1],dp[i-1][j+1]))+matrix[i][j];
                }
            }
        }
        int ans=1e9;
        for(int i=0;i<m;i++){
            ans=min(ans,dp[n-1][i]);
        }
        return ans;
    }
};

动态规划在字符串的应用

最长回文子串

给你一个字符串s,找到s中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:
输入:s = "cbbd"
输出:"bb"

提示:
1 <= s.length <= 1000
s 仅由数字和英文字母组成

做这个题一定要看清楚是最长公共子串还是最长公共子序列,最长公共子串一定要连续,最长公共子序列不需要连续
如果是最长公共子序列的话就用这个

#include<iostream>
#include<algorithm>
#include<cstring> 
using namespace std;
const int maxn=1e3+100;
int f[maxn][maxn];
char s[maxn];
int main(){
	memset(f,0,sizeof(f));
//	string s;
//	cin>>s;
//	int n=s.size();
	scanf("%s",s+1); 
	int n=strlen(s+1);
	int ansi=0,ansj=0,ma=0;
	for(int i=1;i<=n;i++){
		f[i][i]=1;
	}
	for(int len=2;len<=n;len++){//长度 
	    for(int i=1;i+len-1<=n;i++){
	        int j=i+len-1;
	        if(s[i]==s[j]){
	            f[i][j]=max(f[i][j],f[i+1][j-1]+2);
	        }
	        else{
	           	f[i][j]=max(f[i+1][j],f[i][j-1]);
			}
	        if(ma<f[i][j]){
				ma=f[i][j];
				ansi=i;
				ansj=j;
			}
	    }
	}
	printf("%d",f[1][n]);
}

最长公共子串则用这个

class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.size();
        if (n < 2) {
            return s;
        }

        int maxLen = 1;
        int begin = 0;
        // dp[i][j] 表示 s[i..j] 是否是回文串
        vector<vector<int>> dp(n, vector<int>(n));
        // 初始化:所有长度为 1 的子串都是回文串
        for (int i = 0; i < n; i++) {
            dp[i][i] = true;
        }
        // 递推开始
        // 先枚举子串长度
        for (int L = 2; L <= n; L++) {
            // 枚举左边界,左边界的上限设置可以宽松一些
            for (int i = 0; i < n; i++) {
                // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
                int j = L + i - 1;
                // 如果右边界越界,就可以退出当前循环
                if (j >= n) {
                    break;
                }

                if (s[i] != s[j]) {
                    dp[i][j] = false;
                } else {
                    if (j - i < 3) {
                        dp[i][j] = true;
                    } else {
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }

                // 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
                if (dp[i][j] && j - i + 1 > maxLen) {
                    maxLen = j - i + 1;
                    begin = i;
                }
            }
        }
        return s.substr(begin, maxLen);
    }
};

单词拆分

给你一个字符串s和一个字符串列表wordDict作为字典。请你判断是否可以利用字典中出现的单词拼接出s

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。

示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

提示:

1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s 和 wordDict[i] 仅由小写英文字母组成
wordDict 中的所有字符串 互不相同

这个题和上面的那两个差不多

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        vector<bool> dp(s.size() + 1);
        dp[0] = true;
        for(int i = 1; i <= s.size(); i++){
            for(auto& word: wordDict){
                int sz = word.size();        
                if (i - sz >= 0 && s.substr(i - sz, sz) == word)
                    dp[i] = dp[i] || dp[i - sz];            
            }       
        }
        return dp[s.size()];
    }
};

两个最长回文串的问题

这里要区分两个概念,子序列和子串,子串是要连续的。
比如说bbbab,这个bbbb是子序列,而不是子串,子串只能是bbba这种连续的

最长回文子序列

链接
给你一个字符串s,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。

示例 2:
输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。

提示:

1 <= s.length <= 1000
s 仅由小写英文字母组成

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n=s.size();
        int f[n+2][n+2];
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                f[i][j]=0;
            }
        }
        s=" "+s;
        int ansi=0,ansj=0,ma=0;
        for(int len=1;len<=n;len++){//长度 
            for(int i=1;i+len-1<=n;i++){
                int j=i+len-1;
                if(len==1){
                    f[i][j]=1;
                }
                else{
                    f[i][j]=max(f[i+1][j],f[i][j-1]);
                    if(s[i]==s[j]){
                        f[i][j]=max(f[i][j],f[i+1][j-1]+2);
                    }
                }
            }
        }
        return f[1][n];
    }
};

最长回文子串

链接
给你一个字符串s,找到s中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:
输入:s = "cbbd"
输出:"bb"

提示:

1 <= s.length <= 1000
s 仅由数字和英文字母组成

class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.size();
        if (n < 2) {
            return s;
        }

        int maxLen = 1;
        int begin = 0;
        // dp[i][j] 表示 s[i..j] 是否是回文串
        vector<vector<int>> dp(n, vector<int>(n));
        // 初始化:所有长度为 1 的子串都是回文串
        for (int i = 0; i < n; i++) {
            dp[i][i] = true;
        }
        // 递推开始
        // 先枚举子串长度
        for (int L = 2; L <= n; L++) {
            // 枚举左边界,左边界的上限设置可以宽松一些
            for (int i = 0; i < n; i++) {
                // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
                int j = L + i - 1;
                // 如果右边界越界,就可以退出当前循环
                if (j >= n) {
                    break;
                }

                if (s[i] != s[j]) {
                    dp[i][j] = false;
                } else {
                    if (j - i < 3) {
                        dp[i][j] = true;
                    } else {
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }

                // 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
                if (dp[i][j] && j - i + 1 > maxLen) {
                    maxLen = j - i + 1;
                    begin = i;
                }
            }
        }
        return s.substr(begin, maxLen);
    }
};

编辑距离

给你两个单词word1word2, 请返回将word1转换成word2所使用的最少操作数。你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符

示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

提示:
0 <= word1.length, word2.length <= 500
word1 和 word2 由小写英文字母组成

算法思路

使用动态规划 dp[i][j]表示word1中前i个字符转成word2中前j个字符所使用的最少操作数

  1. 如果word1[i-1] == word2[j-1] 则dp[i][j] = dp[i-1][j-1]
  2. 否则需考虑通过插入、删除、替换3种情况得来的最小值即可。其中,插入是dp[i][j-1],删除是dp[i-1][j],替换是dp[i-1][j-1] 考虑两种特殊情况,当word1为空时,只能通过插入操作,当word2为空只能通过删除操作

这里的代码,我们是错了一个的,为了防止0这个情况

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

两个字符串的最小ASCII删除和

给定两个字符串s1s2,返回使两个字符串相等所需删除字符的ASCII值的最小和 。

示例 1:
输入: s1 = "sea", s2 = "eat"
输出: 231
解释: 在 "sea" 中删除 "s" 并将 "s" 的值(115)加入总和。
在 "eat" 中删除 "t" 并将 116 加入总和。
结束时,两个字符串相等,115 + 116 = 231 就是符合条件的最小和。

示例 2:
输入: s1 = "delete", s2 = "leet"
输出: 403
解释: 在 "delete" 中删除 "dee" 字符串变成 "let",
将 100[d]+101[e]+101[e] 加入总和。在 "leet" 中删除 "e" 将 101[e] 加入总和。
结束时,两个字符串都等于 "let",结果即为 100+101+101+101 = 403 。
如果改为将两个字符串转换为 "lee" 或 "eet",我们会得到 433 或 417 的结果,比答案更大。

提示:
0<=s1.length,s2.length<=1000
s1s2

动态规划
dp[i][j]指的是前i个对应前j个的最小值
首先我们需要对dp[i][0]和dp[0][j]进行初始化
然后状态转移方程就是:
如果s1[i]==s2[j]``(代码中我们错开了一位,所以是s1[i-1]==s2[j-1]),那么dp[i][j]=dp[i-1][j-1]; 如果s1[i]!=s2[j]dp[i][j]=min(dp[i-1][j]+s1[i-1],dp[i][j-1]+s2[j-1]),dp[i-1][j]+s1[i-1]指的就是删除i个字符使得前i-1个和j个匹配,dp[i][j-1]+s2[j-1]`指的是删除第j个字符使得前i个和j-1个匹配

class Solution {
public:
    int minimumDeleteSum(string s1, string s2) {
        int n=s1.size(),m=s2.size();
        int dp[n+2][m+2];//前i个和前j个相等的最小的权值
        for(int i=0;i<=n;i++){
            for(int j=0;j<=m;j++){
                dp[i][j]=0;
            }
        }
        dp[0][0]=0;
        for(int i=1;i<=n;i++){
            dp[i][0]=dp[i-1][0]+s1[i-1];
        }
        for(int j=1;j<=m;j++){
            dp[0][j]=dp[0][j-1]+s2[j-1];
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                if(s1[i-1]==s2[j-1]){
                    dp[i][j]=dp[i-1][j-1];
                }
                else{
                    dp[i][j]=min(dp[i-1][j]+s1[i-1],dp[i][j-1]+s2[j-1]);
                }
            }
        }
        return dp[n][m];
    }
};

不同的子序列

给你两个字符串 st ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 109+7 取模。

示例 1:
输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
rabbbit
rabbbit
rabbbit

示例 2:
输入:s = "babgbag", t = "bag"
输出:5
解释:
如下所示, 有 5 种可以从 s 中得到 "bag" 的方案。
babgbag
babgbag
babgbag
babgbag
babgbag

提示:

1 <= s.length, t.length <= 1000
s 和 t 由英文字母组成

class Solution {
public:
    int numDistinct(string s, string t) {
        int n=s.size();
        int m=t.size();
        int dp[n+2][m+2];
        int mod=1e9+7;
        
        for(int i=0;i<=n;i++){
            for(int j=0;j<=m;j++){
                dp[i][j]=0;
            }
        }
        for (int i = 0; i <= n; i++) {
            dp[i][0] = 1;
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                if(s[i-1]==t[j-1]){
                    dp[i][j]=(dp[i-1][j]+dp[i-1][j-1])%mod;
                }
                else{
                    dp[i][j]=dp[i-1][j]%mod;
                }
            }
        }
        return dp[n][m];
    }
};

背包

划分为k个相等的子集

链接
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100

这个就是一个01背包的题目,设dp[0]=1,如果dp[j-nums[i]]==1,dp[j]=1,
最后看看dp[sum/2]是不是等于0

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int dp[20010];
        for(int i=0;i<=20001;i++){
            dp[i]=0;
        }   
        int sum=0;
        for(int i=0;i<nums.size();i++){
            sum+=nums[i];
        } 
        if(sum%2==1){
            return false;
        }   
        sum/=2;
        dp[0]=1;
        for(int i=0;i<nums.size();i++){
            for(int j=sum;j>=nums[i];j--){
				//或者dp[i] = dp[i] || dp[i - num];
                if(dp[j-nums[i]]==1){
                    dp[j]=1;
                }
            }
        }
        if(dp[sum]){
            return true;
        }
        else{
            return false;
        }
    }
};

目标和

这个和上面的那个差不多
给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 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
+1 + 1 + 1 + 1 - 1 = 3

示例 2:
输入:nums = [1], target = 1
输出:1

提示:
1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000

我们想要的 S = 正数和 - 负数和 = x - y
而已知 x 与 y 的和是数组总和:x + y = sum 
可以求出 x = (S + sum) / 2 = target 也就是我们要从 nums 数组里选出几个数,令其和为 targettarget 间接给出)。 于是转化为是否可以用 nums 中的数组合和成 target01 背包问题,外层循环为选择池 nums,内层循环为 target。 dp[i] 表示和为 i 的 num 组合有 dp[i] 种。

外层遍历 nums 每个 num;
内层遍历 target(由大到小)。
对于元素之和等于 i - num 的每一种排列,在最后添加 num 之后即可得到一个元素之和等于 i 的排列,因此在计算 dp[i] 时,应该计算所有的 dp[i − num] 之和。 dp[i] = dp[i] + dp[i - num] 对于边界条件,我们定义 dp[0] = 1 表示只有当不选取任何元素时,元素之和才为 0,因此只有 1 种方案。 最后返回 dp[target]

代码:

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

最长递增子序列

最长递增子序列

给你一个整数数组nums,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1

提示:

1<=nums.length<=105
104<=nums[i]<=104

其实这是一个贪心,假如说我们的最长序列s=[3,5,6,7],假如我们的nums[i]=4,那么我们可以将最长序列中的5用4替换掉,变成s=[3,4,6,7],这是一个贪心的过程,因为你本来那个位置上就有一个了,我们将他替换掉是是没事的,在后续我们可能还会找一个更长的

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n=nums.size();
        int s[n+2];
        int l=0;
        for(int i=0;i<n;i++){
            if(l==0||s[l-1]<nums[i]){
                s[l++]=nums[i];
            }
            else{
                int z=lower_bound(s,s+l,nums[i])-s;//返回第一个大于等于nums[i]的下标
                s[z]=nums[i];
            }
        }
        return l;
    }
};

最长递增子序列的个数

给定一个未排序的整数数组nums, 返回最长递增子序列的个数 。
注意 这个数列必须是严格递增的。

示例 1:
输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。

示例 2:
输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。

提示:

1 <= nums.length <= 2000
106<=nums[i]<=106

定义dp[i] 表示以nums[i]结尾的最长上升子序列的长度,cnt[i]表示以 nums[i] 结尾的最长上升子序列的个数。设nums的最长上升子序列的长度为 maxLen,那么答案为所有满足dp[i]=maxLeni 所对应的cnt[i]之和。

我们从小到大计算dp 数组的值,在计算 dp[i] 之前,我们已经计算出dp[0i1]的值,则状态转移方程为:

dp[i]=max(dp[j])+1,0j<inum[j]<num[i]

即考虑往dp[0i1] 中最长的上升子序列后面再加一个nums[i]。由于 dp[j] 代表 [0j] 中以 nums[j] 结尾的最长上升子序列,所以如果能从 dp[j] 这个状态转移过来,那么nums[i] 必然要大于 nums[j],才能将 nums[i] 放在 nums[j] 后面以形成更长的上升子序列。

对于cnt[i],其等于所有满足dp[j]+1=dp[i]cnt[j] 之和。在代码实现时,我们可以在计算 dp[i] 的同时统计 cnt[i] 的值。

class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        int n=nums.size();//dp[i]表示以nums[i]结尾的最长上升子序列的长度
        int dp[n+2],cnt[n+2],maxLen = 0, ans = 0;
        for(int i=0;i<n;i++){
            dp[i]=1;
            cnt[i]=1;
            for(int j=0;j<i;j++){
                if(nums[i]>nums[j]){
                    if(dp[j]+1>dp[i]){
                        dp[i]=dp[j]+1;
                        cnt[i]=cnt[j];//重置计数
                    }
                    else if(dp[j]+1==dp[i]){
                        cnt[i]+=cnt[j];
                    }
                }
            }
            if(dp[i]>maxLen){
                maxLen=dp[i];
                ans=cnt[i];
            }
            else if(dp[i]==maxLen){
                ans+=cnt[i];
            }
        }
        
    }
};

最长数对链

给你一个由 n 个数对组成的数对数组pairs,其中pairs[i]=[lefti,righti]lefti<righti

现在,我们定义一种 跟随 关系,当且仅当b<c时,数对p2=[c,d]才可以跟在p1=[a,b]后面。我们用这种形式来构造 数对链 。

找出并返回能够形成的 最长数对链的长度 。

你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

示例 1:
输入:pairs = [[1,2], [2,3], [3,4]]
输出:2
解释:最长的数对链是 [1,2] -> [3,4] 。

示例 2:
输入:pairs = [[1,2],[7,8],[4,5]]
输出:3
解释:最长的数对链是 [1,2] -> [4,5] -> [7,8] 。

提示:

n == pairs.length
1 <= n <= 1000
-1000 <= lefti < righti <= 1000

对于这个有两种解法:

  1. 动态规划
    定义 dp[i] 为以 pairs[i] 为结尾的最长数对链的长度。计算 dp[i] 时,可以先找出所有的满足 pairs[i][0]>pairs[j][1]j,并求出最大的 dp[j]dp[i] 的值即可赋为这个最大值加一。这种动态规划的思路要求计算 dp[i] 时,所有潜在的 dp[j] 已经计算完成,可以先将 pairs 进行排序来满足这一要求。初始化时,dp 需要全部赋值为1
class Solution {
public:
    int findLongestChain(vector<vector<int>>& pairs) {
        int n = pairs.size();
        sort(pairs.begin(), pairs.end());
        vector<int> dp(n, 1);
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (pairs[i][0] > pairs[j][1]) {
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
        }
        return dp[n - 1];
    }
};
  1. 贪心

要挑选最长数对链的第一个数对时,最优的选择是挑选第二个数字最小的,这样能给挑选后续的数对留下更多的空间。挑完第一个数对后,要挑第二个数对时,也是按照相同的思路,是在剩下的数对中,第一个数字满足题意的条件下,挑选第二个数字最小的。按照这样的思路,可以先将输入按照第二个数字排序,然后不停地判断第一个数字是否能满足大于前一个数对的第二个数字即可。

如果是贪心的话这个题就和那个活动安排差不多,按照righti,因为你想想如果是按照lifti排序的话,那么如果第一个lifti很小,而righti很大的话,这样肯定不是最优的。

class Solution {
public:
    int findLongestChain(vector<vector<int>>& pairs) {
        int n = pairs.size();
        sort(pairs.begin(), pairs.end(),[&](vector<int> a, vector<int> b){
            if(a[1]==b[1])return a[0]<b[0];
            return a[1]<b[1];
        });
        int cnt=INT_MIN,ans=0;
        for(int i=0;i<pairs.size();i++){
            if(cnt<pairs[i][0]){
                ans++;
                cnt=pairs[i][1];
            }
        }
        return ans;
    }
};

最长定差子序列

给你一个整数数组arr和一个整数difference,请你找出并返回arr中最长等差子序列的长度,该子序列中相邻元素之间的差等于difference

子序列是指在不改变其余元素顺序的情况下,通过删除一些元素或不删除任何元素而从arr派生出来的序列。

示例 1:
输入:arr = [1,2,3,4], difference = 1
输出:4
解释:最长的等差子序列是 [1,2,3,4]。

示例 2:
输入:arr = [1,3,5,7], difference = 1
输出:1
解释:最长的等差子序列是任意单个元素。

示例 3:
输入:arr = [1,5,7,8,5,3,4,2,1], difference = -2
输出:4
解释:最长的等差子序列是 [7,5,3,1]。

提示:

1<=arr.length<=105
104<=arr[i],difference<=104

这个题可以用map,但是还有一个很巧妙的hash的方法

class Solution {
public:
    int longestSubsequence(vector<int>& arr, int difference) {
        int n=arr.size();
        map<int,int>mp;
        for(int i=0;i<n;i++){
            mp[arr[i]]=mp[arr[i]-difference]+1;
        }
        int ans=0;
        for(int i=0;i<n;i++){
            ans=max(ans,mp[arr[i]]);
        }
        return ans;
    }
};

就是如果用数组的话,它需要减去一个数,然后里面可能会有负数,所以我们让x+20001,这样就保证不会出现负数了。

class Solution {
public:
    int longestSubsequence(vector<int>& arr, int difference) {
        int maxLength = 0;
        // dp[i]:以arr[i]结尾的LSS的长度
        vector<int> dp(40001);
        for(auto x:arr){
            // 继承前一个等差元素的LSS长度,加1
            dp[x + 20001] = dp[x + 20001 - difference] + 1;
            maxLength = max(maxLength,dp[x + 20001]);
        }
        return maxLength;
    }

};

最长等差数列

给你一个整数数组nums,返回nums中最长等差子序列的长度。

回想一下,nums的子序列是一个列表nums[i1],nums[i2],...,nums[ik] ,且0<=i1<i2<...<ik<=nums.length1。并且如果seq[i+1]seq[i](0<=i<seq.length1)的值都相同,那么序列seq是等差的。

示例 1:
输入:nums = [3,6,9,12]
输出:4
解释:
整个数组是公差为 3 的等差数列。

示例 2:
输入:nums = [9,4,7,2,10]
输出:3
解释:
最长的等差子序列是 [4,7,10]。

示例 3:
输入:nums = [20,1,15,3,10,5,8]
输出:4
解释:
最长的等差子序列是 [20,15,10,5]。

提示:

2 <= nums.length <= 1000
0 <= nums[i] <= 500

题解:

dp[i][j]表示以nums[i]结尾,公差为j-500的最长等差子序列长度,然后状态转移方程就是int j = nums[i] - nums[k] + 500;dp[i][j] = max(dp[i][j], dp[k][j] + 1);

class Solution {
public:
    int longestArithSeqLength(vector<int>& nums) {
        int n = nums.size();
        int dp[n][1001];//dp[i][j]表示以nums[i]结尾,公差为j-500的最长等差子序列长度
        memset(dp, 0, sizeof(dp));
        int ans = 0;
        for (int i = 1; i < n; ++i) {
            for (int k = 0; k < i; ++k) {
                int j = nums[i] - nums[k] + 500;
                dp[i][j] = max(dp[i][j], dp[k][j] + 1);
                ans = max(ans, dp[i][j]);
            }
        } 
        return ans + 1;
    }
};

俄罗斯套娃信封问题(二维最长递增子序列)

给你一个二维整数数组envelopes,其中envelopes[i]=[wi,hi],表示第i个信封的宽度和高度。

当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。

请计算 最多能有多少个 信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。

注意:不允许旋转信封。

示例 1:
输入:envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出:3
解释:最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。

示例 2:
输入:envelopes = [[1,1],[1,1],[1,1]]
输出:1

提示:

1<=envelopes.length<=105
envelopes[i].length==2
1<=wi,hi<=105

这个只需要将第一维排序,然后第二位求最长递增子序列就行

class Solution {
public:
    int lower(int *q,int l,int r,int x){
        int ans;
        while(r>=l){
            int mid=(l+r)/2;
            if(q[mid]>=x){
                ans=mid;
                r=mid-1;
            }
            else{
                l=mid+1;
            }
        }
        return ans;
    }
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        int n=envelopes.size();
        if(envelopes.empty()){
            return 0;
        }
        sort(envelopes.begin(), envelopes.end(), [](vector<int>& a, vector<int>& b){
            if(a[0] == b[0]){
                // 对于宽度相等的信封,根据高度逆序,大的在前小的在后
                return a[1] > b[1];
            }
            return a[0] < b[0];
        });
        int s=0,num;
        int q[101000];
        for(int i=0;i<n;i++){
            if(s==0||envelopes[i][1]>q[s]){
                q[++s]=envelopes[i][1];
            }
            else{
                num=lower(q,0,s,envelopes[i][1]);
                q[num]=envelopes[i][1];
            }
        }
        return s;
    };
    
};

最长公共子序列

最长公共子序列

给定两个字符串text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

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

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

示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。

示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。

示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。

提示:

1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。

dp[i][j]指的是text1中前i个和text2中前j个的最长公共子序列的长度
然后状态转移方程就是:
如果text1[i]==text2[j]:dp[i][j]=dp[i-1][j-1]+1
否则,dp[i][j]=max(dp[i][j-1],dp[i-1][j])

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int n=text1.size(),m=text2.size();
        int dp[n+2][m+2];
        for(int i=0;i<=n;i++){
            for(int j=0;j<=m;j++){
                dp[i][j]=0;
            }
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;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[n][m];
    }
};

不相交的线

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

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

nums1[i]==nums2[j]

且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。

示例 1:
image
输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。
但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。

示例 2:
输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
输出:3

示例 3:
输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
输出:2

class Solution {
public:
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        int n=nums1.size(),m=nums2.size();
        int dp[n+2][m+2];
        for(int i=0;i<=n;i++){
            for(int j=0;j<=m;j++){
                dp[i][j]=0;
            }
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                if(nums1[i-1]==nums2[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[n][m];
    }
};

买卖股票的最佳时间/状态机

买卖股票的最佳时机

链接
给定一个数组prices,它的第i个元素prices[i]表示一支给定股票第i天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:

1<=prices.length<=105
0<=prices[i]<=104
这个只需要维护一个前i个的最小值就行,然后让每个后面的i都减去这个最小值,然后再取个最大值

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int Min=1e9;
        int Max=0;//维护一个最小值
        for(int i=0;i<prices.size();i++){
            Min=min(Min,prices[i]);
            Max=max(Max,prices[i]-Min);
        }
        return Max;
    }
};

买卖股票的最佳时机 II

链接
给你一个整数数组prices,其中prices[i]表示某支股票第i天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候最多只能持有一股股票。你也可以先购买,然后在同一天出售。

返回 你能获得的最大利润 。

示例 1:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。

示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
总利润为 4 。

示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。

提示:
1<=prices.length<=3104
0<=prices[i]<=104

这道题是典型的状态机,这个题要按照状态分析:
状态dp[i][j]定义如下:
dp[i][j]表示到下标为i的这一天,持股状态为j时,我们手上拥有的最大现金数。
其中:

  • 第一维i表示下标为i的那一天( 具有前缀性质,即考虑了之前天数的交易 );
  • 第二维j表示下标为i的那一天是持有股票,还是持有现金。这里 0 表示持有现金(cash),1 表示持有股票(stock)。

然后状态转移方程就是:

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<int>& prices) {
        // 0:持有现金
        // 1:持有股票
        int n=prices.size();
        int dp[n+2][2];
        memset(dp,0,sizeof(dp));
        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];
    }
};

买卖股票的最佳时机 III

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/description/

给定一个数组,它的第i个元素是一支给定的股票在第i天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。

示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。

示例 4:
输入:prices = [1]
输出:0

提示:
1<=prices.length<=105
0<=prices[i]<=105

这个也是状态机的题:
所以定义状态转移数组dp[天数][当前是否持股][卖出的次数]

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        ///所以定义状态转移数组dp[天数][当前是否持股][卖出的次数]
        int n=prices.size();
        int dp[n+3][3][3];
        memset(dp,0,sizeof(dp));
        //第一天休息
        dp[0][0][0]=0;
        //第一天买入
        dp[0][1][0]=-prices[0];
        // 第一天不可能已经有卖出
        //这里为什么+100000, 因为 在下面dp[i-1][0][1] - prices[i] 可能会溢出,这是根据测试用例调整的
        dp[0][0][1] = INT_MIN+100000;
        dp[0][0][2] = INT_MIN;
        // 第一天不可能已经卖出
        dp[0][1][1]=INT_MIN;
        dp[0][1][2]=INT_MIN;
        for(int i=1;i<n;i++){
            //未持股,未卖出过,说明从未进行过买卖
            dp[i][0][0]=0;
            //未持股,卖出过1次,可能是今天卖的,可能是之前卖的
            dp[i][0][1]=max(dp[i-1][1][0]+prices[i],dp[i-1][0][1]);
            //未持股,卖出过2次,可能是今天卖的,可能是之前卖的
            dp[i][0][2]=max(dp[i-1][1][1]+prices[i],dp[i-1][0][2]);
            //持股,未卖出过,可能是今天买的,可能是之前买的
            dp[i][1][0]=max(dp[i-1][0][0]-prices[i],dp[i-1][1][0]);
            //持股,卖出过1次,可能是今天买的,可能是之前买的
            dp[i][1][1]=max(dp[i-1][0][1]-prices[i],dp[i-1][1][1]);
            //持股,卖出过2次,不可能
            dp[i][1][2]=INT_MIN;
        }
        return max(max(dp[n-1][0][1],dp[n-1][0][2]),0);
    }
};

买卖股票的最佳时机 IV

给你一个整数数组prices和一个整数k,其中prices[i]是某支给定的股票在第i的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成k笔交易。也就是说,你最多可以买k 次,卖k次。

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

示例 1:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

示例 2:
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。

提示:

1 <= k <= 100
1 <= prices.length <= 1000
0 <= prices[i] <= 1000

题解:

我们使用一系列变量存储「买入」的状态,再用一系列变量存储「卖出」的状态,通过动态规划的方法即可解决本题。
我们用buy[i][j]表示对于数组prices[0..i]中的价格而言,进行恰好 j笔交易,并且当前手上持有一支股票,这种情况下的最大利润;用sell[i][j]表示恰好进行j笔交易,并且当前手上不持有股票,这种情况下的最大利润。
那么我们可以对状态转移方程进行推导。对于buy,我们考虑当前手上持有的股票是否是在第i天买入的。如果是第i天买入的,那么在第i1天时,我们手上不持有股票,对应状态 sell[i1][j],并且需要扣除prices[i]的买入花费;如果不是第i天买入的,那么在第i1天时,我们手上持有股票,对应状态buy[i1][j]。那么我们可以得到状态转移方程:

buy[i][j]=max{buy[i1][j],sell[i1][j]price[i]}

同理对于sell[i][j],如果是第i天卖出的,那么在第i1天时,我们手上持有股票,对应状态buy[i1][j1],并且需要增加prices[i]的卖出收益;如果不是第i天卖出的,那么在第i1天时,我们手上不持有股票,对应状态 sell[i1][j]。那么我们可以得到状态转移方程:

sell[i][j]=max{sell[i1][j],buy[i1][j1]+price[i]}

由于在所有的n天结束后,手上不持有股票对应的最大利润一定是严格由于手上持有股票对应的最大利润的,然而完成的交易数并不是越多越好(例如数组prices单调递减,我们不进行任何交易才是最优的),因此最终的答案即为sell[n1][0..k]中的最大值。

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        if (prices.empty()) {
            return 0;
        }

        int n = prices.size();
        k = min(k, n / 2);
        vector<vector<int>> buy(n, vector<int>(k + 1));
        vector<vector<int>> sell(n, vector<int>(k + 1));

        buy[0][0] = -prices[0];
        sell[0][0] = 0;
        for (int i = 1; i <= k; ++i) {
            buy[0][i] = sell[0][i] = INT_MIN / 2;
        }

        for (int i = 1; i < n; ++i) {
            buy[i][0] = max(buy[i - 1][0], sell[i - 1][0] - prices[i]);
            for (int j = 1; j <= k; ++j) {
                buy[i][j] = max(buy[i - 1][j], sell[i - 1][j] - prices[i]);
                sell[i][j] = max(sell[i - 1][j], buy[i - 1][j - 1] + prices[i]);   
            }
        }

        return *max_element(sell[n - 1].begin(), sell[n - 1].end());
    }
};

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

给定一个整数数组prices,其中第prices[i]表示第i天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

示例 2:
输入: prices = [1]
输出: 0

提示:

1 <= prices.length <= 5000
0 <= prices[i] <= 1000

题解:
我们用f[i]表示第i天结束之后的「累计最大收益」。根据题目描述,由于我们最多只能同时买入(持有)一支股票,并且卖出股票后有冷冻期的限制,因此我们会有三种不同的状态:

  • 我们目前持有一支股票,对应的「累计最大收益」记为f[i][0]
  • 我们目前不持有任何股票,并且处于冷冻期中,对应的「累计最大收益」记为f[i][1]
  • 我们目前不持有任何股票,并且不处于冷冻期中,对应的「累计最大收益」记为f[i][2]

这里的「处于冷冻期」指的是在第i天结束之后的状态。也就是说:如果第i天结束之后处于冷冻期,那么第i+1天无法买入股票。

如何进行状态转移呢?在第 iii 天时,我们可以在不违反规则的前提下进行「买入」或者「卖出」操作,此时第 iii 天的状态会从第 i−1i-1i−1 天的状态转移而来;我们也可以不进行任何操作,此时第 iii 天的状态就等同于第 i−1i-1i−1 天的状态。那么我们分别对这三种状态进行分析:

  • 对于f[i][0],我们目前持有的这一支股票可以是在第i1天就已经持有的,对应的状态为 f[i1][0];或者是第i天买入的,那么第i1天就不能持有股票并且不处于冷冻期中,对应的状态为f[i1][2]加上买入股票的负收益prices[i]。因此状态转移方程为:

f[i][0]=max(f[i1][0],f[i1][2]prices[i])

  • 对于f[i][1],我们在第i天结束之后处于冷冻期的原因是在当天卖出了股票,那么说明在第 i−1i-1i−1 天时我们必须持有一支股票,对应的状态为f[i1][0] 加上卖出股票的正收益 prices[i]。因此状态转移方程为:

f[i][1]=f[i1][0]+prices[i]

  • 对于f[i][2],我们在第i天结束之后不持有任何股票并且不处于冷冻期,说明当天没有进行任何操作,即第i1天时不持有任何股票:如果处于冷冻期,对应的状态为 f[i1][1];如果不处于冷冻期,对应的状态为f[i1][2]。因此状态转移方程为:

f[i][2]=max(f[i1][1],f[i1][2])

这样我们就得到了所有的状态转移方程。如果一共有n天,那么最终的答案即为:

max(f[n1][0],f[n1][1],f[n1][2])

注意到如果在最后一天(第n1天)结束之后,手上仍然持有股票,那么显然是没有任何意义的。因此更加精确地,最终的答案实际上是f[n1][1]f[n1][2]中的较大值,即:

max(f[n1][1],f[n1][2])

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        // dp[i][0]: 手上持有股票的最大收益
        // dp[i][1]: 手上不持有股票,并且处于冷冻期中的累计最大收益
        // dp[i][2]: 手上不持有股票,并且不在冷冻期中的累计最大收益
        int n=prices.size();
        int dp[n+2][3];
        memset(dp,0,sizeof(dp));
        dp[0][0]=-prices[0];
        for(int i=1;i<n;i++){
            dp[i][0]=max(dp[i-1][0],dp[i-1][2]-prices[i]);
            dp[i][1]=f[i-1][0]+prices[i];
            
        }
    }
};

背包

完全平方数

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

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4

示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9

提示:
1<=n<=104

这个题就是将平方数打个表然后再进行背包就行

class Solution {
public:
    int numSquares(int n) {
        int q[300];
        int cnt=0;
        for(int i=1;i*i<=n;i++){
            q[++cnt]=i*i;
        }
        int dp[102000];
        for(int i=1;i<102000;i++){
            dp[i]=INT_MAX;
        }
        dp[0]=0;
        for(int i=1;i<=cnt;i++){
            for(int j=q[i];j<=n;j++){
                dp[j]=min(dp[j],dp[j-q[i]]+1);
            }
        }
        return dp[n];
    }
};

零钱兑换 II

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。

示例 1:
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

示例 2:
输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。

示例 3:
输入:amount = 10, coins = [10]
输出:1

提示:

1 <= coins.length <= 300
1 <= coins[i] <= 5000
coins 中的所有值 互不相同
0 <= amount <= 5000

这道题中,给定总金额textitamount和数组coins,要求计算金额之和等于amount的硬币组合数。其中,coins的每个元素可以选取多次,且不考虑选取元素的顺序,因此这道题需要计算的是选取硬币的组合数。

可以通过动态规划的方法计算可能的组合数。用dp[x]表示金额之和等于x的硬币组合数,目标是求dp[amount]

动态规划的边界是dp[0]=1。只有当不选取任何硬币时,金额之和才为0,因此只有 1种硬币组合。

对于面额为coin的硬币,当coiniamount时,如果存在一种硬币组合的金额之和等于icoin,则在该硬币组合中增加一个面额为coin的硬币,即可得到一种金额之和等于i的硬币组合。因此需要遍历coins,对于其中的每一种面额的硬币,更新数组dp中的每个大于或等于该面额的元素的值。

由此可以得到动态规划的做法:

初始化dp[0]=1

遍历coins,对于其中的每个元素coin,进行如下操作:

遍历icoin\(\textit{amount\),将dp[icoin]的值加到dp[i]

最终得到dp[amount]的值即为答案。

上述做法不会重复计算不同的排列。因为外层循环是遍历数组coins的值,内层循环是遍历不同的金额之和,在计算dp[i]的值时,可以确保金额之和等于i的硬币面额的顺序,由于顺序确定,因此不会重复计算不同的排列。

例如,coins=[1,2],对于dp[3]的计算,一定是先遍历硬币面额1后遍历硬币面额2,只会出现以下2种组合:

3=1+1+13=1+2
硬币面额2不可能出现在硬币面额1之前,即不会重复计算3=2+1的情况

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        int n=coins.size();
        int dp[amount+4];
        for(int i=0;i<=amount+1;i++){
            dp[i]=0;
        }
        dp[0]=1;
        for(int i=0;i<n;i++){
            for(int j=coins[i];j<=amount;j++){
                dp[j]+=dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
};

组合总和 Ⅳ

给你一个由不同整数组成的数组nums,和一个目标整数target。请你从nums中找出并返回总和为target的元素组合的个数。
题目数据保证答案符合 32 位整数范围。

示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。

示例 2:
输入:nums = [9], target = 3
输出:0

提示:

1 <= nums.length <= 200
1 <= nums[i] <= 1000
nums 中的所有元素 互不相同
1 <= target <= 1000

这个题需要将循环target放在外层,将nums[j]放在内层。想象一下我们先遍历i的时候,对于i-1的nums[j]的每一个都算上了,所以这个是有顺序的

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        int n=nums.size();
        int dp[target+3];
        memset(dp,0,sizeof(dp));
        dp[0]=1;
        for(int i=1;i<=target;i++){
            for(int j=0;j<n;j++){
                if(i>=nums[j]&& dp[i - nums[j]] < INT_MAX - dp[i]){
                    dp[i]+=dp[i-nums[j]];
                }
            }
        }
        return dp[target];
    }
};

一维

解决智力问题(倒序填表 / 正序刷表)

给你一个下标从 0 开始的二维整数数组 questions ,其中$questions[i] = [points_i, brainpower_i] 0ipoints_ibrainpower_ibrainpowerii$,你可以对下一个问题决定使用哪种操作。

比方说,给你 questions = [[3, 2], [4, 3], [4, 4], [2, 5]] :
如果问题 0 被解决了, 那么你可以获得 3 分,但你不能解决问题 1 和 2 。
如果你跳过问题 0 ,且解决问题 1 ,你将获得 4 分但是不能解决问题 2 和 3 。
请你返回这场考试里你能获得的 最高 分数。

示例 1:
输入:questions = [[3,2],[4,3],[4,4],[2,5]]
输出:5
解释:解决问题 0 和 3 得到最高分。

  • 解决问题 0 :获得 3 分,但接下来 2 个问题都不能解决。
  • 不能解决问题 1 和 2
  • 解决问题 3 :获得 2 分
    总得分为:3 + 2 = 5 。没有别的办法获得 5 分或者多于 5 分。

示例 2:
输入:questions = [[1,1],[2,2],[3,3],[4,4],[5,5]]
输出:7
解释:解决问题 1 和 4 得到最高分。

  • 跳过问题 0
  • 解决问题 1 :获得 2 分,但接下来 2 个问题都不能解决。
  • 不能解决问题 2 和 3
  • 解决问题 4 :获得 5 分
    总得分为:2 + 5 = 7 。没有别的办法获得 7 分或者多于 7 分。

提示:
1<=questions.length<=105
questions[i].length==2
1<=pointsi,brainpoweri<=105

这题乍一看是dp,但是你仔细一看,她不是,动态规划讲究的是无后效性,但是你看看这个题,她对后面是有影响的所以正着不是动态规划,但是如果你反过来看的话,他就变成了动态规划的题目,反过来,也就是你倒着遍历的话,就变成了动态规划的题目

对于任意问题 i而言 dp[i] 表示完成 [0,i] 题能获得的最高分,另外每个问题都有解决和跳过两种选择
如果解决问题i则其得分dp[i]+questions[i][0]可以转移给下一道可解的题j使用(其中 j=questions[i][1]+i+1,转移时dp[j]取二者最大值即dp[j]=Math.max(dp[i]+questions[i][0],dp[j]),如果跳过问题i则其得分dp[i]可以转移给下一道题i+1使用,转移时dp[i+1]取二者最大值即dp[i+1]=Math.max(dp[i],dp[i+1]);

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

零钱兑换

给你一个整数数组coins,表示不同面额的硬币;以及一个整数amount,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

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

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

示例 2:
输入:coins = [2], amount = 3
输出:-1

示例 3:
输入:coins = [1], amount = 0
输出:0

提示:
1<=coins.length<=12
1<=coins[i]<=2311
0<=amount<=104

我们采用自下而上的方式进行思考。仍定义F(i)为组成金额i所需最少的硬币数量,假设在计算 F(i)之前,我们已经计算出F(0)F(i1)的答案。 则F(i)对应的转移方程应为
F(i)=minj=0n1F(icj)+1
其中cj代表的是第 jjj 枚硬币的面值,即我们枚举最后一枚硬币面额是cj,那么需要从 icj这个金额的状态F(icj)转移过来,再算上枚举的这枚硬币数量1的贡献,由于要硬币数量最少,所以F(i)为前面能转移过来的状态的最小值加上枚举的硬币数量1。

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

或者这个可以看成完全背包问题:

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int dp[100100];
        dp[0]=0;
        for(int i=1;i<=amount;i++){
            dp[i]=0x3f3f3f3f;
        }
        // for(int i=1;i<=amount;i++){
        //     for(int j=0;j<coins.size();j++){
        //         if(i>=coins[j]) dp[i]=min(dp[i],dp[i-coins[j]]+1);
        //     }
        // }
        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);
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
};

交错字符串

https://leetcode.cn/problems/interleaving-string/description/
给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。
两个字符串 s 和 t 交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:

  • s = s1 + s2 + ... + sn
  • t = t1 + t2 + ... + tm
  • |n - m| <= 1

交错 是 s1 + t1 + s2 + t2 + s3 + t3 + ... 或者 t1 + s1 + t2 + s2 + t3 + s3 + ...
注意:a + b 意味着字符串 a 和 b 连接。

示例 1:
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true

示例 2:
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false

示例 3:
输入:s1 = "", s2 = "", s3 = ""
输出:true

提示:
0 <= s1.length, s2.length <= 100
0 <= s3.length <= 200
s1、s2、和 s3 都由小写英文字母组成

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int n=s1.size(),m=s2.size(),t=s3.size();
        int f[n+2][m+2];
        memset(f,0,sizeof(f));
        if(n+m!=t){
            return false;
        }
        f[0][0]=true;
        for(int i=0;i<=n;i++){
            for(int j=0;j<=m;j++){
                int p=i+j-1;
                if(i>0){
                    f[i][j]|=(f[i-1][j]&&s1[i-1]==s3[p]);
                }
                if(j>0){
                    f[i][j]|=(f[i][j-1]&&s2[j-1]==s3[p]);
                }
            }
        }
        return f[n][m];
    }
};
posted @   lipu123  阅读(16)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示