leetcode 416. 分割等和子集---直接解法

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了leetcode 416. 分割等和子集---直接解法相关的知识,希望对你有一定的参考价值。

在这里插入图片描述



引言

基本的「将原问题抽象为 01 背包问题」的分析在 上一讲 讲过啦 ~

本节要解决的问题是:如何将「间接求解」的方式转为「直接求解」,并学习为什么能这么做,此类做法是否有共性 ...


直接求解

我们先来回顾一下 上一讲 使用的「状态定义」和「转移方程」。

状态定义:

  • dp[i][j]代表考虑前 i 个数值,其选择数字总和不超过 j 的最大价值。
  • 转移方程:
    在这里插入图片描述
  • 但题目并不是问我们「最大价值是多少」,而是问「是否能凑出最大价值」。
  • 因此我们可以对 01 背包的状态定义进行修改,使其直接与我们答案相关联:
  • dp[i][j]代表考虑前 i 个数值,其选择数字总和是否恰好为 j。
  • 此时 dp 数组中存储的是「布尔类型」的动规值。
  • 相应的状态转移方程调整为:
    在这里插入图片描述
  • V 代表逻辑「或」的意思。
  • 新转移方程代表的意思为:想要 dp[i][j] (考虑前 i 个数值,选择的数字总和恰好为 j ) 为真。需要满足以下两种方案,至少一种为 true:

1. dp[i-1][j] (不选第 i 件物品,选择的数字总和恰好为 j ) 为 true;
2. dp[i-1][j-nums[i]] (选第 i 件物品,选择的数字总和恰好为 j ) 为 true;

  • 至此,我们利用 01 背包的基本思想,修改了「状态定义」,使其与答案直接相关联,然后根据新的「状态定义」调整了我们的「转移方程」。
  • 但还没结束。
  • 当我们与某个模型的「状态定义」进行了修改之后,除了考虑调整「转移方程」以外,还需要考虑修改「初始化」状态。
  • 试考虑,我们创建的 dp 数组存储的是布尔类型,初始值都是 false,这意味着无论我们怎么转移下去,都不可能产生一个 true,最终所有的状态都仍然是 false。
  • 换句话说,我们还需要一个有效值 true 来帮助整个过程能递推下去。
  • 通常我们使用「首行」来初始化「有效值」。
  • 对于本题,显然我们可以通过「先处理第一个物品」来得到「有效值」,即令 dp[0][nums[0]]=true。
  • dp[0][nums[0]]=true代表只有容量为nums[0] 的背包才符合「恰好」的要求。
  • 但我们无法确保nums[0] 不会超过我们的「最大背包」容量(也就是第一个物品过大,永远无法装入背包的情况)。
  • 因此我们要通过处理下一行来得到有效值?或是先给物品排个序?
  • 事实上,这里有一个技巧,就是我们增加一个「不考虑任何物品」的情况讨论。
  • 之前我们的状态定义是 dp[i][j] 代表考虑下标为 i 之前的所有物品。
  • 现在我们可以加入 不考虑任何物品 的情况,也就是 将「物品编号」从 0 开始调整为从 1 开始。
  • 举个🌰,原本我们的 dp[0][x] 代表只考虑第一件物品、 dp[1][x]代表考虑第一件和第二件物品;调整后我们的 dp[0][x]代表不考虑任何物品、 dp[1][x]代表只考虑第一件物品 …
  • 这种技巧本质上还是利用了「哨兵」的思想。
  • 有了以上的分析思路,和 上一讲 的代码基础之后,我们可以很容易写出代码。
  • 虽然更换了状态定义和转移方程,但仍然有「常规解法」、「滚动数组优化」「一维空间优化」几种实现方法。我们快速过一下 ~

图解

在这里插入图片描述
大白话时间: 求解当前物品当前容量的状态下的结果其实就是把当前容量减去物品大小,剩余的空间为p,然后问题就转变为了在考虑前一个物品,对应容量为p的情况下能否满足条件,这里就是能否物品最大价值刚好等于背包最大容量,如果能返回true,不能返回false;


常规解法

class Solution {
public:
	bool canPartition(vector<int>& nums) 
	{
		int size = nums.size();
		if (size < 2) return false;
		int sum = accumulate(nums.begin(), nums.end(), 0);
		if (sum & 1) return false;//总和为奇数
		int target = sum / 2;
		// dp[i][j] 代表考虑前 i 件物品,能否凑出价值「恰好」为 j 的方案
		vector<vector<bool>> dp(size+1,vector<bool>(target + 1, false));
		//最小子问题
		dp[0][0]=true;//当你什么物品都没有,并且背包容量为0时,肯定满足条件
		//枚举每一个物品
		for (int i = 1; i <= size; i++)
		{
			int t = nums[i-1];//临时保存当前物品的价值
			for (int j = 0; j <= target; j++)
			{
				//不选择当前物品放入背包
				bool unsel = dp[i - 1][j];
				//选择当前物品放入背包
				bool sel = j >= t ? dp[i - 1][j - t] : false;

				dp[i][j] = unsel || sel;
			}
		}
		return dp[size][target];
	}
};

在这里插入图片描述


「滚动数组」解法

class Solution {
public:
	bool canPartition(vector<int>& nums) 
	{
		int size = nums.size();
		if (size < 2) return false;
		int sum = accumulate(nums.begin(), nums.end(), 0);
		if (sum & 1) return false;//总和为奇数
		int target = sum / 2;
		// dp[i][j] 代表考虑前 i 件物品,能否凑出价值「恰好」为 j 的方案
		vector<vector<bool>> dp(2,vector<bool>(target + 1, false));
		//最小子问题
		dp[0][0]=true;//当你什么物品都没有,并且背包容量为0时,肯定满足条件
		//枚举每一个物品
		for (int i = 1; i <= size; i++)
		{
			int t = nums[i-1];//临时保存当前物品的价值
			for (int j = 0; j <= target; j++)
			{
				//不选择当前物品放入背包
				bool unsel = dp[(i - 1)&1][j];
				//选择当前物品放入背包
				bool sel = j >= t ? dp[(i - 1)&1][j - t] : false;

				dp[i&1][j] = unsel || sel;
			}
		}
		return dp[size&1][target];
	}
};

在这里插入图片描述


「一维空间优化」解法

class Solution {
public:
	bool canPartition(vector<int>& nums) 
	{
		int size = nums.size();
		if (size < 2) return false;
		int sum = accumulate(nums.begin(), nums.end(), 0);
		if (sum & 1) return false;//总和为奇数
		int target = sum / 2;
		// 取消「物品维度」
		vector<bool> dp(target + 1);
		//最小子问题
		dp[0]=true;//当你什么物品都没有,并且背包容量为0时,肯定满足条件
		//枚举每一个物品
		for (int i = 1; i <= size; i++)
		{
			int t = nums[i-1];//临时保存当前物品的价值
			for (int j = target; j>=0; j--)//防止数据覆盖,从尾端到前段覆盖
			{
				//不选择当前物品放入背包
				bool unsel = dp[j];
				//选择当前物品放入背包
				bool sel = j >= t ? dp[j - t] : false;

				dp[j] = unsel || sel;
			}
		}
		return dp[target];
	}
};

在这里插入图片描述

以上是关于leetcode 416. 分割等和子集---直接解法的主要内容,如果未能解决你的问题,请参考以下文章

leetcode 416. 分割等和子集

LeetCode 416. 分割等和子集

Leetcode刷题Python416. 分割等和子集

(Java) LeetCode 416. Partition Equal Subset Sum —— 分割等和子集

416-分割等和子集(01背包)

LeetCode 416. 分割等和子集 c++/java详细题解