LeetCode刷题集锦

5. 最长回文子串

题目描述

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

示例 1:

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

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

解题思路

①想到最优子结构

大字符串是回文,把首尾字符取去掉后小字符串也是回文 aba是回文,取首尾后子串b也是

②最小子问题

只有一个字母,必定是回文

有两个子母且两个字母相等才是回文

③状态转移方程 最小子问题如何推出大问题 $$ dp(i,j)=\begin{cases} true & i==j只有一个字母 \
str[j]==str[i] & j-i==1两个子母 \
dp(i+1,j-1)且str[j]==str[i] &j-i>1 大于两个字母 \end{cases} $$ ④实现动态规划

dp二维数组表示【i,j】范围子串是否为回文,注意遍历的顺序,当大于两个字母时要左下的数组值才能使用状态转移,所以遍历从最后行自左向右遍历

实现代码

func longestPalindrome(s string) string {
    result_s,result_e := 0,0
    length := len(s)
    dp := make([][]bool, length)

  
    for i:=0; i <= length-1; i++ {
        dp[i] = make([]bool, length)
    }

    for i:=length-1;i>=0;i-- {
        for j:=i;j<=length-1;j++ {
            if i==j || (j-i==1 && s[i]==s[j]) || (j-i>1 && dp[i+1][j-1] && s[i]==s[j]){
                dp[i][j] = true
                if(j-i > result_e-result_s) {
                    result_s,result_e = i,j
                }
            }else {
                dp[i][j] = false
            }
        }
    }

    return s[result_s:result_e+1]

}

62. 不同路径

题目描述

点链接

解题思路

①想到最优子结构

到图中某一点的步数取决于其上方和左方两个位置的走法

②最小子问题

在起点,不用走只有一种走法

③状态转移方程 最小子问题如何推出大问题 $$ dp(i,j)=\begin{cases} 1 & i=0||j=0 \
dp(i-1,j)+dp(i,j-1) &j!=0||i!=0>1 \end{cases} $$ ④实现动态规划

实现代码

注:实现代码多用了一行一列省去了对边界条件的判断

func uniquePaths(m int, n int) int {
     dp := make([][]int, m+1)
     for i:=0;i<m+1;i++ {
         dp[i] = make([]int,n+1)
     }
    dp[0][1]=1
    for i:=1;i<m+1;i++{
        for j:=1;j<n+1;j++{
            dp[i][j]=dp[i-1][j]+dp[i][j-1]
        }
    }
    return dp[m][n]

}

63. 不同路径 II

题目描述

点链接

解题思路

①想到最优子结构

到图中某一点的步数取决于其上方和左方两个位置的走法

②最小子问题

在起点,不用走只有一种走法

在障碍物点上,可能的结果为0中

③状态转移方程 最小子问题如何推出大问题 $$ dp(i,j)=\begin{cases} 1 & (i=0||j=0)且obstacleGrid[i][j]==0 \
0 & obstacleGrid[i][j]==1\
对应上一步位置不是障碍物的点dp(i-1,j)+dp(i,j-1) &obstacleGrid[i][j]==0且(i!=0)且j!=0 \end{cases} $$ ④实现动态规划

实现代码

func uniquePathsWithObstacles(obstacleGrid [][]int) int {
    m,n := len(obstacleGrid),len(obstacleGrid[0])
     dp := make([][]int, m)
     for i:=0;i<m;i++ {
         dp[i] = make([]int,n)
     }
     //初始化
     if(obstacleGrid[0][0]==0){
        dp[0][0] = 1
    }
    
     //迭代
     for i:=0;i<m;i++ {
         for j:=0;j<n;j++ {
             if(obstacleGrid[i][j]==1){//该点不可达
                 dp[i][j]=0
             }else {//该点可达
                if(isValiated(i-1,j,m,n)&&obstacleGrid[i-1][j]==0){
                    dp[i][j] += dp[i-1][j]
                }
                if(isValiated(i,j-1,m,n)&&obstacleGrid[i][j-1]==0){
                    dp[i][j]+=dp[i][j-1]
                }
             }
         }
     }

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

}

func isValiated(i, j , m, n int) bool {
    if(i<0 || j<0||i>=m ||j>=n){
        return false
    }
    return true
}

64. 最小路径和

题目描述

解题思路

先想到最优子结构,到某一点路径和最小其实就是到上、左两侧点最小路径和 中的最小值+本点的值

然后想最小子问题

然后想状态转移方程,如何大问题如何化为更小的问题

最后代码实现 可以自底向上迭代 也可自顶向下递归 +备忘录

实现代码

class Solution {
    public int minPathSum(int[][] grid) {
        if (grid == null || grid.length < 1 || grid[0] == null || grid[0].length < 1) {
            return 0;
        }
        
        int row = grid.length;
        int col = grid[row - 1].length;
        
        int dp[][] = new int[row][col];
        
        dp[0][0] = grid[0][0];
        
        for (int i = 1;i < row;i++) {
            dp[i][0] = dp[i - 1][0] + grid[i][0];
        }
        
        for (int i = 1;i < col;i++) {
            dp[0][i] = dp[0][i - 1] + grid[0][i];
        }
        
        for (int i = 1;i < row;i++) {
            for (int j = 1;j < col;j++) {
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
            }
        }
        
        return dp[row - 1][col - 1];
    }
}

91. 解码方法

题目描述

解题思路

求最优子结构:长的字符的编码可能等于 删去一个字符、删去两个字符的可能之和

最小子问题:只有一个字符时 为0 没有可能 不为0 有1中可能

状态转移方程: $$ dp(i)=\begin{cases} 1 & i==0||i==1\
dp[i-1]+dp[i-2] &i>1 \end{cases} \备注:dp[i-1]条件:str[i]!=‘0’;dp[i-2]条件:str[i]==‘1’||(str[i]==2&&‘0’<=str[i-1]<=‘6’) $$

实现代码

func numDecodings(s string) int {
    if(s[0] == '0') {
        return 0
    }
    dp := make([]int, len(s)+1)
    dp[0],dp[1] = 1,1 //0是为了当只有两个字符时如果删去两个字符dp[0]表示算为有一种可能

    for i:=2; i<len(s)+1; i++ {

        if(s[i-1]!='0'){
            dp[i] = dp[i] + dp[i-1]
        }
        if(s[i-2]=='1'||(s[i-2]=='2' && s[i-1]<='6' && s[i-1]>='0')) {
                dp[i] = dp[i]+dp[i-2]
        }

    }
    return dp[len(s)]
}

难点备注

当只有两个数字时对于dp[i-2]会出现越界的情况,所以dp数组比字符串长度多一位,用最后一位为1表示当两个数字可以解码成字母时会+1

120. 三角形最小路径和

解题思路:

最优子结构 最小子问题 状态转移方程 迭代+dp数组代码实现

实现代码

func minimumTotal(triangle [][]int) int {
    if(triangle==nil) {
        return 0
    }
    width,length := len(triangle),len(triangle) 
    dp  := make([][]int, width)
    for i:=0;i<width;i++ {
        dp[i] = make([]int, length)
    }

    dp[0][0] = triangle[0][0]
    for i:=1; i<width;i++{
        for j:=0; j<=i;j++{
            dp[i][j] = 100000
            if(j-1>=0) {
                dp[i][j] = dp[i-1][j-1] +triangle[i][j]
            }
             if(j <= i-1 &&dp[i][j] > dp[i-1][j] + triangle[i][j]) {
                dp[i][j] = dp[i-1][j] + triangle[i][j]
            }
        }
    }
    min := dp[width-1][0]
    for i:=1;i<length;i++ {
        if(min > dp[width-1][i]) {
            min = dp[width-1][i]
        }
    }
    return min
}

空间压缩优化

dp[i][j]=min(dp[i-1][j-1], dp[i-1][j])本层和上面一整层有关系,而一层最多有n各个元素,所以空间可压缩到O(n)

所以先想到两个一维数组的方法

func minimumTotal(triangle [][]int) int {
    if(triangle==nil) {
        return 0
    }
    length := len(triangle)
    if(length == 1) {
        return triangle[0][0]
    }
    dp  := make([][]int, 2) // 压缩空间
    dp[0] = make([]int, length)
    dp[1] = make([]int, length)

    dp[0][0] = triangle[0][0]
    for i:=1; i<length;i++{
        for j:=0; j<=i;j++{
            min := 100000
            if(j-1>=0) {
                min =  dp[0][j-1]+triangle[i][j]
            }
             if(j <= i-1 &&min > dp[0][j] + triangle[i][j]) {
                min = dp[0][j] + triangle[i][j]
            }
            dp[1][j] = min
        }
        for k:=0;k<length;k++{
            dp[0][k] = dp[1][k]
        }

    }
    min := dp[1][0]
    for i:=1;i<length;i++ {
        if(min > dp[1][i]) {
            min =dp[1][i]
        }
    }
    return min
}

继续优化

使用奇数偶数可以省去循环中dp[1][]导入到dp[0][]

进一步优化的话是第二层循环j的那一层使用倒着遍历方法,只用一个数组就行可以避免dp[j]用到dp[j] 、dp[j-1]正向遍历

dp[1]改了dp[1],但是dp[2]还要用dp[1] 如果倒着遍历则可以不影响了。可见逆向思维

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

解题思路:

①找最优子结构:

这个题不太好找, 自变量、因变量的确定

首先是变量种类多:天数、最大利益、好几种不同的选择

这个时候我们不能一眼看出最优子结构,不想走到某地的走法数,有明显的递推关系

第n天的最大利益取决于前面一系列的选择,且第n天取最大利益和第1天取最大利益可能不一样

一方面当我们定了一个自变量和因变量(状态)时,要进行验证是否合适(如何判断合不合适?)

另一方面当一眼看不出来后就不能靠猜的了,此时第一步先把所有选择对应的转换情况写出如下:

第n天可能的情况,则第n+1天的可能情况

image -w150 -

通过图我们知道什么因变量(状态)彼此相关可以递推呢?一天的三种状态,到下一天的转换

这个和我们的最大利益值有关系吗?知道第1天三种状态的利益,那第二天三种状态的利益自然也可以知道,对于第二天的卖的状态,是由第一天卖第一天持有股票什么也不做得到的,那 我们只保留最大的利益

这样每一天的三种状态的最大利益就可以递推了

这样依次推到第n天的利益

所以状态自变量定义为三种状态的最大利益 因变量为天数

总结:最优子结构为如果今天卖股票 最大利益 等于 昨天买入股票最大利益和昨天什么不做持有股票的最大利益 两者最大值

②最小子问题,

第一天 没有股票不在冷冻期:0 有股票不在冷冻期:0(这个不存在,递推第二天是要忽略的) 没有股票但在冷冻期:0 (这个是不存在的在第一天,递推第二天时要忽略掉)

为了递推时要对第一天特殊处理,所以把第二天直接写出来

第二天 没有股票不在冷冻期:0 有股票不在冷冻期:max(-prices[0],-price[1]) 没有股票但在冷冻期:price[1]-price[0]

③递推公式

$$ 没有股票不在冷冻期:dp[0][n]=max(dp[0][n-1],dp[2][n-1]) \
有股票不在冷冻期:dp[1][n]=max(dp[0][n-1]-prices[i],dp[1][n-1]) \
没有股票但在冷冻期:dp[2][n]=max(dp[1][n]+prices[i])

\备注:当n大于1时才执行递推,否则就按最小子问题处理 $$

在画图基础上定下自变量和因变量和我们猜的,如天数、最大利益(无法递推); 天数+每天做的选择、最大利益(没有递推关系),这个最大利益做为因变量怎么都无法递推,因为最大利益和每一天的选择都有关系

代码编写

func maxProfit(prices []int) int {
    if prices==nil||len(prices)==0||len(prices)==1{
        return 0
    }

    dp := make([][]int, 3)
    for i:=0;i<3;i++ {
        dp[i] = make([]int, len(prices))
    }
    
    dp[0][1] = 0
    dp[1][1] = max(-prices[1],-prices[0])
    dp[2][1] = prices[1]-prices[0]
    for i:=2;i<len(prices);i++ {
        dp[0][i] = max(dp[0][i-1],dp[2][i-1])
        dp[1][i]=max(dp[0][i-1] - prices[i],dp[1][i-1])
        dp[2][i]=dp[1][i-1]+prices[i]
    }
    return max(dp[0][len(prices)-1], dp[2][len(prices)-1])
}
func max (a,b int) int {
    if a>b {
        return a
    }else{
        return b
    }
}

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

解题思路

每天有两种状态 可买、可卖

实现代码

func maxProfit(prices []int) int {
    if(prices == nil || len(prices)==0) {
        return 0
    }
    dp := make([][]int,2)
    dp[0] = make([]int, len(prices))
    dp[1] = make([]int, len(prices))
    dp[0][0],dp[1][0] = 0, 0-prices[0]
    for i:=1;i<len(prices);i++ {
        dp[0][i] = max(dp[0][i-1], dp[1][i-1]+prices[i])
        dp[1][i] = max(dp[1][i-1], dp[0][i-1]-prices[i])
    }
    return dp[0][len(prices)-1]

}

func max(a,b int) int{
    if a>b{
        return a
    }
    return b
}

121. 买卖股票的最佳时机

解题思路

每日有三个状态 没买过可以买股票、买过但还没有卖、买过而且也卖了 彼此之间转化递推,

如果当天某一状态要去最大利益,取决于昨天可以变为该状态的最大利益加上相应操作带来的影响

实现代码

    func maxProfit(prices []int) int {
        if(prices==nil || len(prices)==0) {
            return 0
        }
        length := len(prices) 
        dp := make([][]int, 3)
        for i:=0;i<3;i++{
            dp[i]=make([]int, length)
        }
        dp[0][0],dp[1][0],dp[2][0] = 0,0-prices[0],0
        for i:=1;i<length;i++ {
            dp[0][i] = 0
            dp[1][i] = max(dp[1][i-1], dp[0][i-1]-prices[i])
            dp[2][i] = max(dp[2][i-1], dp[1][i-1]+prices[i])
        }
        return dp[2][length-1]
    }

    func max(a, b int)  int{
        if(a>b) {
            return a
        }
        return b
    }

322. 零钱兑换

解题思路

凑大钱由凑小钱转移而来 ,有1、2、5三种硬币, 凑11元钱 最少 肯定是 凑11-1 11-2 11-5三种情况中最少硬币数+1

如果三种都没办法凑,那11块钱也没办法凑

最小子问题 dp[0] = 0

$$ dp[i]=min\begin{cases} dp[i-coin[1]] & i-coin[1]>=0\ ……\
dp[i-coin[2]] & i-coin[2]>=0 \end{cases}+1\或者当没有i-coin>=0时dp[i]为-1,凑不成 $$

实现代码

func coinChange(coins []int, amount int) int {
    dp := make([]int, amount+1)
    dp[0] = 0
    for i:=1;i<=amount;i++ {
        min := 10000 //表示不可达
        for _, j := range coins {
            if(i-j >= 0 && dp[i-j]!=-1 &&dp[i-j]<min) {
                min = dp[i-j]
            }
        }
        //如果仍是最大值  表示没有可达
        if min == 10000 {
            dp[i] = -1
        }else {
            dp[i] = min+1
        }
    }
    return dp[amount]
}

198. 打家劫舍

解题思路

不能连续偷东西, 每一间房子有两种状态 (可以偷不会触发警报 、 不可以偷会触发警报)

每经过一间房子都会有什么也不做、偷东西两种动作

最小问题 只有一间房子

dp[0][0] = 0 //第1间可以偷不会触发警报

状态转移方程

dp[1][0] = nums[0] //第1间 不可以偷会触发警报 $$ dp[0][i] = \begin{cases} max(dp[1][i-1],dp[0][i-1]) & i-1房 什么也不做,i-1偷过东西\end{cases} \

dp[1][i] = \begin{cases} dp[0][i-1]+nums[i] &i-1房 可以偷 \end{cases} $$

实现代码

  func rob(nums []int) int {
        if(nums==nil ||len(nums)==0){
            return 0
        }
        length := len(nums)
        dp:=make([][]int, 2)
        dp[0] =make([]int, length)
        dp[1] =make([]int,length)
        dp[0][0],dp[1][0] = 0,nums[0]
        for i:=1;i<length;i++ {
            dp[0][i]=max(dp[0][i-1],dp[1][i-1])
            dp[1][i]=dp[0][i-1]+nums[i]
        }
        return max(dp[0][length-1],dp[1][length-1])
    }
    func max(a, b int) int {
        if a>b{
            return a
        }
        return b
    }

343. 整数拆分

解题思路

这道题怎么说,不是多阶段决策达到最优解这一类型,而是大问题化小问题的分治法问题

大数N的拆分的最大乘机 如果只拆成两个数 有 1,n-1 2,n-2 3,n-3 …… [n/2-1,n/2] 奇数偶数情况不一致

拆成两个数后又可再拆,那就是子问题,如2,n-2 n-2的拆分方式一定是最大乘积

所以大数N的拆分用到了子问题拆分情况

但是注意一点 大数N 拆成2,N-2 后 N-2可以继续拆也可以不拆 这是两种情况

对于 N-2,2 拆不拆2

对于2,N-2拆不拆N-2

总结

最小子问题是n=0 n=1是最大乘积为0(不可以拆分成两个正整数) n=2时 最大乘积为1

之后对于n>2,大数N拆成两个数的所有情况 如2,N -2 可以①N-2不拆,2*N-2 ②N-2拆 子问题拆(N-2)*2

所以状态转移方程为

$$ 状态转移方程 \
dp[n] = \begin{cases} max \big[ i*(n-i),i*dp[n-i]) \big] && n >2 \end{cases} \备注i从1遍历到n-1 $$

实现代码

func integerBreak(n int) int {
    dp := make([]int, n+1)
    dp[0], dp[1], dp[2] = 0, 0, 1
    for i:=3;i<=n;i++ {
        tmp_max := 0
        for j:=1;j<i;j++ {
            no_chai,chai :=j*(i-j),j*dp[i-j]
            tmp_max = max(no_chai, chai, tmp_max)
        }
        dp[i] = tmp_max
    }
    return dp[n]
}

//三个数求最大
func max(x,y,z int) int {
    max := x
    if y>max {
        max =y
    }
    if z>max {
        max =z
    }
    return max 
}

分析问题:对于不是分阶段决策的问题,而是分治法大问题套用小问题的解这种情况,要先写出递归解法,把规律找到

如本题 拆分大问题 只拆成两个数i,n-i 、 继续拆n-i 两种情况最大值 继续拆n-i可以递归调用

我们将原问题抽象为 f(n) 那么 f(n) 等价于 max(1 * f(n - 1), 2 * f(n - 2), …, (n - 1) * f(1)) + 只拆分成两个数1*n-1 2*n-2 ……的情况。

152. 乘积最大子数组

解题思路

暴力解决就是遍历所有i,j的组合可能

这道题还是要向分治方法求解问题,找出大小问题之间的关联

以i为结尾连续子数组的最大乘积,和以i-1结尾的最大乘积之间的关系

可以用以i-1结尾的最大乘积*nums[i],也可以不用前i-1个元素只用nums[i]

备注:这是如何想出来的?连乘关系,nums[i]可能为正负,前面的连乘可能为正负,我们的nums[i]可以并入前面或者不并入前面,

这里很容易忽略第三种情况即 以i-1为结尾的最小乘积*nums[i]负负得正变成了最大,综上 以i为结尾的最大乘积的设为dp[0][i],以i为结尾的最小乘积设为dp[1][i],有三种可能得到

①dp[0][i-1]*nums[i]

②dp[1][i-1]*nums[i]

③nums[i]

对于dp[1][i-1]也是从上面的三种可能得到

最小子问题: dp[0]][0]=nums[i] dp[1][0]=nums[i]

状态转移方程 $$ dp[0][i]=MAX\big[ (dp[0][i-1]*nums[i]), (dp[1][i-1]*nums[i], nums[i]) \big]\
dp[0][i]=MIN\big[ (dp[0][i-1]*nums[i]), (dp[1][i-1]*nums[i]), nums[i] \big] \从两种情况中取最大值、最小值 $$

实现代码

func maxProduct(nums []int) int {
    dp := make([][]int, 2)
    dp[0] = make([]int, len(nums))
    dp[1] = make([]int, len(nums))
    dp[0][0], dp[1][0] = nums[0], nums[0]
    for i:=1; i < len(nums);i++ {
        dp[0][i] = Max(dp[0][i-1]*nums[i], dp[1][i-1]*nums[i], nums[i])
        dp[1][i] = Min(dp[0][i-1]*nums[i], dp[1][i-1]*nums[i], nums[i])
    }
    max := -100000
    for _,i:=range dp[0] {
        if max < i {
            max = i
        }
    }
    return max
}

func Max(x, y, z int) int {
    max := x
    if y>max {
        max =y
    }

    if z>max {
        max =z
    }
    return max
}

func Min(x, y, z int) int {
    min := x
    if y<min {
        min =y
    }

    if z<min {
        min =z
    }
    return min
}

300. 最长上升子序列

解题思路

自变量:考虑到第i个数字

状态考虑:

①前i个数最长上升子序列的长度

状态之间没有关联,知道前i的最长,但是前i+1的也没有足够的 信息推出来——因为我们不知道前i最长是那几个数,第i+1的数又是否可以接上去

②以第i个数结尾的最长上升子序列长度

知道前i个数结尾的最长长度,可以判断第i+1个数是否可以接到后面,

第i+1个数比后面i个数大则可以接上,总可以接上的所有可能中选出最长的情况

最小子问题:dp[0]=1 只有一个数字

转载leetcode的图https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/tu-jie-zui-chang-shang-sheng-zi-xu-lie-by-cheng-xu/

2595a56ba8d47cd9d4690bd2125cea57840a7515d0d900a4ee2c939f1191cc11-图片

$$ 状态转移方程\
dp[i]=\begin{cases} MAX \bigg(dp[j]+1\bigg) &j从0到i-1,且nums[i]>nums[j] \end{cases} \
如果nums[i]比前面的nums[j]都大则dp[i]=1 $$

实现代码

func lengthOfLIS(nums []int) int {
    if len(nums)==0 {
        return 0
    }
    result := 1
    dp := make([]int, len(nums))
    dp[0] = 1
    for i:=1;i<len(nums);i++ {
        max := 1
        for j:=0; j<i; j++{
            if dp[j]+1 > max && nums[i] >  nums[j] {
                max = dp[j]+1
            }
        }
        dp[i] = max
        if result < dp[i] {
            result = dp[i]
        }
    }
    return result
    
}

复杂度为O(n2),空间复杂度O(n)

进一步优化

将复杂度变为O(nlogn)

思路:贪心算法+二分查找

贪心算法要每次做的选择得到最优解,连续做的最优选择就时最后的解

我们每次做的最有选择就是 尽可能的缩短我们上升子序列的最大值

当nums[i+1] > nums[i],可以放入一个数据

当nums[i+1] <=nums[i] 将其替换第一个小于nums[i]的数,这样我们可以尽可能的多放进

354. 俄罗斯套娃信封问题

将其按w递增排序,同样的h按递减排序就变成了最长上升子序列问题

实现代码

import (
	"fmt"
	"sort"
)
func maxEnvelopes(envelopes [][]int) int {
    sort.Slice(envelopes, func (i, j int)bool {
		if envelopes[i][0]==envelopes[j][0]{
			return envelopes[i][1] > envelopes[j][1]
		}
		return envelopes[i][0] < envelopes[j][0]
    })
    return maxLength(envelopes[:][1])

}

//最长上升子序列
func maxLength(nums []int) int{ 
    length := len(nums)
    if length == 0 {
        return 0
    }
    dp := make([]int ,length)
    dp[0] = 1
    for i:=1;i<length;i++ {
        dp[i] = 1
        for j:=0;j<i;j++ {
            if nums[i]>nums[j] && dp[i] < dp[j]+1 {
                dp[i] = dp[j]+1
            }
        }
    
    }
    max := dp[0]
    for k:=0;k<length;k++ {
        if max < dp[k] {
            max = dp[k]
        }
    }
    return max
}

引申

对于三维的嵌套问题,则要去引入树状数组这一结构来解决问题,问题更复杂

213. 打家劫舍 II

解题思路

和打家劫舍很像,只是第1和第n间房有冲突关系

我们可以假设两种情况

第一间偷,第2不可偷 第3~第n-1间房用动态规划求得最大价值

第一间不偷,第2~第n间房用动态规划求得最大价值

动态规划问题就是不能连续偷的情况下可获得的最大价值

image-20201209215711213

每一间房有两个状态dp[0]没动,dp[1]偷了 $$ 没动dp[0][i]= max\bigg(dp[0][i-1]+nums[i],dp[1][i-1]\bigg)

\
偷了dp[i][i-1]=dp[0][i-1] $$ 对于第一天偷第二天不偷,从第三天开始算

第三天 没动状态 dp[1][2] 为nums[0]

第三天 偷了状态 不存在 dp[1][2] 特别小到第四天直接舍去 (因为第二天没有偷)

对于第一天不偷,从第二天开始算

第二天没动 dp[0][1]=0

第二天偷了状态 dp[1][1]=nums[1]

实现代码

代码不是很好,优化方案:对于动态规划重复逻辑进行复用

func rob(nums []int) int {
    length := len(nums)
    if length==0 {
        return 0
    }
    if length==1 {
        return nums[0]
    }

    dp := make([][]int, 2)
    dp[0], dp[1] = make([]int, length), make([]int, length)
    dp[0][0], dp[1][0],dp[0][1], dp[1][1] = nums[0], 0, nums[0], 0 //第一天偷了,第二天没偷
    for i:=2;i<length-1;i++ {
        dp[0][i] = max(dp[0][i-1], dp[1][i-1])
        dp[1][i] = dp[0][i-1] + nums[i]
    }
    maxVal := max(dp[0][length-2], dp[1][length-2])
    // 重置为0
    for i:=0;i<length;i++ {
        dp[0][i], dp[1][i] = 0, 0
    }
    dp[0][0], dp[1][0] = 0, 0 // 第一天没投
    for i:=1;i<length;i++ {
        dp[0][i] = max(dp[0][i-1], dp[1][i-1])
        dp[1][i] = dp[0][i-1] + nums[i]
        
    }
    maxVal2 := max(dp[0][length-1], dp[1][length-1])

    return max(maxVal, maxVal2)
    
}

func max(a, b int) int {
    if a>b {
        return a
    }
    return b
}

优化代码如下

func rob(nums []int) int {
    length := len(nums)
    if length==0 {
        return 0
    }
    if length==1 {
        return nums[0]
    }
    if length<=3 { // 对于长度为2的nums[2:length-1]会 出现越界异常   长度为3则是nums[2:length-1]会把第三房偷的情况也返回
        return max(nums[0],  MyRob(nums[1:length]))
    }
    return max(MyRob(nums[2:length-1])+nums[0], MyRob(nums[1:length]))
    
}

func MyRob(nums[] int) int{
    length := len(nums)
    if length==0 {
        return 0
    }
    if length==1 {
        return nums[0]
    }
    dp := make([][]int, 2)
    dp[0],dp[1] = make([]int ,length), make([]int, length)

    dp[0][0], dp[1][0] = 0, nums[0]
    for i:=1;i<length;i++ {
        dp[0][i] = max(dp[0][i-1], dp[1][i-1])
        dp[1][i] = dp[0][i-1] + nums[i]
        
    }
    return  max(dp[0][length-1], dp[1][length-1])

}

func max(a, b int) int {
    if a>b {
        return a
    }
    return b
}

也可以是思路

在不偷窃第一个房子的情况下(即 nums[1:]nums[1:]),最大金额是 p1

在不偷窃最后一个房子的情况下(即 nums[:n-1]nums[:n−1]),最大金额是 p2 综合偷窃最大金额: 为以上两种情况的较大值,即 max(p1,p2)

作者:jyd 链接:https://leetcode-cn.com/problems/house-robber-ii/solution/213-da-jia-jie-she-iidong-tai-gui-hua-jie-gou-hua-/ 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

337. 打家劫舍 III

解题思路:

树状dp问题,第一次遇见啊。

每一个节点定义两种状态,已偷和没动

状态转移如下:

image-20201210201251324

当前节点没动和已偷两种状态下各自可获得的最大价值

自变量是每一个节点,因变量是遍历顺序可以获取最大的价值

难点是原来是数组状态转移方程就是数组dp[i],但是现在是二叉树。

先看状态转移方程 $$ 没动dp[i][0] = 所有子节点\sum max(dp[i-1][0],dp[i-1][1]) 当父节点没动子节点可偷可不偷 \
已偷dp[i][1]=所有子节点\sum dp[i-1][0] +nums[i]当父节点已偷子节点都不能偷 $$ 父节点可以获取的两种状态价值取决于子节点,要先知两个子节点再求父节点—–后续遍历

//伪代码
递归函数func(入参节点node) (返回  do已偷状态价值, undo没动状态价值) {
	if node ==null { 
		//节点为空,两个返回0
    }
    
    leftDo, leftUndo =  func(左节点)的已偷 没动可获价值
    rightDo, rightUndo =  func(右节点)的已偷 没动可获价值
    
	//当前节点已偷=子节点没有偷的情况+当前节点的价值
    curDo = leftUndo + rightDo + nums[cur] 
    //当前节点没动=子节点偷或不偷两种状态可获价值最大值
    curUndo = max(leftDo, leftUndo)+max(rightDo, rightUndo)  
}

这样我们可以获取到根节点的两种状态的最大价值,取个最大值即可

实现代码

/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func rob(root *TreeNode) int {
    return max(doRob(root))
}

func doRob(node *TreeNode) (int, int) {
    if node == nil {
        return 0, 0
    }
    lundo, ldo := doRob(node.Left)
    rundo, rdo := doRob(node.Right)    
    return  max(lundo, ldo) + max(rundo, rdo), lundo+rundo+node.Val
}

func max(a,b int) int {
    if a>b {
        return a
    }
    return b
}

375. 猜数字大小 II

解题思路

MiniMAX—最小化对方最大收益的问题,比如下棋如何下会使对方的收益最小,博弈树

我们的任务是猜数,那无疑会有好多种情况, 我们猜的顺序和对方选择的数都是不同的组合 问题求的是我们最多花多少钱稳赢

这里给一下其他人的解释:原链接https://www.cnblogs.com/fayin/p/10407176.html

题目下方给出了几个提示:

  • 游戏的最佳策略是减少最大损失,这引出了 Minimax 算法,见这里,和这里
  • 使用较小的数开始(例如3),看看在最差的情况下你要支付多少钱?
  • 即使 n 比较小,完全使用递归的效率也很低,试试动态规划吧。

我们就按照上面的提示玩一把:猜数字大小 II。

当 n = 3,那么我们有 3 个选择:1 或 2 或 3。

假设我们先猜 1,就有两种情况:

  • [猜对]:1 就是正确的数字,所以你支付 0¥,或者
  • [猜错]:1 不是正确数字,于是你需要支付 1¥(现在,你知道那个正确的数字 > 1,因为每次猜完后你都将被告知猜大了还是猜小了。但就目前的情况来看正确的那个数肯定比 1 大),于是我们得到了一个子问题(2,3)——在[2,3]范围内猜出正确的数字。运用递归,我们可以使用同样的方法解决这个问题,此时你可以选择 2 或 3。如果选择 2,你又有两个可能的结果:2 是正确的数字于是你支付 0¥,或者 2 不是正确的数字那么你支付 2¥ ,现在你知道正确的数字是 3 了(因为只剩下 3 这个数字)。如果选择 3,要么 3 就是正解要么你支付 3¥ 于是你知道 2 才是正确的答案。总结一下,选择 2 最多支付 2¥,选择 3 最多支付 3¥。两相比较,在支付最多的情况下,我们选择那个最小的值(2¥),即数字 2 作为子问题(2,3)的答案。(注意到 minimax 了么~~)所以,最终的花费是 1¥ + 2¥ = 3¥

如果你最先猜的是 2,同样会有两个可能的结果:

  • 2 是正确的数字,你支付 0¥
  • 2 不是正确的数字,你支付 2¥。此时,你会知道你猜大还是猜小了,于是你马上就知道哪个才是正确答案了。所以,如果你最先猜 2,你最多支付 2¥。

同样的,如果你最先猜 3,那么你最多需要花费 4¥。

所以,最先猜 2 才是最优选择,你最多花费 2¥。

参考下面的代码,你会看到这是一个十分自然的递归过程。(使用了二维矩阵缓存结果)

public class Solution {
  private int[][]dp;

  private int solve(int l, int r) {
    if (l >= r) return 0;
    // 说明 dp[l][r] 已经被计算过了,直接返回
    if (dp[l][r] != Integer.MAX_VALUE) return dp[l][r];

    for (int i = l; i <= r; ++i) {
      dp[l][r] = Math.min(dp[l][r], Math.max(i + solve(l, i-1), i + solve(i+1, r)));
    }

    return dp[l][r];
  }

  public int getMoneyAmount(int n) {
    dp = new int[n+1][n+1];
    for (int[] row : dp)
      Arrays.fill(row, Integer.MAX_VALUE);

    return solve(1, n);
  }
}

我整理一下: 最优解结构—-区间[i,j]中的稳赢金额 等于
猜i,j中的一个数x后再去猜区间[i,x-1]或者区间[x+1,j] 这会花去 x+MAX(【i,x-1】稳赢金额,【x+1,j】稳赢金额)

遍历所有的x情况中,取最小值 即可得到答案

转移方程 $$ dp[i][j]=\begin{cases} 0 & i =j\
MIN \big[x+ MAX(dp[i,x-1], dp[x+1,j])\big] & i<j至少2个数 \end{cases} \其中x属于[i,j]中的所有情况 $$

举例: 只有两个数

dp[0][1]=MIN(arr[0]+ dp[1][1], arr[1]+dp[0][0])

dp[0][2]

在这里附上go的递推迭代版本,没有经过优化,而且可读性不如递归调用

实现代码

func getMoneyAmount(n int) int {
    dp := make([][]int, n)
    for i:=0; i<n; i++ {
        dp[i] = make([]int, n)
    }
    // 除去了i==j为0,其他都指为100000标识代价很大
    for i:=0; i<n; i++ {
        for j:=0; j<n; j++ {
            if(i!=j){
                dp[i][j] = 100000
            }
        }
    }

    for step:=1; step<n;step++ {
        for start:=0;start<n;start++ {
            if(start+step<n) {
                //推出区间为 start 到  start+dp的 稳赢金额,需要遍历区间内每一种情况
                // 因为x现在是下标,当x=0时实际上花费1
                for x:= start; x<=start+step; x++{
                    if(x-1<0) {
                        dp[start][start+step] = min(dp[start][start+step], x+1+ dp[x+1][start+step])
                    }else if(x+1>=n) {
                        dp[start][start+step] = min(dp[start][start+step], x+1+dp[start][x-1])
                    }else{
                        dp[start][start+step] = min(dp[start][start+step], x+1+max(dp[start][x-1], dp[x+1][start+step]))
                    }
                }
            }else{
                break
            }
        }
    }

    return dp[0][n-1]
}

func max(a, b int) int{
    if a<b {
        a=b
    }
    return a
}
func min(a, b int) int{
    if a>b {
        a=b
    }
    return a
}

附:leetcode java代码

public class Solution {
    public int getMoneyAmount(int n) {
        int[][] dp = new int[n + 1][n + 1];
        for (int len = 2; len <= n; len++) {
            for (int start = 1; start <= n - len + 1; start++) {
                int minres = Integer.MAX_VALUE;
                for (int piv = start; piv < start + len - 1; piv++) {
                    int res = piv + Math.max(dp[start][piv - 1], dp[piv + 1][start + len - 1]);
                    minres = Math.min(res, minres);
                }
                dp[start][start + len - 1] = minres;
            }
        }
        return dp[1][n];
    }
}
复杂度分析

作者:LeetCode
链接:https://leetcode-cn.com/problems/guess-number-higher-or-lower-ii/solution/cai-shu-zi-da-xiao-ii-by-leetcode/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

376. 摆动序列

解题思路

最长上升子序列是定义成以第i个数字结尾的最长上升子序列长度

1 3 2 4

长度依次为 1、2、2、3 dp[i+1]= i+1之前的所有满足arr[i+1]>arr[x]的情况dp[x]+1的最大值

最长摆动序列定义成考虑到第i个数字(不一定以第i个数字结尾) 最后差为负的最大长度 以及最后差为正的最大程度两种情况

up[i]和down[i]

以up[i+1]的变化为例

image-20210112145844102

我们知道了arr[i+1]>arr[i]则up[i+1]=down[i]+1, 不论最后下降的两个数字在什么位置

​ arr[i+1]<=arr[i]则up[i+1]=up[i]不变

对于down也是这样

最后状态转移方程 $$ up[i+1]= \begin{cases} 1 & i=0 \
down[i]+1 & arr[i+1]>arr[i]且i>0 \
up[i] & arr[i+1]<=arr[i]且i>0 \end{cases} \————————-\
down[i+1]= \begin{cases} 1 & i=0 \
up[i]+1 & arr[i+1]<arr[i]且i>0 \
down[i] & arr[i+1]>=arr[i]且i>0 \end{cases} $$

参考leetcode题解https://leetcode-cn.com/problems/wiggle-subsequence/solution/tan-xin-si-lu-qing-xi-er-zheng-que-de-ti-jie-by-lg/

引用其例图方便理解实图

实现代码

func wiggleMaxLength(nums []int) int {
    length := len(nums)
    if(length<=0) {
        return 0
    }
    upDp, downDp := make([]int, length), make([]int, length)
    upDp[0], downDp[0] = 1, 1
    for i:=1; i< length; i++ {
        upDp[i] = upDp[i-1]
        downDp[i] = downDp[i-1]
        if nums[i] > nums[i-1] {
            upDp[i] = downDp[i-1]+1
        }else if nums[i] < nums[i-1] {
            downDp[i] = upDp[i-1]+1
        }
    }
    if upDp[length-1] > downDp[length-1] {
        downDp[length-1] = upDp[length-1]
    }
    return downDp[length-1]
    
}

1.为什么会用到动态规划呢?

一个长的摆动序列,逐个截取尾部的元素还是摆动序列

2.如何想到这种要考虑到两个状态转移的思路的呢?

整个题难点在于不是求最长连续的摆动序列而是允许删除部分元素

如果是连续的话和最长上升子序列完全是一个思路,但是如果是不连续的怎么做呢?

想法一:

知道一个范围内不连续的最长摆动序列, 我们如何推演出更大范围的最长摆动序列?

推不出来,因为更大范围多出来的一个元素和前一个元素的关系知道,但是也不知道 小范围 最后一个是上升还是下降呀

想法二: 既然不知道小范围是上升下降,那我们将问题状态细分,分成知道 一个范围内 结尾为上升态和下降态 最长摆动序列长度

如果小范围的摆动序列的最后两个数字包括arr[i]也可以吗通过比较arr[i+1]和arr[i]即可区分了,但是如果不包括呢?

想法三:即使不包括arr[i]也不影响? 如果不包括arr[i],那上升态摆动序列最后的一个元素一定小于等于arr[i]

3.这道题如果用贪心算法如何求解呢?

每次上升尽可能上升到最大,每次下降尽可能下降到最小,这样维持一个贪心最优的摆动序列,一个一个挨个元素比较,o(n)时间即可解决

吐槽一下leet’code的计算耗时0ms,bug了。。。

image-20210113180717051

474. 一和零

解题思路

题目和01背包问题类似,0-1背包问题如下:

给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。

我们转换一下

给定一组物品,每种物品都有自己的重量(w[i]数组),在限定的总重量内,我们如何选择,才能拿到最多的物品。

dp[i][j]表示考虑前i件物品在容量为j的情况下可以拿到的最多物品数量,对于第i件物品有拿和不拿两种选择 $$ dp[i][j] = \begin{cases} dp[i-1][j] &j-w[i]<0装不下,只能不拿\
max(dp[i-1][j], dp[i-1][j-w[i]]+1) &j-w[i]>=0 装的下,拿或不拿 \end{cases} $$ 引申到我们现在的题目,背包容量变成了0和1的最大数量,其实就是重量变成了两个维度

dp[i][j][k]表示考虑前i件物品在容量为j,k的情况下可以拿到的最多物品数量,对于第i件物品有拿或不拿两种选择


$$ dp[i][j][k] = \begin{cases} dp[i-1][j][k] &j-arr0[i]<0||k-arr1[i]<0装不下,只能不拿\
max(dp[i-1][j][k], dp[i-1][j-arr0[i]][k-arr1[i]]+1) &j-arr0[i]>=0且k-arr1[i]>=0 装的下,拿或不拿 \end{cases} $$ arr0[i],arr1[i]表示第i个元素的 0和1的数量


实现代码

func findMaxForm(strs []string, m int, n int) int {
    strLen := len(strs)
    dp := make([][][]int, strLen+1)
    for i := 0; i < strLen+1; i++ {
        dp[i] = make([][]int, m+1)
        for j :=0; j < m+1; j++ {
            dp[i][j] = make([]int, n+1)
        }
    }
    for i:=1; i<=strLen;i++ {
        zeroCnt, oneCnt := zeroAndOneCount(strs[i-1])
        for j:=0;j<=m;j++ {
            for k:=0;k<=n;k++{
                if j>=zeroCnt&&k>=oneCnt {
                    dp[i][j][k] = Max(dp[i-1][j-zeroCnt][k-oneCnt]+1, dp[i-1][j][k])
                }else {
                    dp[i][j][k] = dp[i-1][j][k]
                }
            }
        }
    }
    return dp[strLen][m][n]
}

func zeroAndOneCount(s string) (int, int) {
    zero, one := 0, 0
    for _, ch := range s {
        if ch == '0' {
            zero++ 
        }else {
            one++
        }
    }
    return zero, one
}

func Max(a, b int) int {
    if a>b {
        b = a
    }
    return b
}


Be Water, My Friend.