数据结构与算法学习(3)——动态规划
动态规划
动态规划(英语:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有重叠子问题[1]和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
题目
PS:下列题目均来自leetcode中灵神题单
入门
1.1 爬楼梯
public class Solution {
public int climbStairs(int n) {
int[] d = new int[n + 1];
d[0] = d[1] = 1;
for (int i = 2; i <= n; i++) {
d[i] = d[i - 1] + d[i - 2];
}
return d[n];
}
}
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n=cost.length;
int[] dp= new int[n+1];
dp[0]=0;
dp[1]=0;
for(int i=2;i<=n;i++){
dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
}
return dp[n];
}
}
"""
思路为构建dp,dp[i]为countTexts第i个数字结尾的所有可能性
那么显然dp[i]=dp[i-1]+dp[-2]+dp[i-3]
和爬楼梯类似,从dp[i-3]一次跳到dp[i],dp[i-2]一次跳到dp[i]这样
再使用乘法原理分割连续相同的字符串
我个考虑的时候
1. 忘记了乘法原理
2. 我考虑的时候想的是dp[i]=dp[i-1]+1+dp[i-2]+1+dp[i-3]+1想的很抽象,不知道为什么我在考虑的时候没有正确理解到dp[i]=dp[i-1]+...里面的dp[i-1]到dp[i]其实是一个类似于楼梯问题,这是一个完整可能结果的一条路径!所以+1是完全没有弄明白
3.应该要仔细理解状态转移方程的状态!
"""
#官方题解
class Solution:
def countTexts(self, pressedKeys: str) -> int:
m = 10 ** 9 + 7
dp3 = [1, 1, 2, 4] # 连续按多次 3 个字母按键对应的方案数
dp4 = [1, 1, 2, 4] # 连续按多次 4 个字母按键对应的方案数
n = len(pressedKeys)
for i in range(4, n + 1):
dp3.append((dp3[i-1] + dp3[i-2] + dp3[i-3]) % m)
dp4.append((dp4[i-1] + dp4[i-2] + dp4[i-3] + dp4[i-4]) % m)
res = 1 # 总方案数
cnt = 1 # 当前字符连续出现的次数
for i in range(1, n):
if pressedKeys[i] == pressedKeys[i-1]:
cnt += 1
else:
# 对按键对应字符数量讨论并更新总方案数
if pressedKeys[i-1] in "79":
res *= dp4[cnt]
else:
res *= dp3[cnt]
res %= m
cnt = 1
# 更新最后一段连续字符子串对应的方案数
if pressedKeys[-1] in "79":
res *= dp4[cnt]
else:
res *= dp3[cnt]
res %= m
return res
#初始状态定义为1,表示呆在原地?
class Solution:
def countGoodStrings(self, low: int, high: int, zero: int, one: int) -> int:
dp=[0]*(high+1)
dp[0]=1
for i in range(1,high+1):
if i>=one:
dp[i]=dp[i-one]
if i>=zero:
dp[i]+=dp[i-zero]
return sum(dp[low:]) % (10**9+7)
1.2 打家劫舍
#状态转移方程为dp[i]=max(dp[i−2]+nums[i],dp[i−1])
#dp[i]为前i个房间可以偷到的最大金额
class Solution:
def rob(self, nums: List[int]) -> int:
n=len(nums)
dp=[0]*(n+1)
dp[0]=0
dp[1]=nums[0]
for i in range(2,n+1):
dp[i]=max(dp[i-1],dp[i-2]+nums[i-1])
return dp[n]
class Solution {
public int rob(int[] nums) {
int n=nums.length;
int[] dp=new int[n+1];
dp[0]=0;
dp[1]=nums[0];
for (int i=2;i<(n+1);i++){
dp[i] = Math.max(dp[i - 2] + nums[i-1], dp[i - 1]);
}
return dp[n];
}
}
//滚动数组因为我们只需要dp[n]而不需要dp[i](0<=i<n),所有不需要储存dp[i](0<=i<n),减少内存消耗。
class Solution {
public int rob(int[] nums) {
int n=nums.length;
int last=0;
int now=nums[0];
int temp=0;
for (int i=2;i<(n+1);i++){
temp=now;
now=Math.max(now,last+nums[i-1]);
last=temp;
}
return now;
}
}
740. 删除并获得点数
浅谈一下我的想法首先是状态转移方程
其中\(num_1\)是\(Counter\)后的字典,\(num\)是记录字典的键
\(dp\)是\(num_1\)前\(i\)个数能够得到的最大值,因此我们需要判断在第\(i-1\)步操作是否把\(num_1[i]\)给删除了
如果删除,那么\(dp[i]=dp[i-1]+num_1[i-1]\times num[num_1[i-1]]\)如果没删除那么\(dp[i]=max(dp[i-2]+num_1[i-1]\times num[num_1[i-1]]\)我们需要判断从\(i-2\)跳到\(i-1\)导致\(i\)被删除是否比\(i-2\)直接跳到\(i\)的结果来的好
当然这里还个容易忽视的点是删除\(num_1[i-1]-1\)的问题其实在这里是不用考虑的。因为删除\(num_1[i-2]+1\)的时候已经考虑了这个问题。如果使用\(num_1[i-2]\)那么不能使用\(num_1[i-1]\)
class Solution:
def deleteAndEarn(self, nums: List[int]) -> int:
nums.sort()
num=Counter(nums)
num_1=[]
for k in num.keys():
if k in num_1:
continue
else:
num_1.append(k)
n=len(num_1)
dp=[0]*(n+1)
dp[0]=0
dp[1]=num_1[0]*num[num_1[0]]
for i in range(2,n+1):
if num_1[i-2]==num_1[i-1]-1:
dp[i]=max(dp[i-2]+num_1[i-1]*num[num_1[i-1]],dp[i-1])
else:
dp[i]=dp[i-1]+num_1[i-1]*num[num_1[i-1]]
return dp[n]
class Solution:
def deleteAndEarn(self, nums: List[int]) -> int:
nums.sort()
num=Counter(nums)
num_1=[]
for k in num.keys():
if k in num_1:
continue
else:
num_1.append(k)
n=len(num_1)
last=0
now=num_1[0]*num[num_1[0]]
for i in range(2,n+1):
if num_1[i-2]==num_1[i-1]-1:
now,last=max(last+num_1[i-1]*num[num_1[i-1]],now),now
else:
now,last=now+num_1[i-1]*num[num_1[i-1]],now
return now
2320. 统计放置房子的方式数
本题的重点在于找到初始的两个状态dp[0],dp[1]=1,2,dp[0]=1代表空的状态,dp[1]=1代表唯一的位置为空与非空的状态
\(dp[i]\)的位置如果放房子,那么\(dp[i]\)继承\(dp[i-2]\)的状态否则继承\(dp[i-1]\)
class Solution:
def countHousePlacements(self, n: int) -> int:
#两边是分别独立的
dp=[0]*(n+1)
dp[1],dp[0]=2,1
MOD=10**9+7
for i in range(2,n+1):
dp[i]=dp[i-1]+dp[i-2]
return (dp[n])**2%(MOD)
class Solution {
public int countHousePlacements(int n) {
int[] dp=new int[n+1];
dp[0]=1;
dp[1]=2;
int MOD=(int)1e9+7;
for(int i=2;i<=n;i++){
dp[i]=(dp[i-2]+dp[i-1])% MOD;
}
return (int) ((long) dp[n] * dp[n] % MOD);#注意处理溢出,这是在灵神内边学来的。
}
}
//滚动数组减少内存复杂度
class Solution {
public int countHousePlacements(int n) {
int last=1;
int now=2;
int MOD=(int)1e9+7;
int temp;
for(int i=2;i<=n;i++){
temp=now;
now=(last+now)%MOD;
last=temp;
}
return (int) ((long) now*now % MOD);
}
}
213. 打家劫舍 II
非常简单的一个问题,分为两个数组考虑就行了,一个是不包括末尾,一个是不包括零位置,找到他们的最大值就行
不过值得注意的是,n=1的时候要单独考虑,因为这个时候末尾和零位置是同一个,会使得dp(nums[1:]越界
class Solution:
def rob(self, nums: List[int]) -> int:
def dp(nums):
n=len(nums)
dp=[0]*(n+1)
dp[0]=0
dp[1]=nums[0]
for i in range(2,n+1):
dp[i]=max(dp[i-1],dp[i-2]+nums[i-1])
return dp[n]
n=len(nums)
if n==1:
return nums[0]
return max(dp(nums[1:]),dp(nums[:-1]))
//顺带贴个Java
class Solution {
public int rob(int[] nums) {
int n=nums.length;
if (n==1){
return nums[0];
}
int [] num_1 =new int[n-1];
int [] num_2= new int[n-1];
System.arraycopy(nums,0,num_1,0,n-1);
System.arraycopy(nums,1,num_2,0,n-1);
return Math.max(dp(num_1),dp(num_2));
}
private int dp(int[] nums){
int n=nums.length;
int last=0;
int now=nums[0];
int temp=0;
for (int i=2;i<n+1;i++){
temp=now;
now=Math.max(last+nums[i-1],now);
last=temp;
}
return now;
}
}
1.3 最大子数组和
53. 最大子数组和
很容易看出来,\(dp[i]\)为以第\(i\)个数为结尾的最大的子数组和
那么容易得到
意思是如果\(dp[i-1]<0\),那么这时候\(dp[i]\)应该是\(nums[i]\)相当重新开始一个子数组
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
n=len(nums)
dp=[0]*n
dp[0]=nums[0]
for i in range(1,n):
dp[i]=max(nums[i],dp[i-1]+nums[i])
return max(dp)
#我以为优化了但是实际上没有优化?为什么?
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
n=len(nums)
dp=nums[0]
m=nums[0]
for i in range(1,n):
dp=max(nums[i],dp+nums[i])
m=max(dp,m)
return m
//java不直接使用dp是因为java数组要找到最大值需要转换成stream调用max或者sort,较为麻烦
class Solution {
public int maxSubArray(int[] nums) {
int n=nums.length;
int dp=nums[0];
int m=nums[0];
for (int i=1;i<n;i++){
dp=Math.max(dp+nums[i],nums[i]);
m=Math.max(m,dp);
}
return m;
}
}
最后贴上股票做法,记录之前低价位,然后在高价位卖出。稍微快一些。
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
ans = -inf
min_pre_sum = pre_sum = 0
for x in nums:
pre_sum += x # 当前的前缀和
ans = max(ans, pre_sum - min_pre_sum) # 减去前缀和的最小值
min_pre_sum = min(min_pre_sum, pre_sum) # 维护前缀和的最小值
return ans
# 这里贴的是灵神的解法,简单来说dp[i]=max(dp[i-1],0)+num[i]
class Solution:
def maximumCostSubstring(self, s: str, chars: str, vals: List[int]) -> int:
mapping = dict(zip(chars, vals))
ans = f = 0
for c in s:
f = max(f, 0) + mapping.get(c, ord(c) - ord('a') + 1)
ans = max(ans, f)
return ans
"""
错误解答
例如[8,-2,-6,-1,-10,-6,-6,8],从8开始以8结尾的最大值为8,-2的为6,-6的是8但是按照下面的计算方法就会变成6这是因为我们需要的是绝对值
但是这种方法不对,比如8,-2回导致记录的值变成正的,但是下一个值-6需要的是-2,被我们丢失了这个负数的信息。
正确的解法是应该考虑,子数组的最大值和最小值,这两个的绝对值的最大值就是答案。
"""
class Solution:
def maxAbsoluteSum(self, nums: List[int]) -> int:
n=len(nums)
dp=[0]*(n+1)
m=0
for i in range(1,n+1):
if abs(dp[i-1]+nums[i-1])>abs(nums[i-1]):
dp[i]=dp[i-1]+nums[i-1]
else:
dp[i]=nums[i-1]
m=max(abs(dp[i]),m)
return m
#正确解答
class Solution:
def maxAbsoluteSum(self, nums: List[int]) -> int:
res=0
mi=0
ma=0
for num in nums:
mi=min(mi+num,num)
ma=max(ma+num,num)
res=max(res,ma,-mi)
return res