动态规划
文章目录
- 动态规划
- 1.[打家劫舍](https://leetcode-cn.com/problems/house-robber/)
- 2.[打家劫舍 II](https://leetcode-cn.com/problems/house-robber-ii/)
- 3.[爬楼梯](https://leetcode-cn.com/problems/climbing-stairs/)
- 4.[使用最小花费爬楼梯](https://leetcode-cn.com/problems/min-cost-climbing-stairs/)
- 5.[零钱兑换](https://leetcode-cn.com/problems/coin-change/)
- 6.[最低票价](https://leetcode-cn.com/problems/minimum-cost-for-tickets/)
1.打家劫舍
分析状态
对于一间房子有偷或者不偷两种状态,但是偷了一间房子之后,相邻的房子不能偷。那么对于第 n 间房子,我们可以:
- 偷取第 n 间房子,那么n - 1 间房子不能偷取,只能选择偷或者不偷第n - 2间
- 不偷第 n 间房子,可以选择偷或者不偷第 n - 1间房子
所以动态方程是:
dp[i] = max{dp[i - 1],dp[i - 2] + nums[i]}
但是这个需要初始化大dp[1]和dp[0]
dp[0] = nums[0]
dp[1] = max{nums[0],nums[1]}
所以:
func rob(nums []int) int {
if len(nums) == 0{
return 0;
}
if len(nums) == 1{
return nums[0]
}
dp := make([]int, len(nums))
dp[0] = nums[0]
if len(nums) == 2{
return int(math.Max(float64(nums[0]),float64(nums[1])));
}
dp[1] = int(math.Max(float64(nums[0]),float64(nums[1])))
for i := 2; i < len(nums);i++{
dp[i] = int(math.Max(float64(dp[i - 2] + nums[i]),float64(dp[i - 1])))
}
return dp[len(nums) - 1]
}
2.打家劫舍 II
因为房子[1]和房子[n]相邻,所以不能一起抢劫。因此,问题变成抢劫房屋[1]-房屋[n-1]或房屋[2]-房屋[n],这取决于哪个选择提供更多的钱。现在,这个问题已经退化为抢劫房屋的人,这个问题已经解决了。
所以,状态还是跟上边相同,但是状态转移有变化
我选择的是使用两个dp
dp1[i] : 从第一间抢到n - 1间
所以初始化时是:
dp[0] = 0
dp[1] = nums[0]//抢第一间
但是不抢最后一间:所以是nums[i - 1]
for i := 2;i <len(nums);i++{
dp1[i] = int(math.Max(float64(dp1[i - 2] + nums[i - 1]),float64(dp1[i - 1])))
}
第二个dp
dp2[i]:从第二间到第 n 间
初始化:
dp2[0] = 0 //不抢第一间
dp2[1] = nums[1] //抢第二间
最后一间也抢
for i := 2;i <len(nums);i++{
dp2[i] = int(math.Max(float64(dp2[i - 2] + nums[i]),float64(dp2[i - 1])))
}
最终代码
func rob2(nums []int) int {
if len(nums) == 0{
return 0
}
if len(nums) == 1{
return nums[0]
}
if len(nums) == 2{
return int(math.Max(float64(nums[0]),float64(nums[1])));
}
dp1 := make([]int, len(nums))
dp1[0] = 0
dp1[1] = nums[0]
//抢了第一个不能抢最后一个,所以第一个dp1是从0抢到n - 1
for i := 2;i <len(nums);i++{
dp1[i] = int(math.Max(float64(dp1[i - 2] + nums[i - 1]),float64(dp1[i - 1])))
}
dp2 := make([]int, len(nums))
dp2[0] = 0
dp2[1] = nums[1]
//没抢第一个。从第二个开始抢,抢到n,=
for i := 2;i <len(nums);i++{
dp2[i] = int(math.Max(float64(dp2[i - 2] + nums[i]),float64(dp2[i - 1])))
}
//返回两种情况最大值即可
return int(math.Max(float64(dp2[len(nums) - 1]),float64(dp1[len(nums) - 1])))
}
3.爬楼梯
斐波那契数列问题
对于第n级台阶,我可以有两种方式登上去
- 从n - 1级台阶迈一步上去
- 从n - 2级台阶一下迈两级上去,或者迈一步上去n - 1级台阶在迈一步上去
那么怎么上到n - 1级台阶,n-2级台阶又依次类推
直到一开始第1级和第二级,第一级台阶迈一步就上去了,只有一种方法,第二级台阶则可以一下子两级,可以一级一级,所有有两种方法
所以:
dp[0] = 1
dp[1] = 2
dp[i] = dp[i - 1] + dp[i - 2]
所以:
func climbStairs(n int) int {
if n == 0{
return 0
}
if n == 1{
return 1
}
if n == 2{
return 2
}
dp := make([]int,n)
dp[1] = 2
dp[0] = 1
for i := 2;i < n;i++{
dp[i] = dp[i - 1] + dp[i - 2]
}
return dp[n - 1]
}
4.使用最小花费爬楼梯
分析状态:
原来上一道是分析一共有多少种上楼的方式,现在,这道题是分析上楼需要花费的最少体力,所以对于第 n 层阶梯:
- 从地面可以直接跃向第0级或第1级
- 花费分别为dp[0]=cost[0]、dp[1]=cost[1]
- 对于跨到2阶,
- 可以从第0阶跨两步,耗费dp[0]+cost[2]
- 或者可以从第1阶跨一步,耗费dp[1]+cost[2]
- 所以,对于跨到第n层,可以从第dp[n-2]跨两步,或者从dp[n-1]跨一步,耗费cost[n],求出dp[n-1]和dp[n-2]的最小值就可以得到爬到第n阶的最小耗费。
- 状态转移方程为:dp[n] = Math.min(dp[n-1],dp[n-2]) + cost[n];
- 最后比较从第n-1阶迈到楼顶的花费和从n-2阶迈到楼顶花费的最小值即可
func minCostClimbingStairs(cost []int) int {
if len(cost) == 0{
return 0
}
if len(cost) == 1{
return cost[1]
}
dp := make([]int, len(cost))
dp[0] = cost[0]
dp[1] = cost[1]
for i := 2;i < len(cost);i++{
dp[i] = int(math.Min(float64(dp[i - 1] + cost[i]),float64(dp[i - 2] + cost[i])))
}
return int(math.Min(float64(dp[len(cost) - 1]),float64(dp[len(cost) - 2])))
}
5.零钱兑换
这道题是一道经典的背包问题
我们可以将amount看成是背包的容量,coins数组是物品列表,每一种物品的重量不一样
对于背包问题的关键点是放或者不放
- 放入背包:则背包可容纳的容量减少coins[i]
- 不放:则背包容量不变
所以:
dp[i]表示金额为i需要最少的硬币数是多少
对于任意金额j,
dp[j] = min(dp[j],dp[j-coin]+1),如果j-coin存在的话.
func coinChange(coins []int, amount int) int {
//背包容量 amount
dp := make([]int,amount + 1)
for i := 0;i < len(dp);i++{
dp[i] = amount + 1
}
//当背包容量是0时,就能不能放入物品了
dp[0] = 0
for i := 1;i <= amount;i++{
for j := 0;j < len(coins);j++{
//寻找能放的下的物品
if coins[j] <= i{
//背包问题核心,放 or 不放
dp[i] = int(math.Min(float64(dp[i]),float64(dp[i - coins[j]] + 1)))
}
}
}
if dp[amount] > amount{
return -1
}else{
return dp[amount]
}
}
6.最低票价
分析状态:
对于第n天需要旅行,这种状态,我们需要考虑
- 如果我一天前买票,会不会便宜点(dp[n - 1] + cost[0] )
- 如果我7天前买票,会不会便宜点(dp[ n-7 ] + cost[ 1 ])
- 如果我30天前买票会不会便宜点( dp[ n-30 ] + cost[ 2 ])
即分析旅行日这三种买票方式的最低花费:
dp[ n ]=min( dp[ n-1 ] + cost[ 0 ] , dp[ n-7 ] + cost[ 1 ] , dp[ n-30 ] + cost[ 2 ] )
我用了一个表示一年365天的数组。为了省去了判断当前日子是不是大于1,7,30。我在数组前面增加了30个空位置
用index指向要旅行的第一天
遍历数组,判断当天是否是旅行日
-
当天不旅行:
dp[i] = dp[i - 1]
-
当天旅行:
dp[ n ]=min( dp[ n-1 ] + cost[ 0 ] , dp[ n-7 ] + cost[ 1 ] , dp[ n-30 ] + cost[ 2 ] )
所以:
func mincostTickets(days []int, costs []int) int {
//在数组前面加了30个空位置,省去了判断当前日子是不是大于1,7,30。
dp := make([]int,396)
//day[index]:要旅行的天
index := 0
for i := 31;i < 396 && index < len(days);i++{
//第i天不旅行
if days[index] != i - 30{
dp[i] = dp[i - 1]
continue
}
//当天要旅行
//循环后,index指向下一个要旅行的day
index++
//找出最低票价
dp[i] = int(math.Min(float64(dp[i - 1] + costs[0]),
math.Min(float64(dp[i - 7] + costs[1]),float64(dp[i - 30] + costs[2]))))
}
return dp[days[len(days) - 1] + 30]
}