代码问题

困惑

  1. 字符串统计字符个数的快捷方式?
    c0 = sum(1 for char in str if char=='0')

  2. 0-1背包问题dp有哪些技巧?

    一、dp数组的含义:先搞懂“我要记什么”

    dp数组的本质是“用空间换时间,记录子问题的解”。定义dp时,关键是想清楚:“我需要用什么样的子问题结果,来推导更大的问题?”

    实用技巧:用“主谓宾”造句定义dp
    • 一维dp[j]“在某种限制j下,能达到的目标值是多少”
      例:

      • 0-1背包:dp[j]表示“背包容量为j时的最大价值”;
      • 分割等和子集:dp[j]表示“能否凑出和为j的子集”。
    • 二维dp[i][j]“在两种限制ij下,能达到的目标值是多少”
      例:

      • 一和零问题:dp[i][j]表示“用不超过i个0和j个1时,最多能选多少字符串”;
      • 最长公共子序列:dp[i][j]表示“字符串s1i个字符和s2j个字符的最长公共子序列长度”。
    记忆诀窍:让dp的含义“服务于最终目标”

    最终要求什么,dp的定义就往什么方向靠。比如目标是“最大价值”,dp就定义为“某限制下的最大价值”;目标是“能否达成”,dp就定义为“某限制下能否达成”(布尔值)。

    二、初始化:从“最基本的情况”出发

    初始化的核心是回答:“当问题规模最小的时候,结果是什么?”(比如“什么都不选”“空字符串”的情况)

    常见场景与初始化逻辑
    1. “什么都不选”的情况

      • 0-1背包(最大价值):dp[0] = 0(容量0,价值0);
      • 分割等和子集(能否凑出):dp[0] = True(和为0,不选任何元素就能达成);
      • 目标和问题(方法数):dp[0] = 1(和为0,只有“什么都不选”这1种方法)。
    2. 二维dp的初始化

      • 一和零问题:dp[0][0] = 0(0个0和0个1,只能选0个字符串);
      • 最长公共子序列:dp[0][j] = 0dp[i][0] = 0(空字符串和任何字符串的公共子序列长度为0)。
    记忆诀窍:“最小子问题”法

    想象问题简化到极致(比如输入为空、容量为0),此时的结果就是初始化值。如果想不通,就用具体例子倒逼:比如“目标和问题中,和为0的方法数至少有1种(什么都不选)”,所以dp[0] = 1

    三、dp[0]的值:特殊但有规律

    dp[0]通常对应“限制为0”的子问题,其值取决于问题目标:

    问题类型 dp[0]的含义 常见值 原因
    最大价值(背包) 容量0时的最大价值 0 什么都装不下,价值为0
    能否凑出目标和 能否凑出和为0 True 不选任何元素即可达成
    方法数(目标和) 凑出和为0的方法数 1 只有“什么都不选”这1种方法
    计数(如路径数) 起点到起点的路径数 1 原地不动是1种路径
    关键提醒:dp[0]不是“默认0”,而是“逻辑起点”

    比如目标和问题中,dp[0] = 1看似特殊,但逻辑上:“不选任何元素”是一种有效的方法,必须计数。如果设为0,后续所有方法数都会少算这一种,导致结果错误。

    四、遍历范围:防重复、防越界的核心

    遍历范围的设置,本质是“保证子问题的解是‘之前计算的’,而不是‘当前更新的’”,避免逻辑错误(如0-1背包中元素被重复选择)。

    一维dp的遍历:“从大到小”是0-1背包的标志
    • 场景:每个元素只能用一次(0-1背包及变体)。
    • 遍历顺序:从目标值倒序遍历到当前元素的“重量”
      例:目标为target,当前元素重量w,则for j in range(target, w-1, -1)
    • 为什么?:倒序遍历能确保dp[j - w]是“没选当前元素时的旧值”,避免同一元素被多次使用。
    二维dp的遍历:外层元素,内层倒序
    • 场景:两个限制条件(如0和1的数量、两个字符串的长度)。
    • 遍历顺序:
      1. 外层遍历每个元素(保证每个元素只被考虑一次);
      2. 内层两个维度都从大到小遍历(如imc0jnc1)。
    • 为什么?:和一维同理,防止当前元素被重复选择,同时避免数组越界(j - w不能为负)。
    记忆诀窍:“元素在外,容量在内,倒序防重”

    0-1背包及其变体(分割等和、目标和、一和零)都遵循这个规律:先逐个处理元素(外层),再从大到小处理容量(内层),核心是“不让当前元素干扰之前的子问题”。

    五、状态转移方程:“选或不选”的二选一逻辑

    状态转移是DP的“灵魂”,本质是“用子问题的解推导当前问题的解”,核心逻辑是“选当前元素”和“不选当前元素”的权衡。

    通用模板:两种选择取最优
    • 求最大值/数量(如最大价值、最多字符串数):
      dp[j] = max(不选当前元素的解, 选当前元素的解 + 当前元素的贡献)
      例:0-1背包 dp[j] = max(dp[j], dp[j - w] + v)

    • 求能否达成(如能否凑出目标和):
      dp[j] = 不选当前元素能否达成 OR 选当前元素能否达成
      例:分割等和子集 dp[j] = dp[j] or dp[j - num]

    • 求方法数(如目标和的方法数):
      dp[j] = 不选当前元素的方法数 + 选当前元素的方法数
      例:目标和 dp[j] += dp[j - num]

    记忆诀窍:先想“选了会怎样”,再结合“不选的情况”

    1. 假设选当前元素:需要什么子问题的解?(比如j - w的解)
    2. 不选当前元素:直接用之前的解(dp[j]的旧值)
    3. 用逻辑运算符(max/or/+)将两者结合,得到当前问题的解。
    六、总结:动态规划“五字诀”

    最后用五个字帮你串联所有要点,遇到问题时按这个顺序推导,很快就能理清思路:

    1. “定”:定义dp的含义(主谓宾造句,服务于目标);
    2. “初”:初始化dp(从最小子问题出发,如“什么都不选”);
    3. “边”:确定遍历边界(倒序防重复,范围防越界);
    4. “转”:写状态转移方程(选或不选二选一,用合适的逻辑结合);
    5. “答”:确定最终答案在dp的哪个位置(通常是dp[目标值])。

PLUS