【动态规划专题】一起攻克动态规划吧!

237 阅读17分钟

动态规划之理论基础

62. 不同路径

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

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

问总共有多少条不同的路径?

示例1

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

思路

动态五明确
*明确状态:在机器人行走的过程中,变化的就是坐标,因此状态即坐标
*明确含义:dp[i][j]表示机器人从(0,0)走到(i,j)总共的路径条数
*明确表达式:题目中表示“机器人每次只能向下或者向右移动一步”,因此dp[i][j]的数据是来自左边和上边,即dp[i][j-1]和dp[i-1][j]。-》dp[i][j]=dp[i-1][j]+dp[i][j-1]
*明确遍历方向:根据上述公式确定遍历方向-》从左到右,从上到下
*明确base:dp[0][i]=1 dp[i][0]=1

解法

func uniquePaths(m int, n int) int {
    dp:=make([][]int,m)
    for i:=0;i<m;i++{
        dp[i]=make([]int,n)
    }
    for i:=0;i<m;i++{
        dp[i][0]=1
    }
    for i:=0;i<n;i++{
        dp[0][i]=1
    }
    for i:=1;i<m;i++{
        for  j:=1;j<n;j++{
            dp[i][j]=dp[i-1][j]+dp[i][j-1] // 当前的路基条数来自于左边和上边的路径条数之和
        }
    }
    return dp[m-1][n-1]
}

63. 不同路径 II

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

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

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

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

示例1

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

思路

相比上一题,本题中添加了障碍物,存在障碍物即可确定,当前坐标(i,j)的条数为0,即dp[i][j]=0

动态五明确
*明确状态:在机器人行走的过程中,变化的就是坐标,因此状态即坐标
*明确含义:dp[i][j]表示机器人从(0,0)走到(i,j)总共的路径条数
*明确表达式:题目中表示“机器人每次只能向下或者向右移动一步”,因此dp[i][j]的数据是来自左边和上边,即dp[i][j-1]dp[i-1][j]-dp[i][j]=dp[i-1][j]+dp[i][j-1]
*明确遍历方向:根据上述公式确定遍历方向-》从左到右,从上到下
*明确base:因为可能存在障碍物,因此不能直接按照上面的题目直接dp[0][i]=1 dp[i][0]=1,这种是错误的。当存在障碍物,当前位置及后面的位置均需要将其dp[i][j]设置为0

解法

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)
    }
    for i:=0;i<m && obstacleGrid[i][0]==0;i++{ // 如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0
        dp[i][0]=1 
    }
    for i:=0;i<n && obstacleGrid[0][i]==0;i++{ // 同上
        dp[0][i]=1
    }
    // for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1的情况就停止dp[i][0]的赋值1的操作。代码也可以写成下面方式:
    /*
     for i:=0;i<m ;i++{
        if obstacleGrid[i][0]==0{
            dp[i][0]=1
        }else{
            break
        }
    }
    for i:=0;i<n && obstacleGrid[0][i]==0;i++{
        if obstacleGrid[0][i]==0{
            dp[0][i]=1
        }else{
            break
        }
    }
    */
    for i:=1;i<m;i++{
        for j:=1;j<n;j++{
            if obstacleGrid[i][j]==1{
                dp[i][j]=0
            }else{
                dp[i][j]=dp[i-1][j]+dp[i][j-1]
            }
        }
    }
    return dp[m-1][n-1]
}

343. 整数拆分

给定一个正整数n ,将其拆分为k个正整数的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积 。

示例1

输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

示例2

输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

思路

动态五明确
*明确状态:整个过程,变化的就是整数(从小到大),因此状态即整数
*明确含义:dp[i]表示整数i的最大乘积
*明确表达式:i整数拆成2个或者2个以上,如果拆成2个即i-j和j;如果拆成2个以上即
dp[i]=max(dp[i],max((i-j)*j,dp[i-j]*j) 。如果dp[i-j]*dp[j]则默认将其拆成4个以上了
*明确遍历方向:根据上述公式确定遍历方向-》从左到右
*明确base: dp[1]=1 dp[2]=1

解法

func integerBreak(n int) int {
    /*
    dp[i]表示整数i的最大乘积
    dp[i]=max(dp[i],max(j*(i-j),dp[i-j]*j))
    dp[0]=1
    dp[1]=1
    */
    dp:=make([]int,n+1)
    dp[2]=1
    for i:=3;i<=n;i++{
        for j:=1;j<=i/2;j++{
            dp[i]=max(dp[i],max(j*(i-j),dp[i-j]*j))
        }
    }
    return dp[n]
}

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

96. 不同的二叉搜索树

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

示例1

输入: n = 3
输出: 5

示例2

输入: n = 1
输出: 1

思路

动态五明确
*明确状态:整个过程,变化的就是整数(从小到大),也是节点数,因此状态即节点数
*明确含义:dp[i]表示1i为节点组成的二叉搜索树的个数
*明确表达式:dp[i]+=dp[j-1]*dp[i-j]; j-1j为头结点左子树节点数量,i-j为以j为头结点右子树节点数量
*明确遍历方向:根据上述公式确定遍历方向-》从左到右
*明确base: dp[1]=1 dp[2]=1

解法

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

动态规划之01背包

  • 01背包的含义: N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
  • 二维dp数组01背包
    • 明确状态:变量就是物品和重量,即状态为物品和重量
    • 明确含义:dp[i][j]表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是dp[i][j]
    • 明确表达式:dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]) 分放与不放物品两种情况考虑
    • 明确遍历方向:根据上述公式确定遍历方向-》从左到右
    • 明确base: dp[0][j]=0
  • 遍历方向
// 先遍历物品,然后遍历背包重量的代码
for(int i = 1; i < weight.size(); i++) { // 遍历物品
    for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
        if (j < weight[i]) dp[i][j] = dp[i - 1][j]; 
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

    }
}
// 先遍历背包,再遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
    for(int i = 1; i < weight.size(); i++) { // 遍历物品
        if (j < weight[i]) dp[i][j] = dp[i - 1][j];
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    }
}

416. 分割等和子集

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

示例1

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

思路

01背包相对于本题,主要要理解,题目中物品是nums[i],重量是nums[i],价值也是nums[i],背包体积是sum/2。

也即是说:给一个可装载重量为sum/2的背包和N个物品,每个物品的重量为nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满?

动态五明确
*明确状态:整个过程,变化的就是元素和元素构成的数组和(价值)
*明确含义:容量为j的背包,所背的物品价值最大可以为dp[j]。本题中每一个元素的数值既是重量,也是价值。
*明确表达式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]),背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]
*明确遍历方向:
*明确base:  dp[0]=nums[0]

解法

func canPartition(nums []int) bool {
    /*
    dp[j]表示背包容量为j的最大重量为dp[j],也就是说寻找一个背包,求其最大重量,也是求是否其重量上是否可以达到sum/2
    dp[j]=dp[j-nums[i]]+nums[i]
    dp[0]=nums[0]
    */
    sum:=0
    for i:=0;i<len(nums);i++{
        sum+=nums[i]
    }
    if sum%2!=0{ // 总和为奇数,不能平分
        return false
        
    }
    target:=sum/2
    dp:=make([]int,target+1)
    for i:=0;i<len(nums);i++{ // 遍历物品
        for j:=target;j>=nums[i];j--{ // 遍历背包容量,j应该从后往前反向遍历,因为每个物品(或者说数字)只能用一次,以免之前的结果影响其他的结果;如果是二维,则可正向遍历
            dp[j]=max(dp[j],dp[j-nums[i]]+nums[i])
        }
    }
    if dp[target]==target{
        return true
    }
    return false
}

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

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

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎; 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。 最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。

示例1

输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

思路

01背包相对于本题,主要要理解,题目中物品是nums[i],重量是nums[i],价值也是nums[i],背包体积是sum/2。相比于上一个题目,上题是求背包是否正好装满,而本题是求背包最多能装多少

动态五明确
*明确状态:整个过程,变化的就是元素和元素构成的数组和(价值)
*明确含义:容量为j的背包,所背的物品价值最大可以为dp[j]。本题中每一个元素的数值既是重量,也是价值。
*明确表达式:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]),背包里放入数值,那么物品i的重量是stones[i],其价值也是stones[i]
*明确遍历方向:
*明确base:  dp[0]=stones[0]

解法

func lastStoneWeightII(stones []int) int {
    /*
    dp[j]表示容量为j最大的重量为dp[j]
    dp[j]=max(dp[j],dp[j-stones[i]]+stones[i])
    dp[0]=0
    */
    sum:=0
    for i:=0;i<len(stones);i++{
        sum+=stones[i]
    }
    target:=sum/2 // 背包的容量最大为target
    dp:=make([]int,target+1)
    for i:=0;i<len(stones);i++{
        for j:=target;j>=stones[i];j--{ // 倒序,选择不放或者不放
            dp[j]=max(dp[j],dp[j-stones[i]]+stones[i])
        }
    }
    return sum-2*dp[target]
}

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

494. 目标和

给你一个整数数组 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

思路

01背包相对于本题,主要要理解,题目中物品是nums[i],重量是nums[i],价值也是nums[i]。此题转化为装满容量为(target+sum)/2的背包,有几种方法。

动态五明确
*明确状态:整个过程,变化的就是元素和元素构成的数组和(价值)
*明确含义:填满j(包括j)这么大容积的包,有dp[j]种方法
*明确表达式:dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]]
*明确遍历方向:
*明确base:  dp[0]=stones[0]

解法

func findTargetSumWays(nums []int, target int) int {
    sum:=0
    for i:=0;i<len(nums);i++{
        sum+=nums[i]
    }
    if (sum+target)%2!=0 || abs(target)>sum{
        return 0
    }
    sum=(sum+target)/2
    dp:=make([][]int,len(nums)+1)
    for i:=0;i<=len(nums);i++{
        dp[i]=make([]int,sum+1)
    }
    for i:=0;i<len(nums);i++{
        dp[i][0]=1
    }
    for i:=1;i<=len(nums);i++{
        for j:=0;j<=sum;j++{
            if j>=nums[i-1]{ // 当前背包的容量大于物品的重量,则可考虑放与不放的选择
                dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]]
            }else{
                dp[i][j]=dp[i-1][j] // 当前背包的容量小于物品的重量,则不可放
            }
        }
    }
    return dp[len(nums)][sum]
}

func abs(a int)int{
    if a<0{
        return -a
    }
    return a
}

474. 一和零

给你一个二进制字符串数组strs和两个整数m和n 。

请你找出并返回strs的最大子集的长度,该子集中最多有m个0和n个 1 。

如果x的所有元素也是y的元素,集合x是集合y的子集 。

示例1

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5031 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 41 ,大于 n 的值 3

思路

动态五明确
*明确状态:整个过程,变化的就是strs字符串数组,01的个数,因此状态即字符串数组、01的个数
*明确含义:dp[i][j]i0j1的最大子集长度为dp[i][j]
*明确表达式:dp[i][j]+=dp[i-strs[i]中的0][j-strs[i]中的1],dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有strs[i]中的00strs[i]中的11dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。

然后我们在遍历的过程中,取dp[i][j]的最大值。
*明确遍历方向:从小到大遍历
*明确base:  dp[0][j]=0

解法

func findMaxForm(strs []string, m int, n int) int {
    /*
    dp[i][j]:i个0和j个1的最大子集长度为dp[i][j]
    dp[i][j]+=dp[i-strs[i]中的0][j-strs[i]中的1]
    dp[0][0]=1
    */
    dp:=make([][]int,m+1)
    for i:=0;i<=m;i++{
        dp[i]=make([]int,n+1)
    }
    for k:=0;k<len(strs);k++{ // 遍历物品
        zeros,ones:=0,0
        for _,v:=range strs[k]{
            if v=='0'{
                zeros++
            }else{
                ones++
            }
        }
        for i:=m;i>=zeros;i--{ // 遍历背包容量且从后向前遍历!
            for j:=n;j>=ones;j--{
                dp[i][j]=max(dp[i-zeros][j-ones]+1,dp[i][j])
            }
        }
    }
    return dp[m][n]
}

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

动态规划之完全背包

  • 完全背包的含义:有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次) ,求解将哪些物品装入背包里物品价值总和最大。
  • 完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。

518. 零钱兑换 II

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。 

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

思路

动态五明确
*明确状态:整个过程,变化的就是硬币coins和金钱数额amount,因此状态即硬币和金钱数额
*明确含义:dp[j]:凑成金额j的硬币组合数为dp[j]
*明确表达式:dp[j]+=dp[j-coins[i]]
*明确遍历方向:从小到大遍历
*明确base:  dp[0]=0

解法1

func change(amount int, coins []int) int {
    /*
    dp[j]:表示金额为j的组合数为dp[j]
    dp[j]+=dp[j-coins[i]]
    dp[0]=1
    */
    dp:=make([]int,amount+1)
    dp[0]=1
    for i:=0;i<len(coins);i++{ // 先遍历物品
        for j:=coins[i];j<=amount;j++{ // 再遍历金额
            dp[j]+=dp[j-coins[i]]
        }
    }
    return dp[amount]
}

解法2

func change(amount int, coins []int) int {
    dp:=make([]int,amount+1)
    for i:=0;i<=(amount+1);i++{
        dp[0]=1
    }
    for i:=0;i<len(coins);i++{ // 先遍历物品
        for j:=1;j<=amount;j++{  // 再遍历金额
            if (j-coins[i])>=0{
                dp[j]=dp[j]+dp[j-coins[i]]
            }
        }
    }
    return dp[amount]
}

70. 爬楼梯

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

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例1

输入: n = 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

思路

动态五明确
*明确状态:整个过程,变化的就是台阶数
*明确含义:dp[i]:表示i阶的方法数为dp[i]
*明确表达式:dp[i]+=dp[i-2]+dp[i-1]
*明确遍历方向:从小到大遍历
*明确base:  dp[1]=1 dp[2]=2

解法1

func climbStairs(n int) int {
    /*
    dp[i]:表示i阶的方法数为dp[i]
    dp[i]=d[i-2]+dp[i-1]
    dp[0]=0
    dp[1]=1
    dp[2]=2
    */
    if n==1{
        return 1
    }
    if n==2{
        return 2
    }
    dp:=make([]int,n+1)
    dp[1]=1
    dp[2]=2
    for i:=3;i<=n;i++{
        dp[i]=dp[i-1]+dp[i-2]
    }
    return dp[n]
}

279. 完全平方数

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

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

示例1

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

思路

动态五明确
*明确状态:整个过程,变化的就是n和平方数量,即状态为整数n和平方数
*明确含义:dp[i]:表示和为i的完全平方数的最少数量
*明确表达式:dp[i]=min(dp[i],dp[i-j*j]+1)
*明确遍历方向:从小到大遍历
*明确base:  dp[1]=1

解法

func numSquares(n int) int {
    /*
    dp[i]:表示和为i的完全平方数的最少数量
    dp[i]=min(dp[i],dp[i-j*j])
    dp[1]=1
    */
    dp:=make([]int,n+1)
    for i:=0;i<=n;i++{
        dp[i]=math.MaxInt32
    }
    dp[0]=0
    for i:=0;i<=n;i++{ // 先遍历物品
        for j:=1;j*j<=i;j++{  // 先遍历容量,此题的容量也就是数值i
            dp[i]=min(dp[i],dp[i-j*j]+1)
        }
    }
    return dp[n]
}

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

322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

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

示例1

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

思路

动态五明确
*明确状态:整个过程,变化的就是硬币coins和金钱数额amount,因此状态即硬币和金钱数额
*明确含义:dp[i]:凑成金额i的硬币最小硬币数
*明确表达式: dp[i]=min(1+dp[i-coins[j]],dp[i]),凑足总额为i - coins[j]的最少个数为dp[i - coins[j]],那么只需要加上一个钱币coins[j]即dp[i - coins[j]] + 1就是dp[i](考虑coins[j])
*明确遍历方向:从小到大遍历
*明确base:  dp[0]=0

解法

func coinChange(coins []int, amount int) int {
    dp:=make([]int,amount+1)
    dp[0]=0
    for i:=1;i<=amount;i++{
        dp[i]=amount+1
    }
    for i:=1;i<=amount;i++{ // 遍历背包
        for j:=0;j<len(coins);j++{ // 遍历物品
            if i<coins[j]{
                continue
            }
            dp[i]=min(1+dp[i-coins[j]],dp[i])
        }
    }
    if dp[amount]==amount+1{
        return -1
    }
    return dp[amount]
}

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

377. 组合总和 Ⅳ

给你一个由 不同 整数组成的数组 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)
请注意,顺序不同的序列被视作不同的组合。

思路

动态五明确
*明确状态:整个过程,变化的就是数组nums和数组和target
*明确含义:dp[i]:表示凑成和为i的元素组合数为dp[i]
*明确表达式: dp[i]+=dp[i-nums[j]]
*明确遍历方向:从小到大遍历
*明确base:  dp[0]=1

解法

func combinationSum4(nums []int, target int) int {
    /*
    dp[i]:表示和为i的元素组合数为dp[i]
    dp[i]+=dp[i-nums[i]]
    dp[0]
    */
    dp:=make([]int,target+1)
    dp[0]=1
    for i:=0;i<=target;i++{  // 遍历背包
        for j:=0;j<len(nums);j++{  // 遍历物品
            if i>=nums[j]{
                dp[i]+=dp[i-nums[j]]
            }
            
        }
    }
    return dp[target]
}

139. 单词拆分

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

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

示例1

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

解法

func wordBreak(s string, wordDict []string) bool {
    /*
    dp[i]:表示0~i-1的字符子串是否可以拼接出字典中的单词
    dp[i]=dp[j]为true,j~i的字符子串也可以拼接出字典中的单词
    dp[0]=true
    */
    wordMap:=map[string]bool{}
    for i:=0;i<len(wordDict);i++{
        wordMap[wordDict[i]]=true
    }
    dp:=make([]bool,len(s)+1)
    dp[0]=true
    for i:=1;i<=len(s);i++{
        for j:=0;j<i;j++{
            if dp[j] && wordMap[s[j:i]]{
                dp[i]=true
            }
        }
    }
    return dp[len(s)]
}

动态规划之打家劫舍

198. 打家劫舍

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

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

示例1

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

解法

func rob(nums []int) int {
    /*
    dp[i]:表示下标为i的房间,偷窃的最高金额为dp[i]
    dp[i]=max(dp[i-1],dp[i-2]+nums[i])
    dp[0]=0
    dp[1]=nums[0]
    遍历物品,物品是房间
    */
    dp:=make([]int,len(nums)+1)
    dp[0]=0
    dp[1]=nums[0]
    for i:=2;i<=len(nums);i++{
        dp[i]=max(dp[i-1],dp[i-2]+nums[i-1]) // 不偷dp[i-1]和偷dp[i-2]+nums[i-1]
    }
    return dp[len(nums)]
}

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

213. 打家劫舍 II

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

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

示例1

输入: nums = [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

解法1

func rob(nums []int) int {
    /*
    dp[i]:表示i索引的房间能偷窃到的最大金额
    dp[i]=max(dp[i-2]+nums[i],dp[i-1])
    dp[0]=0
    dp[1]=nums[0]
    考虑到环,因此分两种情况讨论:
    0~n-2;1~n-1
    */
    if len(nums)==1{
        return nums[0]
    }
    if len(nums)==2{
        return max(nums[0],nums[1])
    }
    return max(dfs(nums,0,len(nums)-2),dfs(nums,1,len(nums)-1))
}

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

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

解法2

func rob(nums []int) int {
    if len(nums)==1{
        return nums[0]
    }
    dp:=make([]int,len(nums)+2)
    var dfs func(int,int)int
    dfs=func(start int,end int)int{
        for i:=end;i>=start;i--{
            dp[i]=max(dp[i+1],dp[i+2]+nums[i])
        }
        return dp[start]
    }
    res:=max(dfs(0,len(nums)-2),dfs(1,len(nums)-1)) // 循环需要分两种;(0,len(nums)-2)和(1,len(nums)-1)
    return res
}



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

337. 打家劫舍 III

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

示例1

输入: root = [3,2,3,null,3,null,1]
输出: 7 
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

解法

/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
var memory=make(map[*TreeNode]int)
func rob(root *TreeNode) int {
    if root==nil{
        return 0
    }
    if v,ok:=memory[root];ok{
        return v
    }
    // 偷
    res1:=root.Val
    if root.Left!=nil{
        res1+=rob(root.Left.Left)+rob(root.Left.Right)
    }
    if root.Right!=nil{
        res1+=rob(root.Right.Left)+rob(root.Right.Right)
    }
    // 不偷
    res2:=rob(root.Left)+rob(root.Right)
    temp:=max(res1,res2)
    memory[root]=temp
    return temp
}

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

动态规划之股票问题

121. 买卖股票的最佳时机

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

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

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

示例1

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

解法

func maxProfit(prices []int) int {
    /*
    dp[i][2]:dp[i][0]表示第i天持有股票的最大金额,dp[i][1]表示第i天不持有股票的最大金额
    dp[i][0]=max(dp[i-1][0],-prices[i])
    dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i])
    dp[0][0]=-prices[0]
    dp[0][1]=0
    */
    dp:=make([][2]int,len(prices))
    dp[0][0]=-prices[0]
    dp[0][1]=0
    for i:=1;i<len(prices);i++{
        dp[i][0]=max(dp[i-1][0],-prices[i])
        dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i])
    }
    return dp[len(prices)-1][1]
}

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

122. 买卖股票的最佳时机 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

解法1

func maxProfit(prices []int) int {
    res:=0
    for i:=1;i<len(prices);i++{
        if (prices[i]-prices[i-1])>0{
            res+=prices[i]-prices[i-1]
        }
    }
    return res
}

解法2

func maxProfit(prices []int) int {
    /*
    dp[i][2]:dp[i][0]表示第i天持有股票的最大金额,dp[i][1]表示第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])
    dp[0][0]=-prices[0]
    dp[0][1]=0
    */
    dp:=make([][2]int,len(prices))
    dp[0][0]=-prices[0]
    dp[0][1]=0
    for i:=1;i<len(prices);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(prices)-1][1]
}

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

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

给定一个数组,它的第 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

解法

func maxProfit(prices []int) int {
    /*
    dp[i][5]:表示第i天所剩下的最大金额,其中:
    dp[i][0]表示第i天不操作剩下的最大金额
    dp[i][1]表示第i天买入剩下的最大金额
    dp[i][2]表示第i天卖出剩下的最大金额
    dp[i][3]表示第i天买入剩下的最大金额
    dp[i][4]表示第i天卖出剩下的最大金额
    dp[i][0]=dp[i-1][0]
    dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i])
    dp[i][2]=max(dp[i-1][2],dp[i-1][1]+prices[i])
    dp[i][3]=max(dp[i-1][3],dp[i-1][2]-prices[i])
    dp[i][4]=max(dp[i-1][4],dp[i-1][3]+prices[i])
    dp[0][0]=0
    */
    dp:=make([][5]int,len(prices))
    dp[0][0]=0
    dp[0][1]=-prices[0]
    dp[0][3]=-prices[0]
    for i:=1;i<len(prices);i++{
        dp[i][0]=dp[i-1][0]
        dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i])
        dp[i][2]=max(dp[i-1][2],dp[i-1][1]+prices[i])
        dp[i][3]=max(dp[i-1][3],dp[i-1][2]-prices[i])
        dp[i][4]=max(dp[i-1][4],dp[i-1][3]+prices[i])
    }
    return dp[len(prices)-1][4]
}

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

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

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

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

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

示例1

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

解法

func maxProfit(k int, prices []int) int {
     /*
    dp[i][4]:表示第i天的进行下述操作所剩下的最大金额,其中:
    dp[i][0]表示第i天不操作剩下的最大金额
    dp[i][1]表示第i天第一次买入剩下的最大金额
    dp[i][2]表示第i天第一次卖出剩下的最大金额
    dp[i][3]表示第i天第二次买入剩下的最大金额
    dp[i][4]表示第i天第二次卖出剩下的最大金额
    dp[i][0]=dp[i-1][0]
    dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i])
    dp[i][2]=max(dp[i-1][2],dp[i-1][1]+prices[i])
    dp[i][3]=max(dp[i-1][3],dp[i-1][2]-prices[i])
    dp[i][4]=max(dp[i-1][4],dp[i-1][3]+prices[i])
    依次类推
    dp[i][k]=max(dp[i-1][k],dp[i-1][k+1]+prices[i])
    dp[i][k+1]=max(dp[i-1][k+1],dp[i-1][k-1]-prices[i])
    dp[0][0]=0
    */
    dp:=make([][]int,len(prices))
    for i:=0;i<len(prices);i++{
        dp[i]=make([]int,2*k+1)
    }
    dp[0][0]=0
    for j:=1;j<(2*k);j=j+2{
        dp[0][j]=-prices[0]
    }
   
    for i:=1;i<len(prices);i++{
       for j:=1;j<(2*k);j=j+2{
           dp[i][j]=max(dp[i-1][j],dp[i-1][j-1]-prices[i])
           dp[i][j+1]=max(dp[i-1][j+1],dp[i-1][j]+prices[i])
       }
    }
    return dp[len(prices)-1][2*k]
}

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

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

给定一个整数数组prices,其中第  prices[i] 表示第 i 天的股票价格 。​

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

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

示例1

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

解法

func maxProfit(prices []int) int {
    /*
    状态:持有,不持有(保持卖出,今天卖出),冷冻期
    dp[i][0]=max(dp[i-1][0],dp[i-1][3]-prices[i],dp[i-1][1]-prices[i]) // 前一天是卖出状态,前一天是冷冻期,今天买入
    dp[i][1]=max(dp[i-1][1],dp[i-1][3]) // 保持卖出状态或者前几天就卖出了,度过了冷冻期
    dp[i][2]=dp[i-1][0]+prices[i] // 今天卖出
    dp[i][3]=dp[i-1][2] // 卖出后第二天开始冷冻
    */
    dp:=make([][4]int,len(prices))
    dp[0][0]=-prices[0]
    for i:=1;i<len(prices);i++{
        dp[i][0]=max(dp[i-1][0],max(dp[i-1][3],dp[i-1][1])-prices[i])
        dp[i][1]=max(dp[i-1][1],dp[i-1][3])
        dp[i][2]=dp[i-1][0]+prices[i]
        dp[i][3]=dp[i-1][2]
    }
    return max(max(dp[len(prices)-1][1],dp[len(prices)-1][2]),dp[len(prices)-1][3])
}

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