Leetcode-动态规划
动态规划5部曲
- 确定dp数组以及下标的含义
- 确定递推公式
- dp数组如何进行初始化,如dp[0]、dp[1],以及边界条件
- 确定遍历顺序
- 举例推导dp数组
Leetcode题目
1.斐波那契数
- dp数组代表最后的数列和
- 递推公式dp[n]=dp[n-1]+dp[n-2]
- 初始化dp[0]=0,dp[1]=1,
- 遍历顺序从左向右遍历,for循环从i=2开始
- 声明数组dp长度为n+1大小,最后返回dp[n]
注:声明数组长度为n+1是因为数组索引是从0开始的,要想返回dp[n],必须要声明n+1长度的数组。
class Solution {
public:
int fib(int n) {
if(n<=1){
return n;
}
vector<int>dp(n+1,0);
dp[0]=0;
dp[1]=1;
for(int i=2;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
};
2.爬楼梯
- dp代表有多少中方法爬到楼顶
- 递推公式dp[n]=dp[n-1]+dp[n-2]
- 初始化这时不要使用dp[0]了,容易混淆概念,先初始化一个长度为n+1的数组dp(n+1,0),再初始化dp[1]=1,dp[2]=2;
- for循环遍历从左到右,初始化int i=3,i<=n.
class Solution {
public:
int climbStairs(int n) {
if(n<=1){
return n;
}
vector<int>dp(n+1,0);
dp[1]=1;
dp[2]=2;
for(int i=3;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
};
3.爬楼梯-最小花费
- dp下标含义为最小花费
- 递推公式为dp[i]=min(dp[i-1],dp[i-2])+cost[i];注意这里是比较dp[i-1]和dp[i-2]之间最小值再加上cost[i]
- 初始化dp[0]=cost[0],dp[1]=cost[1],声明数组dp大小为n
- 最后返回return min(dp[n-1],dp[n-2]);为什么要比较n-1和n-2位置呢
以cost=[10,15,20]为例:
dp[0]=10
dp[1]=15
dp[2]=min(dp[0],dp[1])+cost[2]=30,此时等于30这不是最小花费,最小花费是要将n-1和n-2处花费进行比较
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int n=cost.size();
vector<int>dp(n);
dp[0]=cost[0];
dp[1]=cost[1];
for(int i=2;i<n;i++){
dp[i]=min(dp[i-1],dp[i-2])+cost[i];
}
return min(dp[n-1],dp[n-2]);
}
};
4.不同路径
- dp数组代表路径数
- 递推公式dp[m][n]=dp[m-1][n]+dp[m][n-1]
- 初始化由于左边一列和上边一行都只有一条路径才能到达,因此全部初始化为1
注:二维数组初始化vecor<vector>dp(m,vector(n,0))
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>>dp(m,vector<int>(n,0));
for(int i=0;i<m;i++){
dp[i][0]=1;
}
for(int j=0;j<n;j++){
dp[0][j]=1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
4.不同路径||
- dp数组下标代表路径数
- 递推公式dp[m][n]=dp[m-1][n]+dp[m][n-1]
- 这道题主要考虑遇到障碍怎么处理?
初始化时如果遇到障碍怎么处理?障碍值为1,如果grid值为1你那么立即终止循环,后面的路径数应该都为0.
比如初始化列我给出两段代码:
遇到grid值为1初始化为0,值为0初始化为1
for(int j=0;j<n;j++){
if(obstacleGrid[0][j]==1){
dp[0][j]=0;
}
dp[0][j]=1;
}
在循环体判断条件加上如果grid值为0才给与初始化,否则直接退出循环,这才是正确的代码,因为如果遇到障碍1后面的路都不通了,路径数就到此为止了。
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++){
dp[0][j] = 1;
}
双层for循环代码细节:遇到障碍为1时直接continue下一次循环。
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m=obstacleGrid.size();
int n=obstacleGrid[0].size();
vector<vector<int>>dp(m,vector<int>(n,0));
for(int i=0;i<m&&obstacleGrid[i][0]==0;i++){
dp[i][0]=1;
}
for(int j=0;j<n&&obstacleGrid[0][j]==0;j++){
dp[0][j]=1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
if(obstacleGrid[i][j]==1){
continue;
}
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
5.整数拆分
- dp数组代表乘积和
- 递推公式:dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]))
- 由于0和1拆分没有任何意义,因此从dp[2]=1开始初始化
- for循环遍历初始化i=3,j=1,j<i作为内层循环判断条件
class Solution {
public:
int integerBreak(int n) {
vector<int>dp(n+1,0);
dp[2]=1;
for(int i=3;i<=n;i++){
for(int j=1;j<i;j++){
dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]));
}
}
return dp[n];
}
};
}
6.分割等和子集
- dp数组代表数组元素和
- 递推公式采用一维滚动数组dp[j]=max(dp[j],dp[j-nums[i]]+nums[i])
- 初始化dp[0]=0;声明数组长度为10001,防止数组加在一起超过20000除以2,注:在动态规划里数组长度可以随便声明多长,只是占用空间复杂度罢了。
- for循环外层循环遍历物品,内层循环遍历背包,且内层循环是从后往前进行循环。
- 最后如果dp[bagweight]=bagweight,return true;
class Solution {
public:
bool canPartition(vector<int>& nums) {
int bagweight=0;
for(int i=0;i<nums.size();i++){
bagweight+=nums[i];
}
if(bagweight %2==1){
return false;
}
bagweight=bagweight/2;
vector<int>dp(10001,0);
dp[0]=0;
for(int i=0;i<nums.size();i++){
for(int j=bagweight;j>=nums[i];j--){
dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
if(dp[bagweight]==bagweight){
return true;
}
return false;
}
};
7.目标和
- 这道题是0-1背包的组合问题,之前的0-1背包解决的是背包最多能放多少物品,而这道题解决的是装满背包有多少中方法,且每个数只能取一次。类似组合问题:
dp[j]+=dp[j-nums[i]];
- dp数组代表方法数
- 递推公式:
dp[j]+=dp[j-nums[i]];
- 初始化dp[0]=1;
- 计算:正子集left,负子集sum-left,正子集-负子集=target,left-(sum-left)=target,得到left=(sum+target)/2;判断:
if(abs(target)>sum){ return 0; } if((sum+target)%2==1){ return 0; }
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum=0;
for(int i=0;i<nums.size();i++){
sum+=nums[i];
}
if(abs(target)>sum){
return 0;
}
if((sum+target)%2==1){
return 0;
}
int bagsize=(sum+target)/2;
vector<int>dp(bagsize+1,0);
dp[0]=1;
for(int i=0;i<nums.size();i++){
for(int j=bagsize;j>=nums[i];j--){
dp[j]+=dp[j-nums[i]];
}
}
return dp[bagsize];
}
};
8.零钱兑换||
- 这是一道完全背包问题,因为物品可以一直加入。注意遍历顺序
- dp数组代表组合数:组合不强调组合的顺序。排列强调组合的顺序
- dp数组初始化dp[0]=1
- 递推公式求组合数
dp[j]+=dp[j-coins[i]]
- 遍历顺序外层物品开始遍历,内层遍历背包容量从小到大遍历
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int>dp(amount+1,0);
dp[0]=1;
for(int i=0;i<coins.size();i++){
for(int j=coins[i];j<=amount;j++){
dp[j]+=dp[j-coins[i]];
}
}
return dp[amount];
}
};
9.组合总和IV
- 求组合数外层for循环遍历物品,内层for循环遍历背包
- 求排列数外层for循环遍历背包,内层for循环遍历物品
- do数组代表组合数
- dp[0]=1
- 递推公式dp[i]+=dp[i-nums[i]]
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target + 1, 0);
dp[0] = 1;
for (int i = 0; i <= target; i++) { // 遍历背包
for (int j = 0; j < nums.size(); j++) { // 遍历物品
if (i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]) {
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
};