当前位置: 首页 > ops >正文

【LeetCode 热题 100】416. 分割等和子集——(解法一)记忆化搜索

Problem: 416. 分割等和子集

文章目录

  • 整体思路
  • 完整代码
  • 时空复杂度
    • 时间复杂度:O(N * target) 或 O(N * sum)
    • 空间复杂度:O(N * target) 或 O(N * sum)

整体思路

这段代码旨在解决经典的 “分割等和子集” (Partition Equal Subset Sum) 问题。问题要求判断一个给定的、只包含正整数的数组 nums 是否可以被分割成两个子集,使得这两个子集的元素和相等。

该算法将原问题巧妙地转化为一个 0/1 背包问题,并采用 自顶向下(Top-Down)的动态规划,即 记忆化搜索 (Memoization) 来求解。

  1. 问题转化:0/1 背包模型

    • 核心洞察:如果一个数组可以被分割成两个和相等的子集,那么每个子集的和必然等于数组总和的一半
    • 初步检查:因此,算法首先计算数组的总和 sum。如果 sum 是奇数,那么它不可能被平分成两个整数和,可以直接返回 false
    • 新问题:如果 sum 是偶数,问题就转化为:能否从 nums 数组中挑选出若干个数字,使得它们的和恰好等于 target = sum / 2 如果能找到这样一个子集,那么剩下的数字自然也构成一个和为 target 的子集。
    • 背包类比
      • 背包容量target
      • 物品nums 数组中的每一个数字。
      • 物品重量nums[i] 的值。
      • 目标:能否恰好装满背包(即子集和等于target)。
      • “0/1”:每个数字(物品)只有两种选择:选入子集或不选入子集,不能重复使用。
  2. 状态定义与递归关系

    • 算法的核心是递归函数 dfs(i, target),其状态定义为:
      dfs(i, target) = 在只考虑前 i+1 个数字(从 nums[0]nums[i])的情况下,是否能凑出和为 target
    • 对于当前考虑的数字 nums[i],我们有两个选择:
      a. 不选 nums[i]:如果我们不把它放入子集,问题就变成了“用前 i 个数字(nums[0]nums[i-1])能否凑出 target”,这对应于递归调用 dfs(i - 1, target, ...)
      b. nums[i]:如果我们把它放入子集(前提是 target >= nums[i]),问题就变成了“用前 i 个数字能否凑出 target - nums[i]”,这对应于 dfs(i - 1, target - nums[i], ...)
    • 只要这两种选择中有任何一种能成功(返回true),dfs(i, target) 的结果就是 true
  3. 记忆化与基础情况

    • 记忆化:使用二维数组 memo 存储子问题的解。memo[i][target] 记录 dfs(i, target) 的结果(用1表示true,0表示false),避免重复计算。
    • 基础情况if (i < 0) 是递归的出口。当没有数字可选时,只有当 target 恰好也为0时,才算成功。

完整代码

import java.util.Arrays;class Solution {/*** 判断数组是否可以被分割成两个和相等的子集。* @param nums 只包含正整数的数组* @return 如果可以分割则返回 true,否则返回 false*/public boolean canPartition(int[] nums) {// 1. 计算数组总和int sum = 0;for (int num : nums) {sum += num;}// 2. 如果总和为奇数,不可能平分,直接返回 falseif (sum % 2 == 1) {return false;}int n = nums.length;// 3. 确定目标和(背包容量)int target = sum / 2;// 4. 初始化记忆化数组// memo[i][j] 存储用前 i+1 个数能否凑成 j 的结果// -1: 未计算, 0: false, 1: trueint[][] memo = new int[n][target + 1];for (int[] row : memo) {Arrays.fill(row, -1);}// 5. 从最后一个元素开始,启动递归求解return dfs(n - 1, target, nums, memo);}/*** 记忆化搜索函数* @param i      当前考虑的数字的索引* @param target 当前需要凑成的目标和* @param nums   原始数组* @param memo   记忆化数组* @return 是否能凑成 target*/private boolean dfs(int i, int target, int[] nums, int[][] memo) {// 基础情况:如果没有数字可选了if (i < 0) {// 如果此时 target 恰好为 0,说明找到了一组解;否则失败。return target == 0;}// 记忆化检查:如果该子问题已计算过,直接返回结果。if (memo[i][target] != -1) {return memo[i][target] == 1;}// 状态转移:// 结果 res 为 true 的条件是以下两者之一为 true:// 1. (target >= nums[i] && dfs(i-1, ...)): 选择 nums[i] 的情况,前提是 target 足够大,//    并且剩余的数字能凑成 target - nums[i]。// 2. dfs(i-1, ...): 不选择 nums[i] 的情况,剩余的数字需要凑成完整的 target。boolean res = (target >= nums[i] && dfs(i - 1, target - nums[i], nums, memo)) || dfs(i - 1, target, nums, memo);// 将计算结果存入 memo 数组(1 for true, 0 for false)memo[i][target] = res ? 1 : 0;return res;}
}

时空复杂度

时间复杂度:O(N * target) 或 O(N * sum)

  • N 是数组 nums 的长度。
  • target 是数组总和的一半。
  1. 状态数量:由于记忆化的存在,每个状态 (i, target) 只会被实际计算一次。
    • i 的范围是从 0N-1(共 N 种)。
    • target 的范围是从 0sum/2(共 target + 1 种)。
    • 因此,总的状态数量是 N * (target + 1)
  2. 每个状态的计算时间:在 dfs 函数内部,除了递归调用,其他的操作都是 O(1) 的。

综合分析
总的时间复杂度等于(状态数量)×(每个状态的计算时间),即 O(N * target)。由于 targetsum 是线性关系,也可以写作 O(N * sum)

空间复杂度:O(N * target) 或 O(N * sum)

  1. 记忆化数组:算法创建了一个 memo 二维数组来存储所有子问题的解。其大小为 N * (target + 1)。这部分空间占用为 O(N * target)
  2. 递归调用栈:递归的深度也需要考虑。在最坏的情况下,例如 dfs(n-1, target) 一路调用 dfs(n-2, ...) 直到 dfs(-1, ...),递归深度可达 N。因此,递归栈所占用的空间是 O(N)
  3. 综合分析
    • 记忆化数组的空间开销是 O(N * target)。
    • 递归栈的空间开销是 O(N)。
    • 由于 target 通常远大于1,N * target 是主导项。因此最终的空间复杂度为 O(N * target)

参考灵神

http://www.xdnf.cn/news/18657.html

相关文章:

  • ansible的搭建与安装
  • 在数字化转型过程中,如何确保数据安全和隐私保护?
  • Linux 软件编程(十一)网络编程:TCP 机制与 HTTP 协议
  • 我的项目管理之路-组织级项目管理(二)
  • 【spring进阶】spring应用内方法调用时长统计
  • 【C语言强化训练16天】--从基础到进阶的蜕变之旅:Day13
  • Python之matplotlib 基础三:绘制折线图
  • 什么是JSON-RPC 2.0,在项目中应该怎么使用
  • Jenkins+docker 微服务实现自动化部署安装和部署过程
  • More Effective C++ 条款08:理解各种不同意义的new和delete
  • (操作系统)死锁是什么 必要条件 解决方式
  • 【Task05】:向量数据库实践(第三章3、4节)
  • Fory序列化与反序列化
  • ArcGIS JSAPI 高级教程 - 创建渐变色材质的自定义几何体
  • MYSQL(DDL)
  • 算法:驱动智能社会的核心引擎
  • OpenIM应用机器人自动应答
  • Zabbix 7.0中文乱码矫正
  • AI产品经理面试宝典第75天:Agentic RAG系统优化策略面试题实战解析
  • 08-系统能力调用与权限管理
  • BERT(Bidirectional Encoder Representations from Transformers)模型详解
  • 【RAGFlow代码详解-1】概述
  • 【Android】从一个AndroidRuntime看类的加载
  • 结构化智能编程:用树形向量存储重构AI代码理解范式
  • 第16届蓝桥杯C++中高级选拔赛(STEMA)2025年4月真题
  • More Effective C++ 条款05: 谨慎定义类型转换函数
  • 【Flex SerialPort】一个基于Qt6的支持自定义按键指令的串口工具
  • Kubernetes保姆级教学
  • centos搭建gitlab服务器
  • 【贪心算法】day2