动态规划刷题总结(二)
求解动态规划问题的关键,通常在怎么找出最优子结构的递推公式,和dp数组中应该储存的什么信息;
解决了这两个问题,动态规划问题就很好求解了。但是,从最近的刷题经历来看,这两个问题是不好解
决的,不同的问题存在不同的变化,得进行不同方面的思考。
494.目标和
题目介绍:
给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意
一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
解题关键:
设全添加和为sum
,全添负号和为-sum
。
建立一个size*(2*sum+1)
大小的数组vector<vector<int> > dp(size,vector<int>(2*sum+1,0));
用来储存对第i个数添加正号或者负号时和为-sum
到sum
间某个值的方法数。
状态转移方程如下:
dp[i][j] = dp[i - 1][j - nums[i]] + dp[i - 1][j + nums[i]]
可换成如下递推方程:
dp[i][j + nums[i]] += dp[i - 1][j]
dp[i][j - nums[i]] += dp[i - 1][j]
此时只需满足dp[i - 1][j]
大于0即可。
代码如下:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int S) {
if(!nums.size())
return 0;
int size=nums.size();
int sum=0;
for(int i=0;i<size;i++)
sum+=nums[i];
vector<vector<int> > dp(size,vector<int>(2*sum+1,0));
dp[0][nums[0]+sum]=1;
dp[0][-nums[0]+sum]+=1;
for(int i=1;i<size;i++){
for(int j=-sum;j<=sum;j++){
if(dp[i-1][j+sum]>0){
dp[i][j+nums[i]+sum]+=dp[i-1][j+sum];
dp[i][j-nums[i]+sum]+=dp[i-1][j+sum];
}
}
}
return S>sum?0:dp[size-1][S+sum];
}
};
注意:因为数组的下标不能为负,所以所有的j
都进行了**+sum
的偏移**。
1049.最后一块石头的重量II
题目介绍:
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那
么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
解题关键:
将石头分成最相近的两堆,此时就能得出最小的最后一块石头的重量。
递推关系式dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);
其中int maxCap=sum/2;
dp[j]
储存的是容量为j
时的最大重量
代码如下:
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int size=stones.size();
if(stones.size()==1)
return stones[0];
if(size==0)
return 0;
int sum=0;
for(int i=0;i<size;i++){
sum+=stones[i];
}
int maxCap=sum/2;
vector<int> dp(sum/2+1);
for(int i=0;i<size;i++){
for(int j=maxCap;j>=stones[i];j--){
dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
return sum-dp[maxCap]-dp[maxCap];
}
};
注意:j从后往前是因为进行了状态压缩,防止覆盖;return
的是两堆石头重量之差
935.骑士拨号器
题目介绍:
国际象棋中的骑士可以按下图所示进行移动:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ErjWuGj-1605354371976)(C:\Users\xxj99\AppData\Roaming\Typora\typora-user-images\image-20201114193739901.png)]
这一次,我们将 “骑士” 放在电话拨号盘的任意数字键(如上图所示)上,接下来,骑士将会跳 N-1 步。
每一步必须是从一个数字键跳到另一个数字键。
每当它落在一个键上(包括骑士的初始位置),都会拨出键所对应的数字,总共按下 N 位数字。
你能用这种方式拨出多少个不同的号码?
因为答案可能很大,所以输出答案模 10^9 + 7。
解题关键:
此题其实比较简单,只需要注意当前数字能由哪些数字跳过来就ok了
直接上代码:
class Solution {
public:
int knightDialer(int n) {
vector<vector<long long> > dp(n,vector<long long>(10,0));
int mod=(int)(pow(10,9)+7);
for(int i=0;i<10;i++)
dp[0][i]=1;
for(int i=1;i<n;i++){
dp[i][1]=(dp[i-1][6]+dp[i-1][8])%mod;
dp[i][2]=(dp[i-1][7]+dp[i-1][9])%mod;
dp[i][3]=(dp[i-1][4]+dp[i-1][8])%mod;
dp[i][4]=(dp[i-1][3]+dp[i-1][9]+dp[i-1][0])%mod;
dp[i][5]=0;
dp[i][6]=(dp[i-1][1]+dp[i-1][7]+dp[i-1][0])%mod;
dp[i][7]=(dp[i-1][2]+dp[i-1][6])%mod;
dp[i][8]=(dp[i-1][1]+dp[i-1][3])%mod;
dp[i][9]=(dp[i-1][2]+dp[i-1][4])%mod;
dp[i][0]=(dp[i-1][4]+dp[i-1][6])%mod;
}
int ans=0;
for(int i=0;i<10;i++)
ans=(ans+dp[n-1][i])%mod;
return ans;
}
};
注意:答案为什么要模1e9+7
呢?首先因为1e9+7
是一个质数,模它减少了余数间存在公因数的可能;
dp[i][0]=(dp[i-1][4]+dp[i-1][6])%mod;
}
int ans=0;
for(int i=0;i<10;i++)
ans=(ans+dp[n-1][i])%mod;
return ans;
}
};
注意:答案为什么要模1e9+7
呢?首先因为1e9+7
是一个质数,模它减少了余数间存在公因数的可能;
其次两个1e9+7
相加小于2^31
,未超过int
的范围,两个1e9+7
相乘未超过long
的范围。