dp总结

复习笔记 专栏收录该内容
14 篇文章 0 订阅

文章目录

0-1背包

int beibao(int w,int n,vector<int>&weight,vector<int>&value)
{
    vector<vector<int>>dp(n+1,vector<int>(w+1,0));
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=w;j++)
        {
            // 当前背包容量装不下,只能选择不装入背包
            if(j<weight[i-1])
            {
                dp[i][j]=dp[i-1][j];
            }
            else
            {
                // 装入或者不装入背包,择优
                dp[i][j]=max(dp[i-1][j-weight[i-1]] + value[i-1] , dp[i-1][j]);

            }
        }
    }
    return dp[n][w];
}

64. 最小路径和

难度中等642收藏分享切换为英文关注反馈

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

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

示例:

输入:
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        if(grid.empty() || grid[0].empty())
            return 0;
        int row=grid.size();
        int col=grid[0].size();
        for(int i=1;i<col;i++)
        {
            grid[0][i]=grid[0][i]+grid[0][i-1];
        }
        for(int i=1;i<row;i++)
        {
            grid[i][0]=grid[i][0]+grid[i-1][0];
        }
        for(int i=1;i<row;i++)
        {
            for(int j=1;j<col;j++)
            {
                grid[i][j]=min(grid[i][j-1],grid[i-1][j])+grid[i][j];
            }
        }

        return grid[row-1][col-1];
        
    }
};

剪绳子

给你一根长度为n的绳子,请把绳子剪成m段 (m和n都是整数,n>1并且m>1)每段绳子的长度记为k[0],k[1],…,k[m].请问k[0]k[1]…*k[m]可能的最大乘积是多少?例如,当绳子的长度为8时,我们把它剪成长度分别为2,3,3的三段,此时得到的最大乘积是18.

  • O ( n 2 ) O(n^2) O(n2)时间和 O ( n ) O(n) O(n)空间的动态规划
  • O ( 1 ) O(1) O(1)时间和空间的贪婪算法

动态规划

第一步:确定最优策略,使得剪掉绳子的几段乘积最大。最后一步:乘积最大。子问题:假设该绳子剪成两段,剪完的两个部分都必须是最大乘积。

第二步:状态转移方程: dp[i] = max{ dp[i - j]*dp[j]} ,这是一个自上而下的递归公式。由于递归会有大量的不必要的重复计算。更好的办法是按照从下而上的顺序计算,也就是说我们先得到f(2),f(3),再得到f(4),f(5),直到得到f(n)。

第三步:确定初始值和边界:dp[1] = 1,dp[2] = 2, dp[3] = 3

第四步:计算顺序,从dp[3]开始到dp[n]

C++

#include<iostream>
#include<vector>
using namespace std;

class Solution {
public:
    int cuttingRope(int n) {
        if(n<1)
            return 0;
        if(n==1 || n==2)
            return 1;
        if(n==3)
            return 2;
        vector<int>dp(n+1,0);
        dp[1]=1;
        dp[2]=2;
        dp[3]=3;//初始化到3是因為3>1*2,其本身比分割的大,大于4的数分割得到乘机大于本身
        for(int i=4;i<=n;i++)
        {
            for(int j=1;j<=i/2;j++)
            {
                dp[i]=max(dp[j]*dp[i-j],dp[i]);
            }
        }

        return dp[n];
    }
};

int main(){
    int number;
    cin>>number;
    Solution s;
    int res;
    res=s.cutRope(number);
    cout<<res<<endl;
    return 0;

}

贪婪算法

按如下策略来剪绳子

  • 当n>=5,尽可能多剪长度为3的绳子
  • 当剩下的绳子长度为4,把绳子剪成两段为2的绳子
class Solution {
public:
    int cuttingRope(int n) {
        if (n < 4) return n-1;
        if (n == 4) return 4;
        long int ans = 1;
        while (n > 4) {
            n -= 3;
            ans = (ans * 3) ;
        }
        ans = (ans * n) ;
        return ans;
    }
};

221. 最大正方形

在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。

示例:

输入: 

1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0

输出: 4

用 dp[i][j] 表示直到 (i, j) 位置的可以包含最大正方形的边长,注意这里的正方形需要包含 (i, j) 位置。

在矩形左侧和上边界,如果 matrix[i][j]==‘1’,有 dp[0][j]==dp[i][0]==1,即能构成的最大正方形的边长为 1。

而对于更一般的情况:

  1. 如果 matrix[i][j]==‘0’,显然 dp[i][j]==0。
  2. 如果 matrix[i][j]==‘1’,状态转移方程为:dp[i][j]=min(min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1])+1,即 (i, j) 位置的值被其左侧、上侧、左上侧的值所限制。
class Solution {
public:
    int maximalSquare(vector<vector<char>>& matrix) {
        if(matrix.empty())
            return 0;
        int row=matrix.size();
        int col=matrix[0].size();
        vector<vector<int>>dp(row,vector<int>(col,0));
        int res=0;
        for(int i=0;i<row;i++)
        {
            for(int j=0;j<col;j++)
            {
                if(i==0 || j==0)
                    dp[i][j]=matrix[i][j]-'0';
                else if(matrix[i][j]=='1')
                {
                    dp[i][j] = min(dp[i-1][j-1] , min(dp[i-1][j],dp[i][j-1]))+1;

                }
                else
                    dp[i][j]=0;
                res=max(res,dp[i][j]);
            }
        }
        //返回面积
        return res*res;

    }
};

410. 分割数组的最大值

给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。

注意:
数组长度 n 满足以下条件:

  • 1 ≤ n ≤ 1000
  • 1 ≤ m ≤ min(50, n)

示例:

输入:
nums = [7,2,5,10,8]
m = 2

输出:
18

解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。

动态规划

在这里插入图片描述

class Solution {
public:
    int splitArray(vector<int>& nums, int m) {
        int n = nums.size();
        vector<vector<long long>> f(n + 1, vector<long long>(m + 1, LLONG_MAX));
        vector<long long> sub(n + 1, 0);
        for (int i = 0; i < n; i++) {
            sub[i + 1] = sub[i] + nums[i];
        }
        f[0][0] = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= min(i, m); j++) {
                for (int k = 0; k < i; k++) {
                    f[i][j] = min(f[i][j], max(f[k][j - 1], sub[i] - sub[k]));
                }
            }
        }
        return (int)f[n][m];
    }
};

时间复杂度: O ( n 2 × m ) O(n^2×m) O(n2×m),其中 n 是数组的长度,m 是分成的非空的连续子数组的个数。总状态数为 O ( n × m ) O(n \times m) O(n×m),状态转移时间复杂度 O ( n ) O(n) O(n),所以总时间复杂度为 O ( n 2 × m ) O(n^2 \times m) O(n2×m)

空间复杂度: O ( n × m ) O(n \times m) O(n×m),为动态规划数组的开销。

剑指 Offer 46. 把数字翻译成字符串

难度中等111给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

示例 1:

输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"

提示:

  • 0 <= num < 231

动态规划

class Solution {
public:
    int translateNum(int num) {
        //unordered_map<int,string>v;
        // for(int i=0;i<26;i++)
        //     v[i]='a'+i;
        string s=to_string(num);
        int len=s.size();
        vector<int>dp(len+1);
        dp[0]=1;
        dp[1]=1;
        for(int i=2;i<=len;i++)
        {
            //int temp=10*(s[i-2]-'0')+(s[i-1]-'0');//切记指引i不要写错
            int temp=atoi(s.substr(i-2,2).c_str());
            if(temp<26 && temp>=10)
                dp[i]=dp[i-1]+dp[i-2];
            else 
                dp[i]=dp[i-1];
        }

        return dp[len];

    }
};

解码方法

一条包含字母 A-Z 的消息通过以下方式进行了编码:

'A' -> 1
'B' -> 2
...
'Z' -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。

示例 1:

输入: “12”
输出: 2
解释: 它可以解码为 “AB”(1 2)或者 “L”(12)。
示例 2:

输入: "226"
输出: 3
解释: 它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 

image.png

class Solution {
public:
    int numDecodings(string s) {
        int l=s.size();
        if(s[0]=='0')
            return 0;
        vector<int>dp(l+1);
        dp[0]=1;
        dp[1]=1;
        
        for(int i=2;i<l+1;i++)
        {
            if(s[i-1]=='0')//dp遍历相比s提前一位
                if(s[i-2]=='1' || s[i-2]=='2')
                    dp[i]=dp[i-2];
                else 
                    return 0;
            else if(s[i-2]=='1' || (s[i-2]=='2' && s[i-1]>='1' && s[i-1]<='6'))
                dp[i]=dp[i-1]+dp[i-2];
            else
                dp[i]=dp[i-1];
        }

        return dp[l];
   
    }
};

72. 编辑距离

给你两个单词 word1 和 word2,请你计算出将 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')

在这里插入图片描述

class Solution {
public:
    int minDistance(string word1, string word2) {
        //定义dp[i][j]代表 word1 中前 i 个字符,变换到 word2 中前 j 个字符,最短需要操作的次数
        int m=word1.size(),n=word2.size();
        vector<vector<int>>dp(m+1,vector<int>(n+1));
        for(int i=0;i<m+1;i++)
        {
            dp[i][0]= i;
        }

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

        for(int i=1;i<m+1;i++)
        {
            for(int j=1;j<n+1;j++)
            {
                dp[i][j] = min(dp[i-1][j-1],min(dp[i][j-1],dp[i-1][j]))+1;
                if(word1[i-1] == word2[j-1])
                {
                    dp[i][j] = min(dp[i][j], dp[i - 1][j - 1]);
                }
            }


        }

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

312. 戳气球

有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。如果你戳破气球 i ,就可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。

求所能获得硬币的最大数量。

说明:

你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。
0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100
示例:

输入: [3,1,5,8]
输出: 167 
解释: nums = [3,1,5,8] --> [3,5,8] -->   [3,8]   -->  [8]  --> []
     coins =  3*1*5      +  3*5*8    +  1*3*8      + 1*8*1   = 167

戳气球.jpg

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0yfaqDbg-1598243577351)(C:\Users\zhaoxh\AppData\Roaming\Typora\typora-user-images\image-20200722092720857.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xMjSWFpN-1598243577355)(C:\Users\zhaoxh\AppData\Roaming\Typora\typora-user-images\image-20200722092826591.png)]

class Solution {
public:
    int maxCoins(vector<int>& nums) {
        int len = nums.size();
        nums.push_back(1);
        nums.insert(nums.begin(),1);
        
        vector<vector<int>> dp(len+2,vector<int>(len+2,0));
        for(int i=1;i<=len;i++)
        {
            dp[i][i] = nums[i-1]*nums[i]*nums[i+1];
        }
        
        for(int l=2;l<=len;l++)
        {
            for(int start=1;start<=len-l+1;start++)
            {
                int end = start+l-1;
                for(int k=start;k<=end;k++)
                {
                    int old = dp[start][end];
                    int now = nums[start-1]*nums[k]*nums[end+1] + dp[start][k-1] + dp[k+1][end];
                    dp[start][end] = max(old,now);
                }
            }
        }
        
        return dp[1][len];
    }
};

303. 区域和检索 - 数组不可变

给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。

示例:

给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()

sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3

说明:

你可以假设数组不可变。
会多次调用 sumRange 方法。

class NumArray {
private:
    vector<int>arr;
public:
    NumArray(vector<int>& nums) {
        arr.resize(nums.size()+1);
        for(int i=0;i<nums.size();i++)
        {
            arr[i+1]=arr[i]+nums[i];
        }

    }
   
    int sumRange(int i, int j) {
        return arr[j+1]-arr[i];

    }
};

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray* obj = new NumArray(nums);
 * int param_1 = obj->sumRange(i,j);
 */

304. 二维区域和检索 - 矩阵不可变

给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2)。

上图子矩阵左上角 (row1, col1) = (2, 1) ,右下角(row2, col2) = (4, 3),该子矩形内元素的总和为 8。

示例:

给定 matrix = [
  [3, 0, 1, 4, 2],
  [5, 6, 3, 2, 1],
  [1, 2, 0, 1, 5],
  [4, 1, 0, 1, 7],
  [1, 0, 3, 0, 5]
]

sumRegion(2, 1, 4, 3) -> 8
sumRegion(1, 1, 2, 2) -> 11
sumRegion(1, 2, 2, 4) -> 12

说明:

你可以假设矩阵不可变。
会多次调用 sumRegion 方法。
你可以假设 row1 ≤ row2 且 col1 ≤ col2。

动态规划(缓存)

class NumMatrix {
private:
    vector<vector<int>> dp;
public:
    NumMatrix(vector<vector<int>>& matrix) {
        if(matrix.empty())
            return;
        int row=matrix.size();
        int col=matrix[0].size();
        dp=vector<vector<int>>(row+1,vector<int>(col+1));
        for (int i = 1;i < row + 1;i++){
            for (int j = 1;j < col + 1;j++){
                dp[i][j] = dp[i][j-1] + dp [i-1][j] - dp[i-1][j-1] + matrix[i-1][j-1];
            }
        }
    }
    
    int sumRegion(int row1, int col1, int row2, int col2) {
        return dp[row2+1][col2+1] - dp[row2+1][col1] - dp[row1][col2+1] + dp[row1][col1];
    }
};

/**
 * Your NumMatrix object will be instantiated and called as such:
 * NumMatrix* obj = new NumMatrix(matrix);
 * int param_1 = obj->sumRegion(row1,col1,row2,col2);
 */

大家劫舍

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

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

示例 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 。

提示:

0 <= nums.length <= 100
0 <= nums[i] <= 400

相邻不能抢劫,抢劫第i-1个住户,就不能抢劫第i个住户

dp[i]=max(dp[i-1],dp[i-2]+nums[i])
class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.empty())
            return 0;
        if(nums.size()==1)
        {
            return nums[0];
        }
        int len=nums.size();
        //dp[i]表示抢到第i个住户的最大抢劫量
        vector<int>dp(len);
        dp[0]=nums[0];
        dp[1]=max(nums[0],nums[1]);
        for(int i=2;i<len;i++)
        {
            dp[i]= max(dp[i-1],dp[i-2]+nums[i]);
        }

        return dp[len-1];

    }
};

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
相邻不能抢劫,抢劫第i-1个住户,就不能抢劫第i个住户
在这里插入图片描述

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

    int rob(vector<int>& nums,int first,int last)
    {
        int dp1=0,dp2=0;
        for(int i=first;i<=last;i++)
        {
            int temp=dp2;
            dp2=max(dp2,dp1+nums[i]);
            dp1=temp;
        }

        return dp2;
    }
};

354. 俄罗斯套娃信封问题

给定一些标记了宽度和高度的信封,宽度和高度以整数对形式 (w, h) 出现。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。

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

说明:
不允许旋转信封。

示例:

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

动态规划

排序+最长递归子序列

class Solution {
public:
    //按照二维数组第一列的大小对每个一维数组升序排序,
    //如何第一列相同时,按照第二列大小对每行的数组降序排序
    static bool cmp(vector<int>&a,vector<int>&b){
        if(a[0]!=b[0]) 
            return a[0]<b[0];
        else 
            return a[1]<b[1];
        }
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        if(envelopes.empty())
            return 0;
        sort(envelopes.begin(),envelopes.end(),cmp);//不需要定义cmp
        int len=envelopes.size();
        vector<int>dp(len,1);
        int res=1;
        for(int i=1;i<len;i++)
        {
            for(int j=0;j<i;j++)
            {
                if(envelopes[j][0]<envelopes[i][0] && envelopes[j][1]<envelopes[i][1])
                    dp[i]=max(dp[j]+1,dp[i]);
            }   
            res=max(res,dp[i]); 

        }
        return res;
    }

动态规划+二分

  • lower_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
  • upper_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
class Solution {
public:
    //先按w排序,若w相同,则按h由高到低排序;若w不同,则按w由小到大排序 
    //若w相同,则按h由大到小排序进行排序。这样做的好处是:由于w相等,那么只有h由大到小排序才不会计算重复的子序列
    static bool cmp(vector<int>&a,vector<int>&b){
        if(a[0]!=b[0]) 
            return a[0]<b[0];
        else 
            return a[1]>b[1];
        }
    //优化:动态规划+二分法,时间复杂度O(nlogn),空间复杂度O(n)
    int maxEnvelopes(vector<vector<int>>& envelopes){
        if(envelopes.empty())return 0;
        sort(envelopes.begin(),envelopes.end(),cmp);
        vector<int> dp;
        for(auto& en:envelopes){
            //从数组的begin位置到end-1位置二分查找第一个大于或等于en[1]的数字
            int idx=lower_bound(dp.begin(),dp.end(),en[1])-dp.begin();
            if(idx>=dp.size()){
                dp.emplace_back(en[1]);
            }
            else{
                dp[idx]=en[1];
            }
        }
        return dp.size();
    }
};

丑数

把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

创建数组保存已经找到的丑数,用空间换时间,不需要在非丑数的整数上计算

丑数 *2,3,5还是丑数

class Solution {
public:
    int nthUglyNumber(int n) {
        if(n==0)
            return 0;
        vector<int>dp(n);
        dp[0]=1;
        int number_2=0,number_3=0,number_5=0;
        for(int i=1;i<n;i++)//i从1开始
        {
            dp[i]=min(min(dp[number_2]*2,dp[number_3]*3),dp[number_5]*5);
            //去除重复元素 要进行多次比较
            if(dp[i]==dp[number_2]*2)
                number_2++;
            if(dp[i]==dp[number_3]*3)
                number_3++;
            if(dp[i]==dp[number_5]*5)
                number_5++;   
        }

        return dp[n-1];
    }
};

279. 完全平方数

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

示例 1:

输入: n = 12
输出: 3 
解释: 12 = 4 + 4 + 4.

示例 2:

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

动态规划

class Solution {
public:
    int numSquares(int n) {
        vector<int>dp(n+1,n);//个数不会超过n
        dp[0]=0;
        //一次求解1,2,3...直到n的解
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j*j<=i;j++)
            {
                //dp[i - j*j]表示了i-j*j最少个数再加上这一次,与dp[i]比较取最小
                dp[i]=min(dp[i],dp[i-j*j]+1);
            }
        }

        return dp[n];
    }

};

877. 石子游戏

亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 piles[i] 。

游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。

亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。

假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true ,当李赢得比赛时返回 false 。

示例:

输入:[5,3,4,5]
输出:true
解释:
亚历克斯先开始,只能拿前 5 颗或后 5 颗石子 。
假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
如果李拿走前 3 颗,那么剩下的是 [4,5],亚历克斯拿走后 5 颗赢得 10 分。
如果李拿走后 5 颗,那么剩下的是 [3,4],亚历克斯拿走后 4 颗赢得 9 分。
这表明,取前 5 颗石子对亚历克斯来说是一个胜利的举动,所以我们返回 true 。

提示:

2 <= piles.length <= 500
piles.length 是偶数。
1 <= piles[i] <= 500
sum(piles) 是奇数。

dp数组
在这里插入图片描述

状态转移方程
在这里插入图片描述

dp[i][j][fir	or	sec]
其中:
0	<=	i	<	piles.length
i	<=	j	<	piles.length

对于这个问题的每个状态,可以做的选择有两个:选择最左边的那堆石头,
或者选择最右边的那堆石头

n = piles.length
for	0 <= i < n:
	for	j<=	i < n:
		for	who	in	{fir,	sec}:												    dp[i][j][who] = max(left,right)

base case

dp[i][j].fir = piles[i]
dp[i][j].sec = 0
其中0	<= i ==	j <	n
# 解释:i和j相等就是说面前只有一堆石头	piles[i]
# 那么显然先手的得分为	piles[i]
# 后手没有石头拿了,得分为0

算法不能简单的一行一行遍历dp数组,而要斜着遍历数组:

class Solution {
public:
    struct Pair{
        int fir;
        int sec;
    };
    bool stoneGame(vector<int>& piles) {
        int size = piles.size();
        vector<vector<Pair>> dp(size, vector<Pair>(size));
        //base case
        for(int i = 0; i < size; i++) {
            dp[i][i].fir = piles[i];
            dp[i][i].sec = 0;
        }
        //方程
        for(int l = 1; l < size; l++) {//当前斜列
            for(int i = 0; i < size - l; i++) { //行号,注意是size-l,不是size-1!!!
                int j = i + l;//列号
                int left = piles[i] + dp[i + 1][j].sec;
                int right = piles[j] + dp[i][j - 1].sec;
                // 套用状态转移方程
                if (left > right) {
                    dp[i][j].fir = left;
                    dp[i][j].sec = dp[i+1][j].fir;
                } else {
                    dp[i][j].fir = right;
                    dp[i][j].sec = dp[i][j-1].fir;
                }
            }
        }
        return dp[0][size - 1].fir;
    }
};

3.无重复字符的最长子串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

动态规划+hash表

思路和 方法1 一样,动态规划。同时利用结构unordered_map作为哈希表,统计字符目前为止最后一次出现的位置(假设递推公式计算到dp[i],那么哈希表统计的是s[i]0~i-1最后出现的位置)。这样在递推计算dp[i]的时候,避免了在 0~i-1 查找s[i]的过程,空间换时间,降低了时间复杂度。

  • 时间复杂度O(n)
  • 空间复杂度O(n)
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        if(s.empty())
            return 0;
        int len=s.size();
        vector<int>dp(len,1);
        unordered_map<char,int>mp;
        mp[s[0]]=0;
        int res=1;
        for(int i=1;i<len;i++)
        {
            if(mp.count(s[i]) == 0) 
                dp[i] = dp[i-1] +1;
            else
            {
                int d = i - mp[s[i]];
                if(d > dp[i-1]) //重复字符在以s[i-1]结尾的不重复序列前
                    dp[i] = dp[i-1] + 1;
                else //重复字符在以s[i-1]结尾的不重复序列后
                    dp[i] = d;
            }
            mp[s[i]] = i;
            res=max(res,dp[i]);
        }

        return res;

    }
};

5. 最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例 1:

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

示例 2:

输入: "cbbd"
输出: "bb"

动态规划

开辟二维数组dp,类型bool,设dp[i][j]表示s[i]~s[j]组成的字符串是否为回文子串。

初始化:先初始化一个字母和两个字母的字符串,初始化公式:

1. 一字母:dp[i][i] = true 
2. 两字母:dp[i][i+1] = (s[i] == s[i+1])

动态转移方程:

dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1] 

其他的按照动态转移方程类推,就可以得出最长回文子串。

  • 时间复杂度O(n2)
  • 空间复杂度O(n2)
class Solution {
public:
    string longestPalindrome(string s) {
        int len=s.size();
        if(len==0||len==1)
            return s;
        int start=0;//回文串起始位置
        int max=1;//回文串最大长度
        vector<vector<int>>  dp(len,vector<int>(len));//定义二维动态数组
        for(int i=0;i<len;i++)//初始化状态
        {
            dp[i][i]=1;
            if(i<len-1 && s[i]==s[i+1])
            {
                dp[i][i+1]=1;
                max=2;
                start=i;
            }
        }
        for(int l=3;l<=len;l++)//l表示检索的子串长度,等于3表示先检索长度为3的子串
        {
            for(int i=0;i+l-1<len;i++)
            {
                int j=l+i-1;//终止字符位置
                if(s[i]==s[j]&&dp[i+1][j-1]==1)//状态转移
                {
                    dp[i][j]=1;
                    start=i;
                    max=l;
                }
            }
        }
        return s.substr(start,max);//获取最长回文子串
    }
};

中心扩展算法

在数组中选取中心元素向两边扩展,直到扩展到两边的元素不相等或者到达边界,就换其他元素进行中心扩展。由于回文串的长度有奇数和偶数两种情况,因此扩展分为两种情况:

  1. 长度为奇数:以一个元素为中心,向两边扩展
  2. 长度为偶数:以两个元素为中心,向两边扩展

找到长度最长的那个回文子串即为所求。

  • 时间复杂度O(n2)
  • 空间复杂度O(1)

计算收尾节点

  • 奇数个数,i为中心

  • 偶数个数,i为中心偏左

			if (len > end - start)
			{
				start = i - (len - 1) / 2;//计算收尾节点的位置
				end = i + len / 2;
			}
class Solution {
public:
	string longestPalindrome(string s) 
	{
		if (s.length() < 1)
		{
			return "";
		}
		int start = 0, end = 0;
		for (int i = 0; i < s.length(); i++)
		{
			int len1 = expandAroundCenter(s, i, i);//一个元素为中心
			int len2 = expandAroundCenter(s, i, i + 1);//两个元素为中心
			int len = max(len1, len2);
			if (len > end - start)
			{
				start = i - (len - 1) / 2;//计算收尾节点的位置
				end = i + len / 2;
			}
		}
		return s.substr(start, end - start + 1);
	}

	int expandAroundCenter(string s, int left, int right)
	{
		int L = left, R = right;
		while (L >= 0 && R < s.length() && s[L] == s[R])
		{// 计算以left和right为中心的回文串长度
			L--;
			R++;
		}
		return R - L - 1;
	}
          
};

53. 最大子序和

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

示例:

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

进阶:

如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

动态规划

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YFZXGxRM-1598117037630)(/home/xiaohu/.config/Typora/typora-user-images/image-20200719230840653.png)]

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

        //return *max_element(dp.begin(),dp.end());
        return res;
        
    }
};

300. 最长上升子序列

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:

可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

动态规划
image.png

定义 dp[i]为考虑前 i 个元素,以第 i 个数字结尾的最长上升子序列的长度,注意 nums[i] 必须被选取。

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

image-20200720211617654

时间复杂度为: O ( n 2 ) O(n^2) O(n2)

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

        return *max_element(dp.begin(),dp.end());

    }
};

673. 最长递增子序列的个数

给定一个未排序的整数数组,找到最长递增子序列的个数。

示例 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。

动态规划

class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        if(nums.size() == 1)
        return 1;
        int N = nums.size();
        vector<int> lengths(N,1);
        int maxLen(0);
        vector<int> count(N,1);
        for(int i = 1;i < N;i++)
        {
            for(int j = 0;j < i;j++)
            {
                if(nums[j] < nums[i])
                {
                    if(lengths[j]+1 > lengths[i])//代表第一次遇到最长子序列
                    {
                        lengths[i] = lengths[j] + 1;
                        count[i] = count[j];
                    }
                    else if( lengths[j] + 1 == lengths[i]) //代表已经遇到过最长子序列
                    {
                        count[i] += count[j];//count[j]也是最长递增的子序列
                    }
                }
            }
            maxLen = max(maxLen,lengths[i]);
        }
        int res(0);
        for(int k = 0;k < N;k++)
        {
            if(lengths[k] == maxLen)
                res += count[k];
        }
        return res;
    }
};

贪心+二分

674. 最长连续递增序列

给定一个未经排序的整数数组,找到最长且连续的的递增序列,并返回该序列的长度。

示例 1:

输入: [1,3,5,4,7]
输出: 3
解释: 最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为5和7在原数组里被4隔开。 

示例 2:

输入: [2,2,2,2,2]
输出: 1
解释: 最长连续递增序列是 [2], 长度为1。
class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        if(nums.empty())
            return 0;
        int len=nums.size();
        vector<int>dp(len);
        dp[0]=1;
        for(int i=1;i<nums.size();i++)
        {
            if(nums[i]>nums[i-1])
                dp[i]= dp[i-1]+1;
            else
                dp[i]=1;  
        }

        return *max_element(dp.begin(),dp.end());
    }
};

1143. 最长公共子序列

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

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

示例 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 <= 1000
  • 1 <= text2.length <= 1000
  • 输入的字符串只含有小写英文字符。

动态规划

  • 明确 dp 数组的含义

dp[i][j] 的含义:dp(i, j) 表示在text1的前i个字符, text2的前j个字符的能组成的公共子序列的长度

  • 定义 base case

    边界: i == 0 || j == 0 , return 0

  • 状态转移方程

dp(i-1, j-1), dp(i-1, j), dp(i, j-1), dp(i-1, j-1) + 1

化简: dp(i-1, j) 和 dp(i, j-1) 包括了状态 dp(i-1, j-1), 所以只要有三个状态就行了
dp(i-1, j-1) 要在text[i] == text2[j] 的时候才可以转移过来

时间复杂度:O(MN)
空间复杂度: O(MN)

class Solution { 
public:
    int longestCommonSubsequence(string text1, string text2) {
        if(text1.empty() || text2.empty())
            return 0;
        int len1=text1.size();
        int len2=text2.size();
        vector<vector<int>>dp(len1+1,vector<int>(len2+1));
        for(int i=1;i<len1+1;i++)
        {
            for(int j=1;j<len2+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[len1][len2];
    }
};

股票相关问题:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/solution/122-mai-mai-gu-piao-de-zui-jia-shi-ji-iibao-li-tan/

121. 买卖股票的最佳时机

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。

注意:你不能在买入股票前卖出股票。

示例 1:

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

例 2:

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

动态规划

Profit Graph

用一个变量记录一个历史最低价格 minprice,跟新最低价格minprice,我们就可以假设自己的股票是在那天买的。那么我们在第 i 天卖出股票能得到的利润就是 prices[i] - minprice。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int minPrice=INT_MAX;
        int maxProfit=0;
        // for(int i=0;i<prices.size();i++)
        // {
        //     minPrice=min(minPrice,prices[i]);
        //     maxProfit=max(maxProfit,prices[i]-minPrice);
        // }
        for(auto m:prices)
        {
            minPrice=min(minPrice,m);
            maxProfit=max(maxProfit,m-minPrice);
        }
        
        return maxProfit;

    }
};

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

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

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

示例 1:

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

示例 2:

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

示例 3:

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

提示:

1 <= prices.length <= 3 * 10 ^ 4
0 <= prices[i] <= 10 ^ 4

贪心算法

思路:
从贪心算法思路看,就是逢低就买入,逢高就卖出。(记住,贪心算法就是说一个目光短浅的贪心的人,只会考虑下一步的得失,从不考虑长远的利益)
意思就是,只要第i-1天的价格小于第i天的价格(即prices[i] - prices[i-1] > 0),便在第i-1天买入,第i天卖出。
注意,假设 prices[1]=1, prices[2]=2, prices[3]=1000,即便在第一天买入第三天卖出可以获利999,而第一天买入第二天卖出只能获利1,我们也要在第一天买入第二天卖出。这就是贪心算法的思路,始终保持目光短浅,只注重眼前的利益!但这其实不会影响最终结果的,下一点会做出解释。
我们从数学的角度来看,其实第三点的思路有点类似我们高中学的“裂项相消法”的“相消”部分。假设 prices = [1,2,3,4,1000],我们知道答案是返回999,即在第一天买入,第四天卖出获利最大。但是如果我们用贪心算法,则会有:
第一天:无获利,获利0
第一天买入,第二天卖出:2-1 = 1,获利0+1=1
第二天买入,第三天卖出:3-2 = 1,获利1+1=2
第三天买入,第四天卖出:4-3=1,获利2+1=3
第四天买入,第五天卖出:1000-4=996,获利3+996=999,与第一天买入,第四天卖出所获得的利益是一样的,是不是很神奇?
其实把它们合起来,就是“相消”的写法:0+(2-1)+(3-2)+(4-3)+(1000-4)=0+2-1+3-2+4-3+1000-4=0-1+1000=999。现在理解贪心算法了吧?这是一个贪心但不笨的人~

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

        return maxProfit;
        
    }
};

动态规划

  • 第 1 步:定义状态
    状态 dp[i][j] 定义如下

    第一维 i 表示索引为 i 的那一天(具有前缀性质,即考虑了之前天数的收益)能获得的最大利润;
    第二维 j 表示索引为 i 的那一天是持有股票,还是持有现金。这里 0 表示持有现金(cash),1 表示持有股票(stock)。

  • 第 2 步:思考状态转移方程
    状态从持有现金(cash)开始,到最后一天我们关心的状态依然是持有现金(cash);
    每一天状态可以转移,也可以不动。状态转移用下图表示:

image.png

说明:

因为不限制交易次数,除了最后一天,每一天的状态可能不变化,也可能转移;
写代码的时候,可以不用对最后一天单独处理,输出最后一天,状态为 0 的时候的值即可。

  • 第 3 步:确定起始
    起始的时候:

    如果什么都不做,dp[0][0] = 0;
    如果买入股票,当前收益是负数,即 dp[0][1] = -prices[i];

  • 第 4 步:确定终止
    终止的时候,上面也分析了,输出 dp[len - 1][0],因为一定有 dp[len - 1][0] > dp[len - 1][1]。

  • 时间复杂度:O(N),这里 N*N 表示股价数组的长度。

  • 空间复杂度:O(N),虽然是二维数组,但是第二维是常数,与问题规模无关。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len=prices.size();
        if(len<2)
            return 0;
        //第二维的0:持有现金
        //第二维的1:持有股票
        //状态转移:
        vector<vector<int>>dp(len,vector<int>(2));
        dp[0][0]=0;
        dp[0][1]=-prices[0];// 买入的股票,当前收益是负的
        for(int i=1;i<len;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[len-1][0];


    }
};

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

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

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

示例 1:

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

示例 2:

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

示例 3:

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

动态规划

一维数组

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if(n <= 1)
            return 0;

        // 5个状态:0-4分别表示 未交易、买入一次、完成交易1次、买入2次、完成交易2次
        int dp[5];

        // 初始化
        dp[0] = 0;
        dp[1] = -prices[0];
        dp[2] = INT_MIN;
        dp[3] = INT_MIN;
        dp[4] = INT_MIN;
        for(int i = 1; i < n; i++)
        {
            dp[0] = 0;
            dp[1] = max(dp[1], dp[0] - prices[i]);
            dp[2] = max(dp[2], dp[1] + prices[i]);
            dp[3] = max(dp[3], dp[2] - prices[i]);
            dp[4] = max(dp[4], dp[3] + prices[i]);
        }
        return max(dp[2], dp[4]);
    }
};

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

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

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

示例 1:

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

示例 2:

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

图片.png

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rAulIiAM-1598243233773)(/home/xiaohu/.config/Typora/typora-user-images/image-20200718102847756.png)]

class Solution {
public:
    // 无限次交易(贪心算法)
    int maxProfit_inf(vector<int>& prices)
    {
        int max_profit = 0;
        for(int i = 0; i < prices.size() - 1; i++)
        {
            if(prices[i + 1] > prices[i])
            // 只要上涨就进行买卖股票
                max_profit += (prices[i + 1] - prices[i]);
        }
        return max_profit;
    }

    int maxProfit(int k, vector<int>& prices) {
        int n = prices.size();
        if(n <= 1 || k < 1)
            return 0;

        if(k > n/2)
            // 无限次交易
            return maxProfit_inf(prices);

        // 第一维k个状态:k次交易
        // 第二维2个状态:0(买入股票)、1(卖出股票)
        int dp[k][2];

        // 初始化
        for(int i = 0; i < k; i++)
        {
            dp[i][0] = INT_MIN;
            dp[i][1] = INT_MIN;
        }
            
        for(int price : prices)
        {
            dp[0][0] = max(dp[0][0], -price);             // 第 1 次买
            dp[0][1] = max(dp[0][1], dp[0][0] + price);   // 第 1 次卖
            for (int i = 1; i < k; i++) 
            {
                dp[i][0] = max(dp[i][0], dp[i - 1][1] - price); // 第 i 次买
                dp[i][1] = max(dp[i][1], dp[i][0] + price);     // 第 i 次卖
            }
        }
        return dp[k - 1][1];
    }
};

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

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例

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

二维数组DP

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if (n == 0)
            return 0;
        
        // f[i][0]: 持有股票的最大收益
        // f[i][1]: 不持有股票且处于冷冻期的累计最大收益
        // f[i][2]: 不持有股票且不处于冷冻期的累计最大收益
        vector<vector<int>> f(n, vector<int>(3));

        // 初始化
        f[0][0] = -prices[0];

        for (int i = 1; i < n; i++) 
        {
            f[i][0] = max(f[i - 1][0], f[i - 1][2] - prices[i]);
            f[i][1] = f[i - 1][0] + prices[i];
            f[i][2] = max(f[i - 1][1], f[i - 1][2]);
        }
        return max(f[n - 1][1], f[n - 1][2]);
    }
};

基于方法一的空间优化

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if (n == 0)
            return 0;
        
        // 初始化
        int f0 = -prices[0];
        int f1 = 0;
        int f2 = 0;
        for (int i = 1; i < n; i++) 
        {
            int new_f0 = max(f0, f2 - prices[i]);
            int new_f1 = f0 + prices[i];
            int new_f2 = max(f1, f2);
            f0 = new_f0;
            f1 = new_f1;
            f2 = new_f2;
        }
        return max(f1, f2);
    }
};

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

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

示例 1:

输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:  
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

注意:

0 < prices.length <= 50000.
0 < prices[i] < 50000.
0 <= fee < 50000.

二维数组DP

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();
        if(n <= 1)
            return 0;
        // 第二维的 0 表示当前持有的现金(cash), 1 表示当前持有的股票(stock)   
        vector<vector<int>> dp(n, vector<int> (2));
        // 初始化
        dp[0][0] = 0;
        dp[0][1] = -prices[0];  // 买入的股票
        for(int i = 1; i < n; i++)
        {
            dp[i][0] = max(dp[i - 1][0], prices[i] + dp[i - 1][1] - fee);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return dp[n - 1][0];
    }
};

滚动数组 优化DP(变量压缩)

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();
        if(n <= 1)
            return 0;

        //初始化:cash表示当前持有的现金,stock表示当前持有的股票
        int cash = 0;
        int stock = -prices[0];

        for (int i = 1; i < n; i++) 
        {
            cash = max(cash, stock + prices[i] - fee);
            stock = max(stock, cash - prices[i]);
        }
        return cash;
    }
};
  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值