Skip to content

algo11 动态规划(上)— exercise.py 练习指南

Download exercise.py

练习目标

通过四个 DP 变体的实现,巩固 DP 五步设计法:状态定义 → 转移方程 → 初始条件 → 计算顺序 → 提取答案。

预备知识

  • 0-1 背包的两种实现(Top-Down/Bottom-Up)
  • 子集和、LCS、编辑距离的 DP 模板
  • 0-1 背包空间优化中 j 逆序的关键原因

任务清单

任务1:完全背包 unbounded_knapsack(weights, values, capacity)

  • 与 0-1 背包的唯一区别:j 的遍历方向从逆序变为正序
  • 原因:正序遍历时,dp[j - w] 可能已经被当前物品更新过,相当于允许同一物品被多次使用——这正是完全背包的语义。
  • 回溯:用二维 DP 表 dp2d[i][j] 记录完整状态,从 dp2d[n][capacity] 回溯每种物品的使用数量。

任务2:不同子序列计数 num_distinct_subsequences(s, t)

  • 状态dp[i][j] = s[:i] 中 t[:j] 作为子序列出现的不同方式数
  • 转移
    • s[i-1] == t[j-1]dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
      • dp[i-1][j-1]:使用 s[i-1] 匹配 t[j-1]
      • dp[i-1][j]:不使用 s[i-1] 但仍然匹配 t[:j]
    • 否则 → dp[i][j] = dp[i-1][j](只能跳过 s[i-1])
  • 初始化dp[i][0] = 1(空串在任何字符串中出现恰好 1 次——什么都不选)

任务3:最短公共超序列 shortest_common_supersequence(s1, s2)

  • 关键公式SCS长度=len(s1)+len(s2)LCS长度
  • 构建 SCS 字符串:在 LCS 回溯的基础上,当字符不匹配时,需要同时保留两个字符串的字符(因为 SCS 必须包含两者)。
  • 示例:"abac" + "cab" → LCS="ab" → SCS="cabac"

任务4:分割等和子集 can_partition(nums)

  • 归约为子集和问题target=sum(nums)/2,能否找到和为 target 的子集?
  • 空间优化:一维布尔 DP 数组,逆向遍历。

提示

  1. 完全背包的正序遍历容易理解:想象你有一个无限的物品仓库,每次考虑一个物品时,你可以多次取用,所以 j 从小到大更新。
  2. 子序列计数注意边界:空串的情况。dp[i][0] 总是 1。
  3. SCS 字符串构建的关键是:每一步不仅要看 LCS 匹配,还要确保所有字符都被包含。
py
# -*- coding: utf-8 -*-
"""
algo11 动态规划(上)— 练习代码
===============================
请完成以下 TODO 任务,巩固对 DP 五步设计法的理解。
"""


# ============================================================================
# TODO 1: 完全背包问题(Unbounded Knapsack)
# ============================================================================
def unbounded_knapsack(weights, values, capacity):
    """
    完全背包:每种物品无限量。
    与 0-1 背包的区别:j 的遍历方向从逆序变为正序!

    状态定义:dp[j] = 容量为 j 时的最大价值
    转移方程:dp[j] = max(dp[j], dp[j - w_i] + v_i)

    参数:
        weights: list,各物品重量
        values: list,各物品价值
        capacity: 背包容量
    返回:
        最大总价值,以及每种物品的使用数量
    """
    # TODO: 补全以下代码
    n = len(weights)
    dp = [0] * (capacity + 1)

    # TODO: 外层遍历物品,内层遍历容量(正序!)
    # for i in range(n):
    #     w, v = weights[i], values[i]
    #     for j in range(w, capacity + 1):  ← 注意:正序!
    #         dp[j] = max(dp[j], dp[j - w] + v)

    # TODO: 回溯每种物品使用的数量
    # 提示:使用二维 dp 表 dp2d[i][j] 来记录"考虑前 i 件物品"的状态
    # 然后从 dp2d[n][capacity] 回溯

    # 临时返回
    return 0, {}  # TODO: 替换为正确返回值


# ============================================================================
# TODO 2: 不同子序列计数(Distinct Subsequences)
# ============================================================================
def num_distinct_subsequences(s, t):
    """
    计算字符串 t 在字符串 s 中作为子序列出现的不同方式数。

    dp[i][j] = s[:i] 中 t[:j] 出现的次数
    转移: s[i-1]==t[j-1] → dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
          否则 → dp[i][j] = dp[i-1][j]

    例: s="rabbbit", t="rabbit" → 3

    参数:
        s: 源字符串
        t: 目标子序列
    返回:
        不同子序列的数量
    """
    # TODO: 补全实现
    m, n = len(s), len(t)
    # dp = [[0] * (n + 1) for _ in range(m + 1)]
    # 初始化: dp[i][0] = 1 (空串在任何字符串中出现 1 次)
    # dp[0][j] = 0 for j > 0

    # 填表
    # for i in range(1, m + 1):
    #     for j in range(1, n + 1):
    #         if s[i-1] == t[j-1]:
    #             dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
    #         else:
    #             dp[i][j] = dp[i-1][j]
    # return dp[m][n]
    return 0  # TODO


# ============================================================================
# TODO 3: 最短公共超序列(Shortest Common Supersequence)
# ============================================================================
def shortest_common_supersequence(s1, s2):
    """
    最短公共超序列:包含 s1 和 s2 作为子序列的最短字符串。
    利用 LCS:SCS 长度 = len(s1) + len(s2) - LCS 长度

    参数:
        s1, s2: 两个输入字符串
    返回:
        scs 字符串
    """
    # TODO: 先计算 LCS 表
    # m, n = len(s1), len(s2)
    # dp = [[0] * (n + 1) for _ in range(m + 1)]
    # ... (LCS 填表)

    # TODO: 从 dp 表回溯构建 SCS
    # i, j = m, n
    # scs = []
    # while i > 0 and j > 0:
    #     if s1[i-1] == s2[j-1]:
    #         scs.append(s1[i-1])
    #         i -= 1; j -= 1
    #     elif dp[i-1][j] >= dp[i][j-1]:
    #         scs.append(s1[i-1])  # 保留了 s1 但没保留 s2 → 加入 s1
    #         i -= 1
    #     else:
    #         scs.append(s2[j-1])
    #         j -= 1
    # # 追加剩余字符
    # while i > 0: scs.append(s1[i-1]); i -= 1
    # while j > 0: scs.append(s2[j-1]); j -= 1
    # return ''.join(reversed(scs))
    return ''  # TODO


# ============================================================================
# TODO 4: 分割等和子集(Partition Equal Subset Sum)
# ============================================================================
def can_partition(nums):
    """
    判断能否将数组分成两个和相等的子集。
    等价于:是否存在子集,其和为 sum(nums)/2。
    所以:target = sum(nums)/2,调用子集和 DP。

    参数:
        nums: list of int,整数数组
    返回:
        (can_partition, subset1, subset2)
    """
    # TODO: 实现
    total = sum(nums)
    # 如果总和为奇数,不可能等分
    # if total % 2 != 0: return False, [], []
    # target = total // 2
    # 使用一维布尔 DP(空间优化版)
    # dp = [False] * (target + 1)
    # dp[0] = True
    # for num in nums:
    #     for j in range(target, num - 1, -1):  # 逆序!
    #         dp[j] = dp[j] or dp[j - num]
    # 回溯分拆两个子集...
    return False, [], []  # TODO


# ============================================================================
# 测试代码
# ============================================================================
if __name__ == '__main__':
    print('=' * 50)
    print('algo11 动态规划(上)— 练习')
    print('=' * 50)

    # 测试 TODO 1: 完全背包
    print('\n--- TODO 1: 完全背包 ---')
    w, v = [2, 3, 4], [4, 5, 6]
    cap = 8
    max_val, counts = unbounded_knapsack(w, v, cap)
    print(f'  物品 (w={w}, v={v}), 容量={cap}')
    print(f'  最大价值={max_val}, 使用数量={counts}')

    # 测试 TODO 2: 不同子序列
    print('\n--- TODO 2: 不同子序列计数 ---')
    print(f'  s="rabbbit", t="rabbit" → {num_distinct_subsequences("rabbbit", "rabbit")} (期望: 3)')
    print(f'  s="babgbag", t="bag" → {num_distinct_subsequences("babgbag", "bag")} (期望: 5)')

    # 测试 TODO 3: 最短公共超序列
    print('\n--- TODO 3: 最短公共超序列 ---')
    print(f'  "abac", "cab" → SCS = "{shortest_common_supersequence("abac", "cab")}"')

    # 测试 TODO 4: 分割等和子集
    print('\n--- TODO 4: 分割等和子集 ---')
    for test in [[1, 5, 11, 5], [1, 2, 3, 5], [2, 2, 3, 5]]:
        ok, s1, s2 = can_partition(test)
        print(f'  {test}{ok}', end='')
        if ok:
            print(f'  ({s1} | {s2})')
        else:
            print()

    print('\n提示: 请补全上方所有 TODO 函数后再运行测试。')