LeetCode高频面试题|LeetCode 416. 分割等和子集 【c++/java详细题解】

来自专栏《LeetCode高频面试题》 欢迎订阅

目录
      • 1、题目
      • 2、思路
      • 3、二维c++代码
      • 4、二维java代码
      • 5、一维优化
      • 6、一维c++代码
      • 7、一维java代码

1、题目
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:
输入:nums = [1,2,3,5] 输出:false 解释:数组不能分割成两个元素和相等的子集。

提示:
  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100
2、思路
(动态规划)O ( n ? m ) O(n * m) O(n?m)
给定一个只包含正整数的非空数组 nums[0],判断是否可以从数组中选出一些数字,使得这些数字的和恰好等于整个数组的元素和的一半。
样例:
LeetCode高频面试题|LeetCode 416. 分割等和子集 【c++/java详细题解】
文章图片

如样例所示,nums = [1,5,11,5],数组可以分割成 [1, 5, 5][11],因此返回ture
从题意来看,这个问题可以转换成0-1背包问题,如何看出来的? 我们不妨将换种表述方式:
将大小为n的数组看成n件物品,数组元素和sum的一半看成一个容量为sum / 2的背包, 每件物品只能使用一次,每件物品的体积是nums[i],求解是否可以选出一些物品,使得这些物品的总体积恰好为背包的容量,因此可以使用动态规划求解,下面我们来讲解具体做法。
首先,如果sum 是奇数,则不可能将数组分割成元素和相等的两个子集,因此直接返回false。接下来我们去定义状态表示和推导状态转移方程。
【LeetCode高频面试题|LeetCode 416. 分割等和子集 【c++/java详细题解】】状态表示: f[i][j]表示从前i个数中选若干个数,是否使得这些数字的和恰好等于j。因此f[i][j]有两种状态,true或者false
状态计算:
假定nums[]数组下标从1开始,如何确定f[i][j]的值?
一般去考虑最后一步,那么对于当前的数字 nums[i],可以选取也可以不选取:
  • 1、不选 nums[i],那么我们就从前i - 1个数中选,看是否使得这些数字的和恰好等于j,即 f[i][j] = f[i - 1][j]
  • 2、选择nums[i] ,在背包可以装下的情况下,那么相应的背包容量就要减去nums[i] ,f[i][j]的状态就可以从f[i - 1][j - nums[i]]转移过来,即f[i][j] = f[i - 1][j - nums[i]]
综上,两种情况只要有一个为truef[i][j]就为true。因此状态转移方程为f[i][j] = f[i - 1][j] | f[i - 1][j - nums[i]]
初始化:
f[0][0] = true:在前0个数中,我们可以一个数都不去选,因此从前0个数中选,使得这些数字的和恰好等于0的状态为true,其余的状态都初始化为false
实现细节:
在推导状态转移方程时,我们假设的nums[]数组下标是从1开始的,而实际中的nums[]数组下标是从0开始的,因此在代码的编写过程中,我们需要将所有nums[i]的下标减去 1,与使用的语言保持一致。
时间复杂度分析:O ( n ? m ) O(n * m) O(n?m),nnums数组的大小,m数组元素和的一半。
空间复杂度分析: O ( n ? m ) O(n * m) O(n?m)
3、二维c++代码
class Solution { public: bool canPartition(vector& nums) { int n = nums.size(), sum = 0; for(int x : nums) sum += x; if(sum % 2) return false; int m = sum / 2; vector> f(n + 1, vector(m + 1, false)); f[0][0] = true; for(int i = 1; i <= n; i++){ for(int j = 1; j <= m; j++){ if(j >= nums[i - 1]) f[i][j] = f[i - 1][j - nums[i - 1]] | f[i - 1][j]; else f[i][j] = f[i - 1][j]; } } return f[n][m]; } };

4、二维java代码
class Solution { public boolean canPartition(int[] nums) { int n = nums.length, sum = 0; for(int x : nums) sum += x; if(sum % 2 != 0) return false; int m = sum / 2; boolean[][] f = new boolean[n + 1][m + 1]; f[0][0] = true; for(int i = 1; i <= n; i++){ for(int j = 1; j <= m; j++){ if(j >= nums[i - 1]) f[i][j] = f[i - 1][j - nums[i - 1]] || f[i - 1][j]; else f[i][j] = f[i - 1][j]; } } return f[n][m]; } }

5、一维优化
我们可以发现,在计算 f[i][j]的过程中,每一行f[i][j]的值只与上一行的f[i - 1][j]有关,因此考虑去掉前一维,则状态转移方程为:f[j] = f[j] | f[j - nums[i]]
如果此时我们继续考虑第二层循环j从小往大计算,即:
for (int i = 1; i <= n; i++){//为了下标对应,实际nums[i]应取nums[i - 1] for (int j = nums[i]; j <= m; j++){ f[j] = f[i] | f[j - nums[i]]; } }

此时的状态便与二维的状态不等价了,因为在计算第i层的状态时,我们从小到大枚举, j - nums[i]严格小于j,那么f[ j- nums[i]]一定会先于f[j]被计算出来,于是我们计算出来的f[j - nums[i]]仍为第i层状态,这样f[j - nums[i]]等价于f[i][j-nums[i]] ,实际上f[j - nums[i]]应该等价于f[i - 1][j - nums[i]]
为了解决这个问题只需要将j从大到小枚举。
for (int i = 1; i <= n; i++){//为了下标对应,实际nums[i]应取nums[i - 1] for (int j = m; j >= nums[i]; j -- ){ f[j] = f[i] | f[j - nums[i]]; } }

因为我们从大到小枚举j,而 j - nums[i]严格小于j,于是我们在计算f[j] 的时候f[j - nums[i]]还未被第i层状态更新过,那么它存的就是上一层(i - 1层)的状态,即等价于f[i - 1][j - nums[i]]
空间复杂度分析: O ( n ) O(n) O(n)
6、一维c++代码
class Solution { public: bool canPartition(vector& nums) { int n = nums.size(), m = 0; for (int x: nums) m += x; if (m % 2) return false; m /= 2; vector f(m + 1); f[0] = true; for (int i = 1; i <= n; i++) for (int j = m; j >= nums[i - 1]; j -- ) f[j] = f[j] | f[j - nums[i - 1]]; return f[m]; } };

7、一维java代码
class Solution { public boolean canPartition(int[] nums) { int n = nums.length, m = 0; for (int x: nums) m += x; if (m % 2 != 0) return false; m /= 2; boolean[] f = new boolean[m + 1]; f[0] = true; for (int i = 1; i <= n; i++) for (int j = m; j >= nums[i - 1]; j -- ) f[j] |= f[j - nums[i - 1]]; return f[m]; } }

如果我的文章对你有帮助的话,欢迎一键三连!!!
原题链接: 416. 分割等和子集
LeetCode高频面试题|LeetCode 416. 分割等和子集 【c++/java详细题解】
文章图片

    推荐阅读