Leetcode 分割等和子集

ops/2024/10/19 10:56:09/

在这里插入图片描述
这段代码的目的是解决 LeetCode 416 问题:分割等和子集,即判断一个只包含正整数的数组,是否能够将其分割成两个子集,使得这两个子集的元素和相等。

算法思想(动态规划 - 背包问题)

该问题本质上是一个经典的动态规划问题,可以转化为“0-1 背包问题”,具体解释如下:

1. 总和判断:
  • 首先,我们需要计算数组中所有元素的总和 sum
  • 如果这个总和 sum奇数,那我们无法将其分为两个和相等的子集,因为奇数无法均匀分成两个整数。因此,直接返回 false
  • 如果 sum偶数,则问题转化为:我们能否找到一个子集,使得该子集的和等于 sum / 2。如果可以找到这样一个子集,那么剩余的元素自然也会构成另一个子集,它们的和也为 sum / 2
2. 动态规划:
  • 动态规划的核心思想是:我们用一个布尔数组 dp 来表示从数组中是否可以选出若干元素使得这些元素的和为 j。其中 dp[j] 表示数组中的某些子集是否可以构成和为 j 的子集。
  • 初始化:dp[0] = true,表示我们可以通过不选择任何元素来构成和为 0 的子集。
  • 遍历数组中的每个元素 num,从后向前更新 dp 数组。对于每一个 num,我们要判断是否能通过之前的选择和这个 num 构成和为 j 的子集。
3. 代码执行过程:
  • 首先计算数组元素的总和。
  • 如果总和是奇数,直接返回 false
  • 然后创建一个目标和 target = sum / 2,这也是我们希望找到的子集的和。
  • 初始化一个长度为 target + 1 的布尔数组 dp,用于记录是否可以通过某些元素构成特定的子集和。
  • 对数组中的每个元素 num,从后向前遍历 dp 数组,更新 dp[j],表示是否可以用之前的元素和当前元素 num 构成和为 j 的子集。
  • 最后,dp[target] 的值即为我们想要的结果,如果 dp[target]true,则表示可以找到这样的子集,返回 true;否则返回 false

动态规划的状态转移方程:

  • dp[j] = dp[j] || dp[j - num],即:
    • 如果在没有当前数字 num 的情况下可以构成和为 j 的子集,那么 dp[j]true
    • 或者,如果在没有当前数字 num 的情况下可以构成和为 j - num 的子集,那么加上当前数字 num 后也可以构成和为 j 的子集。

时间复杂度:

  • 时间复杂度为 O(n * target),其中 n 是数组中元素的个数,target 是总和的一半(即 sum / 2)。

总结:

这个问题使用动态规划解决,类似于“背包问题”的思路。通过判断是否可以找到和为 sum / 2 的子集,我们就可以判断数组是否能够被分割成两个和相等的子集。

java solution

class Solution {public boolean canPartition(int[] nums) {//只有nums所有元素和为偶数时,才有可能分割等和子集int sum = 0;for(int num : nums) {sum = sum + num;}if(sum % 2 != 0) {return false;}//如果所有元素和为偶数, 那么我们只要可以找到凑成和为 sum / 2 的子集,就可以返回 trueint target = sum / 2;//创建boolean类型的dp数组, dp[i] 表示子集元素是否可以凑成和为 i// 通过 dp[target] 来判断是否凑成功, 所以创建  target + 1 个存储单元boolean[] dp = new boolean[target + 1];  //创建的boolean数组初始值默认为falsedp[0] = true; //我们可以不选择任何元素来凑成 0//遍历每个数组元素,从后往前更新 dpfor(int num : nums) {for(int j = target; j >= num; j--) {dp[j] = dp[j] || dp[j - num];}}return dp[target];}
}

如果 dp[j] 原本是 true,表示已经能够找到和为 j 的子集,保留 true
或者,如果 dp[j - num]true,表示可以通过之前的元素组合成和为 j - num 的子集,那么加上当前的 num,就可以组合成和为 j 的子集,所以 dp[j] 也应更新为 true

这部分代码片段,为什么是从后往前更新dp数组?

    // 遍历每个数字for (int num : nums) {// 从后向前更新 dp 数组for (int j = target; j >= num; j--) {dp[j] = dp[j] || dp[j - num];}}

这个代码片段中从 后往前 更新 dp 数组是为了防止在更新过程中产生错误的结果。原因与避免重复使用同一个元素有关,这是动态规划中处理“0-1 背包问题”时的常见技巧。

问题背景:

在这个问题中,每个元素只能使用一次。这与完全背包问题不同,后者允许一个元素多次使用。为了确保我们在更新 dp 数组时,使用的每个数字 num 只被处理一次,我们需要从后往前更新 dp 数组。

详细解释:

假设我们从前往后更新 dp 数组(即 for (int j = num; j <= target; j++))会导致的问题:

  • 例如,如果我们有一个数字 num = 5,当前目标和 target = 11
  • 当我们从前往后更新时,如果首先更新了 dp[5],假设原本 dp[5] = false,在这次更新中我们设置了 dp[5] = true
  • 但是,当我们接着更新 dp[10] 时,dp[10] 的更新依赖于 dp[5]。由于我们刚刚把 dp[5] 更新为了 true,此时 dp[10] 也会被更新为 true这样导致的问题是我们实际上用了两次 num = 5 来实现目标和,因为 dp[10] 是由新更新的 dp[5] 推导而来的,而这个 dp[5] 实际上是在同一轮循环中刚刚被更新过的。

因此,从前往后更新会导致我们错误地“多次使用”同一个元素。

为什么从后往前更新可以避免这个问题?

通过从后往前更新,可以确保每个元素 num 在每轮循环中只被使用一次。因为当我们更新 dp[j] 时,dp[j - num] 是基于 上一次循环的结果,而不是当前循环已经更新过的结果。具体原因如下:

  • 当我们从 targetnum 方向遍历时,dp[j] 只依赖于 dp[j - num],而 dp[j - num] 在当前循环之前还没有被更新过,因此不会重复使用同一个元素。

举例说明:

假设我们有数组 nums = [1, 5],目标和为 6,dp 数组的初始状态如下:

dp = [true, false, false, false, false, false, false]

其中 dp[0]true 表示可以通过不选任何元素实现和为 0

  • 首先处理 num = 1,从后往前更新 dp 数组:
    • 更新 dp[6],不变,因为 dp[5] 还是 false
    • 更新 dp[5],不变,因为 dp[4] 还是 false
    • 更新 dp[1] = dp[1] || dp[0] = true。更新后:
dp = [true, true, false, false, false, false, false]
  • 接下来处理 num = 5,从后往前更新 dp 数组:
    • 更新 dp[6] = dp[6] || dp[1] = true,因为 dp[1] 已经是 true(由之前的 num = 1 得到)。
    • 更新 dp[5] = dp[5] || dp[0] = true,因为 dp[0]true

最终得到:

dp = [true, true, false, false, false, true, true]

这说明我们可以找到两个子集,分别为 {1, 5},它们的和都是 6。

总结:

从后往前更新 dp 数组的目的是防止在同一次循环中重复使用同一个元素。这样确保了每个元素在一轮循环中只被考虑一次,从而正确解决问题。

为什么外层循环是遍历nums数组,而不是遍历j?

这段代码的外层循环遍历 nums 数组而不是直接遍历 j,是因为我们需要逐个处理数组中的每个数字,并根据这些数字来逐步更新 dp 数组。它遵循的算法思想是“0-1 背包问题”,每个数字只能使用一次,而不是可以无限制地使用。

具体原因和解释:

1. 动态规划的过程依赖于逐个数字的处理:
  • 这个问题的核心是寻找是否存在一个子集,其和等于目标值 target。为了实现这一点,我们必须逐个遍历数组中的每一个元素,检查当前元素 num 是否能与之前选择的数字组合成新的子集,从而更新是否能够构成特定的和。
  • 外层循环遍历 nums 数组,表示我们依次将数组中的每个元素加入到子集的选择中,然后根据这些元素更新 dp 数组,记录我们是否能够构成特定的子集和。
2. 动态规划状态的更新依赖于每个数字:
  • 对于每个数字 num,我们需要判断当前这个 num 能否帮助我们构成新的子集和,即从和为 j - num 的子集中,加上 num,能否构成和为 j 的子集。这是通过内层循环来完成的。
  • 外层循环遍历数组中的每个数字,每次拿到一个新的 num,我们就尝试更新所有可能的子集和(从 targetnum)。因此,外层循环必须遍历 nums 数组,以确保每个元素都被考虑一次。
3. 逐步构造子集和:
  • 动态规划的过程是一步步地构造所有可能的子集和,并用 dp[j] 来记录是否能构成和为 j 的子集。因此,必须逐个遍历每个 num,以确保每个数字都被正确处理,并且能基于前面的状态更新新的状态。
4. 确保每个数字只被使用一次:
  • 如果外层循环遍历 j,而不是遍历 nums 数组,我们就无法确保每个数字 num 只被使用一次。这是因为,如果直接遍历 j,那么在每一轮更新 dp 数组时,我们可能会使用同一个 num 多次。
  • 通过外层循环遍历 nums,我们保证每次只处理一个数字 num,并在内层循环中根据这个 num 更新 dp 数组。这符合“0-1 背包问题”的要求:每个元素最多只能使用一次。

举个例子:

假设我们有数组 nums = [1, 5],目标和为 6,dp 数组初始为:

dp = [true, false, false, false, false, false, false]

此时:

  • 外层循环遍历 nums,首先处理 num = 1,它会从 target 开始逐步更新 dp 数组,确保它和之前的组合是否可以构成新的子集。
  • 接下来处理 num = 5,同样从 target 开始往前更新。

通过这种方式,我们保证每个 num 只被处理一次,避免重复使用同一个元素。

总结:

  • 外层循环遍历 nums 数组,是为了逐一处理数组中的每个数字,确保每个数字 num 都能正确参与到子集和的构造过程中。
  • 通过外层循环遍历 nums 数组,可以有效地控制每个数字 num 只使用一次,避免重复选择。这符合“0-1 背包问题”的要求,即每个元素只能使用一次。

http://www.ppmy.cn/ops/126703.html

相关文章

基于ESP32的便携式游戏机

基于ESP32的便携式游戏机 一、项目说明二、项目材料三、程序测试四、设置LCD屏幕五、控制设置六、测试电路七、外壳制作八、结果 视频&#xff1a; ESP32 pro 一、项目说明 欢迎来到复古游戏的世界&#xff01;你是否曾经想要以便携格式重温童年的经典游戏&#xff1f;在这个…

Shades of Gray 算法

免责声明:本文所提供的信息和内容仅供参考。作者对本文内容的准确性、完整性、及时性或适用性不作任何明示或暗示的保证。在任何情况下,作者不对因使用本文内容而导致的任何直接或间接损失承担责任,包括但不限于数据丢失、业务中断或其他经济损失。 读者在使用本文信息时,应…

特斯拉Robotaxi发布会2024:自动驾驶未来的开端

引言 2024年10月&#xff0c;特斯拉在洛杉矶举行了一场引发全球科技界高度关注的发布会&#xff0c;主题为“We Robot”。这场发布会展示了特斯拉的最新自动驾驶技术&#xff0c;包括无人驾驶出租车Cybercab和无人驾驶厢式货车Robovan&#xff0c;并且还展示了人形机器人Optim…

智能家居安全系统和使用卷积神经网络的活性检测

论文标题&#xff1a;Smart Home Security System and Liveness Detection using Convolutional Neural Networks&#xff08;智能家居安全系统和使用卷积神经网络的活性检测&#xff09; 作者信息&#xff1a; Amith Kumar N1, Satheesh Kumar.G.R1, Sreedhar.V1, Surya.S.S…

论文笔记 ICLR 2024 MogaNet: Multi-Order Gated Aggregation Network

配图中有2个分支&#xff0c;一个是subtract的输出和缩放因子&#xff08;γs&#xff09;相乘之后的结果&#xff0c;另一个是11卷积输出的结果&#xff0c;这两个分支的输出进行element-wise addition&#xff0c;这两个分支的输出分别代表什么&#xff1f; 为什么”增强局部…

科技云报到:大模型时代下,向量数据库的野望

科技云报到原创。 自ChatGPT爆火&#xff0c;国内头部平台型公司一拥而上&#xff0c;先后发布AGI或垂类LLM&#xff0c;但鲜有大模型基础设施在数据层面的进化&#xff0c;比如向量数据库。 在此之前&#xff0c;向量数据库经历了几年的沉寂期&#xff0c;现在似乎终于乘着Ch…

Linux :at crontab简述

at命令 在指定的日期、时间点自动执行预先设置的一些命令操作&#xff0c;属于一次性计划任务系统服务的名称&#xff1a;/etc/init.d/atd存放一次性计划任务的文件&#xff1a;/var/spool/at/^a 依靠 /etc/at.allow&#xff08;白名单&#xff09;和 /etc/at.deny&#xff08…

WebSocked基础

一. WebSocket 基本概念 WebSocket是什么&#xff1f; WebSocket 是基于 TCP 的一种新的应用层网络协议。它提供了一个全双工的通道&#xff0c;允许服务器和客户端之间实时双向通信。因此&#xff0c;在 WebSocket 中&#xff0c;浏览器和服务器只需要完成一次握手&#xff…