代码随想录Day33-动态规划:力扣第343m、96m、416m、1049m、494m题

343m. 整数拆分

题目链接
代码随想录文章讲解链接

方法一:动态规划

用时:22m52s

思路

dp数组:dp[i]表示拆分i得到的最大乘积
状态转移方程:将i拆分成j和i-j,当固定j时,dp[i] = max(j * (i - j), j * dp[i - j]),j * (i - j)就相当于将i拆成了两个数i和i-j,j * dp[i - j]就相当于将i拆成了i和i-j后,i-j继续拆分,dp[i - j]就是i-j拆分得到的最大乘积。遍历所有的j,就相当于遍历了所有拆分情况。

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n ) O(n) O(n)
C++代码
class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(n + 1, 0);  // dp数组
        dp[2] = 1;  // dp数组初始化
        for (int i = 3; i <= n; ++i) {
            for (int j = 1; j <= i / 2; ++j) {
                dp[i] = max(dp[i], max(j * dp[i - j], j * (i - j)));
            }
        }
        return dp[n];
    }
};

方法二:动态规划优化

思路

方法一对于每个i,j的取值需要从1遍历到i-1,但其实只用讨论j取2或者3的情况即可。
j=1:j取1不可能得到最大乘积。
j=4:4可以拆分成2和2,22=4,所以j取4的情况和j取2的情况是一致的。
j>=5:此时j不拆分一定比拆分的最大乘积小,所以j取大于等于5一定不是最大乘积。(例如:5<2
3、6<33、7<23*3)
所以,在计算dp[i]时,只用判断j取2和3的情况,对于每个i计算dp[i]的时间复杂度将为常数级。

  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n)
C++代码
class Solution {
public:
    int integerBreak(int n) {
        if (n <= 3) return n - 1;
        vector<int> dp(n + 1, 0);
        dp[2] = 1;
        for (int i = 3; i <= n; ++i) {
            dp[i] = max(max(2 * (i - 2), 2 * dp[i - 2]), max(3 * (i - 3), 3 * dp[i - 3]));
        }
        return dp[n];
    }
};

看完讲解的思考

妙蛙妙蛙。

代码实现遇到的问题

无。


96m. 不同的二叉搜索树

题目链接
代码随想录文章讲解链接

方法一:动态规划

用时:9m47s

思路

dp数组:dp[i]表示i个节点的BST的种数
如何计算dp[i]:对于整数i,BST的节点的值是1到i,假设根节点的值为j,由BST的性质可知,左子树的节点数就是j-1,右子树的节点数就是i-j,左子树的种数就是dp[j-1],右子树的种树就是dp[i-j],那么以j为根节点的种数就为dp[j-1]*dp[i-j],从1到i遍历j,计算不同节点作为根节点的种数并求和,就能得到dp[i]。

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n ) O(n) O(n)
C++代码
class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n + 1, 0);
        dp[0] = 1, dp[1] = 1;
        for (int i = 2; i <= n; ++i) {
            for (int j = 1; j <= i; ++j) {
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
    }
};

看完讲解的思考

居然自己一枪A了,嘿嘿。

代码实现遇到的问题

无。


01背包

问题描述

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

示例:
输入:w = 4, weight = [1, 3, 4], value = [15, 20, 30]
输出:35
解释:装入索引为0和1的物品,总重为1+3<=4,物品价值最大,为15+20=35。

方法一:动态规划

思路

dp数组:使用二维数组,dp[i][j]表示物品0i放置到容量为j的背包的最大价值
递推公式:对于dp[i][j],我们把它看作对于容量为j的背包,已经从物品0i - 1中选择物品放入了,此时新来了物品i,有三种情况:

  1. 物品i的重量大于背包的容量,物品i无法放进背包:dp[i][j] = dp[i - 1][j]
  2. 物品i的重量小于背包的容量,物品i不放进背包:dp[i][j] = dp[i - 1][j]
  3. 物品i的重量小于背包的容量,物品i放进背包:物品i放进背包,那么背包的剩余容量为总容量减去物品i的重量,即j - weight[i],剩余容量能放置物品的最大价值为dp[i - 1][j - weight[i]],所以dp[i][j]就等于剩余容量的最大价值加上物品i的价值,即dp[i][j] = dp[i - 1][j - weight[i]] + value[i]

对于后两种情况,我们取其中的最大值即为最大价值。

  • 时间复杂度: O ( n ∗ w ) O(n*w) O(nw),n是物品的个数,w是背包的总容量。
  • 空间复杂度: O ( n ∗ w ) O(n*w) O(nw)
C++代码
class Solution {
public:
    bool zeroOnePack(int w, vector<int>& weight, vector<int>& value) {
    	int size = weight.size();
		vector<vector<int>> dp(size, vector<int>(w + 1, 1));
		// 初始化dp数组
		for (int j = 0; j <= w; ++j) {
			if (weight[0] > j) dp[0][j] = 0;
			else break;
		}
		// dp
		for (int i = 1; i < size; ++i) {
			for (int j = 0; j <= w; ++j) {
				if (weight[i] > j) dp[i][j] = dp[i - 1][j];
				else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
			}
		}
		return dp[size - 1][w];
    }
};

方法二:动态规划+滚动数组

思路

分析方法一的递推公式dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]),可以发现dp[i]的状态只依赖于dp[i - 1],所以对于i - 1之前的dp数组其实不用记录下来,空间上还可以优化,我们可以只用一个一维数组来记录上一时刻的dp数组,然后不断更新这个一维数组即可:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
dp数组初始化:使用滚动数组不需要像二维数组一样初始化,直接初始化为0即可。
遍历顺序:从右往左遍历,因为当前位置的值依赖于前边的值,如果先遍历左边的话会改变前边的值。

  • 时间复杂度: O ( n ∗ w ) O(n*w) O(nw)
  • 空间复杂度: O ( w ) O(w) O(w)
C++代码
class Solution {
public:
    bool zeroOneBag(int w, vector<int>& weight, vector<int>& value) {
		int size = weight.size();
		vector<vector<int>> dp(w + 1, 0);
		for (int i = 0; i < size; ++i) {
			for (int j = w; j >= weight[i]; --j) {  // 当前背包容量j要大于物品i的重量
				dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
			}
		}
		return dp[w];
    }
};

看完讲解的思考

方法二好妙啊

代码实现遇到的问题

无。


416m. 分割等和子集

题目链接
代码随想录文章讲解链接

方法一:动态规划-01背包+滚动数组

用时:14m40s

思路

本题关键在于将题目转化,找到两个子集的元素和相等,其实就是:能否找到一个子集,该子集的和等于数组和sum的一半。
再进一步转化为01背包问题:背包容量为sum / 2,nums数组相当于每个物品的重量。
dp数组:一维滚动数组,dp[j]表示用容量为j的背包装物品0到i,背包能装的最大重量。
状态转移方程:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])

  • 时间复杂度: O ( s u m 2 ∗ n ) O(\frac{sum}{2}*n) O(2sumn),n是数组的大小。
  • 空间复杂度: O ( s u m 2 ) O(\frac{sum}{2}) O(2sum)
C++代码
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for (int i = 0; i < nums.size(); ++i) sum += nums[i];
        if (sum & 1) return false;
        vector<int> dp(sum / 2 + 1, 0);
        for (int i = 0; i < nums.size(); ++i) {
            for (int j = sum / 2; j >= nums[i]; --j) {
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
                if (dp[j] == sum / 2) return true;
            }
        }
        return false;
    }
};

看完讲解的思考

这题目居然都能转化成01背包,牛。

代码实现遇到的问题

无。


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

题目链接
代码随想录文章讲解链接

方法一:动态规划-01背包

用时:15m57s

思路

关键在于将问题转化为01背包问题:找到一个石头的子集A,使得A的总重量w尽可能地接近所有石头总重量sum的一半,这样将石头粉碎后最后一块石头的重量就是sum - w - w,因为用A和剩下的石头粉碎,A肯定全都没了,所以重量是sum - w,剩下的石头因为和A粉碎,自己也会减少w,所以最终重量就是sum - w - w。
故问题就转换成:背包容量为sum / 2,求装石头能装的最大重量。

  • 时间复杂度: O ( s u m 2 ∗ n ) O(\frac{sum}{2}*n) O(2sumn),n是石头的个数。
  • 空间复杂度: O ( s u m 2 ) O(\frac{sum}{2}) O(2sum)
C++代码
class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = accumulate(stones.begin(), stones.end(), 0);
        vector<int> dp(sum / 2 + 1, 0);
        for (int i = 0; i < stones.size(); ++i) {
            for (int j = sum / 2; j >= stones[i]; --j) dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
        }
        return sum - 2 * dp[sum / 2];
    }
};

看完讲解的思考

无。

代码实现遇到的问题

无。


494. 目标和

题目链接
代码随想录文章讲解链接

方法一:动态规划-01背包

用时:23m12s

思路

数组的和为sum,我们要将一部分数字做加法,另一部分数字做减法,设做加法的数字的和为x,则做减法的数字的和为sum - x,我们的目标是找到x,使得target = x - (sum - x),即x = (target + sum) / 2
将题目转换为01背包问题:背包的容量为x,求和为x的子集的数量。

  • dp数组:dp[j]表示背包容量为j,用数组0i的元素来装背包,恰好装满背包的子集数量。
  • 状态转移:dp[j] = dp[j] + dp[j - nums[i]]。如何理解:新增元素i,组合的数量是在原先的dp[j]的基础上增加,因为可以不要新增的元素i,不要元素i的组合数量就等于原先的dp[j];如果要元素i,那么组合的数量就等于dp[j - nums[i]],要了元素i,相当于背包容量变成了j - nums[i],然后在0i - 1的元素中挑选,恰好装满背包的组合数量,即dp[j - nums[i]]
  • 初始化dp数组:dp数组初始化相当于不选任何元素放到背包,此时元素和为0,也就是只有dp[0] = 1,其余都为0。
  • 时间复杂度: O ( t a r g e t + s u m 2 ∗ n ) O(\frac{target + sum}{2}*n) O(2target+sumn)
  • 空间复杂度: O ( t a r g e t + s u m 2 ) O(\frac{target + sum}{2}) O(2target+sum)
C++代码
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if (abs(target) > sum || (target + sum) & 1) return 0;
        int x = (target + sum) / 2;
        vector<int> dp(x + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < nums.size(); ++i) {
            for (int j = x; j >= nums[i]; --j) dp[j] += dp[j - nums[i]];
        }
        return dp[x];
    }
};

方法二:回溯

用时:9m27s

思路

回溯搜索所有的组合,判断是否满足条件,时间复杂度高。

  • 时间复杂度: O ( 2 n ) O(2^n) O(2n)
  • 空间复杂度: O ( n ) O(n) O(n)
C++代码
class Solution {
private:
    int x;
    int curSum;
    int res;

    void dfs(vector<int>& nums, int begin) {
        if (curSum == x) ++res;  // 如果当前和等于x,则种数加一
        if (begin == nums.size() || curSum > x) return;  // 回溯终止条件:遍历完全部元素,或者当前和已经大于x
        for (int i = begin; i < nums.size(); ++i) {
            curSum += nums[i];
            dfs(nums, i + 1);  // 递归
            curSum -= nums[i];  // 回溯
        }
    }

public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        x = (target + sum) / 2;
        if (abs(target) > sum || (target + sum) & 1) return 0;
        
        curSum = 0;
        res = 0;
        dfs(nums, 0);
        return res;
    }
};

看完讲解的思考

无。

代码实现遇到的问题

无。


最后的碎碎念

第一天做dp题目,我:就这?
第二天做dp题目,dp题目:就这?
今天的dp题狠狠地拷打我了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值