【算法·MEGA-15】动态规划-计数类DP

计数类动态规划(Counting DP) 是动态规划中的一种应用,通常用于计算满足某些条件的组合数量或特定状态的数量,而不是具体的数值大小。计数类 DP 问题的核心思想是通过递推计算可能的组合、排列或状态的数量,并通过状态转移方程累加这些数量。

计数类 DP 问题通常用于以下类型的场景:

  1. 组合问题:求某个特定条件下的组合数量。
  2. 排列问题:求满足特定条件的排列数量。
  3. 子序列和子集问题:例如求解给定数组中满足某种条件的子序列或子集的数量。
  4. 路径计数问题:例如在一个网格中求从起点到终点的路径数量。
  5. 背包问题的计数:例如求解在背包问题中,使用多少种方式能将物品放入背包等。

1. 计数类 DP 的基本思想

计数类 DP 的基本思想是将问题分解为子问题,通过递推得到总的计数。与普通的 DP 问题不同,计数类 DP 关心的是 状态的数量,而不是最优解的值。通过累加每个子问题的解,我们可以得到最终的结果。

2. 典型问题

2.1 背包问题的计数

在经典的 0-1 背包问题中,我们通常求解最大价值。而在 计数类 DP 变种中,我们可能要计算有多少种不同的方式可以将物品装入背包,满足背包的总重量不超过给定的容量。

问题:给定若干物品,每个物品有一个重量和价值,求解将物品放入背包的不同方案数,且背包总重量不超过给定的容量 WW。

状态定义

  • dp[w]dp[w] 表示背包容量为 ww 时的方案数。

状态转移

  • 对于每个物品 ii,可以选择不放入背包,或者放入背包。对于放入背包的情况,更新方案数。

状态转移方程

dp[w]=dp[w]+dp[w−weighti](如果 w≥weighti)dp[w] = dp[w] + dp[w - weight_i] \quad \text{(如果} \, w \geq weight_i\text{)}

代码实现

def knapsack_count(weights, W):
    dp = [0] * (W + 1)
    dp[0] = 1  # 背包容量为0时,有1种选择方式(不放任何物品)

    for weight in weights:
        for w in range(W, weight - 1, -1):  # 从大到小遍历
            dp[w] += dp[w - weight]
    
    return dp[W]

解释

  • dp[w] 表示容量为 ww 时的方案数。
  • 初始化时,dp[0] = 1,因为总重量为0时只有一种选择方式,即不选任何物品。
  • 对于每个物品,尝试将其放入背包(从大到小遍历,避免重复计算同一个物品)。
2.2 不同路径问题(Dynamic Programming on Grid)

问题:在一个 m×nm \times n 的网格中,从左上角到右下角的路径数量。每次只能向下或向右移动。

状态定义

  • dp[i][j]dp[i][j] 表示从网格的左上角到达位置 (i,j)(i, j) 的路径数量。

状态转移

  • 每个位置的路径数可以由其左边或上边的路径数递推而来,即:

dp[i][j]=dp[i−1][j]+dp[i][j−1]dp[i][j] = dp[i-1][j] + dp[i][j-1]

代码实现

def uniquePaths(m, n):
    dp = [[0] * n for _ in range(m)]
    dp[0][0] = 1  # 起点有1条路径

    for i in range(m):
        for j in range(n):
            if i > 0:
                dp[i][j] += dp[i-1][j]  # 从上方来的路径
            if j > 0:
                dp[i][j] += dp[i][j-1]  # 从左边来的路径
    
    return dp[m-1][n-1]

解释

  • 每个位置的路径数由其左边和上边的路径数相加。
  • 初始状态是 dp[0][0] = 1,表示从起点开始有1条路径。
2.3 子序列计数问题

问题:给定一个字符串,求其不同的子序列数量。子序列是由原字符串中按顺序选取的字符组成的,不必连续。

状态定义

  • dp[i]dp[i] 表示从字符串的前 ii 个字符构成的子序列的数量。

状态转移

  • 对于每个字符,我们可以选择包含它或不包含它。因此,状态转移方程是:

dp[i]=2×dp[i−1]dp[i] = 2 \times dp[i-1]

(乘以2是因为每个字符都可以选择是否加入子序列)

但如果允许重复字符,我们需要去除重复计算。

代码实现

def countDistinctSubsequences(s):
    dp = [0] * (len(s) + 1)
    dp[0] = 1  # 空字符串有1种子序列(空子序列)

    last_seen = {}  # 用于记录每个字符最后一次出现的位置
    for i in range(1, len(s) + 1):
        dp[i] = 2 * dp[i - 1]  # 每个字符可以选择是否加入
        if s[i - 1] in last_seen:
            dp[i] -= dp[last_seen[s[i - 1]] - 1]  # 去除重复子序列
        
        last_seen[s[i - 1]] = i  # 更新字符最后出现的位置

    return dp[len(s)] - 1  # 不考虑空子序列

解释

  • dp[i] 表示包含字符串 s[0...i-1] 的子序列的数量。
  • 每添加一个字符,都可以选择是否包含它,因此是乘以2。
  • 使用 last_seen 字典来避免重复计算相同字符的子序列。
2.4 组合问题

问题:求从 nn 个物品中选取 kk 个物品的组合数。

状态定义

  • dp[i][j]dp[i][j] 表示从 ii 个物品中选择 jj 个物品的方案数。

状态转移

  • 可以选择不选择当前物品或选择当前物品。因此:

dp[i][j]=dp[i−1][j]+dp[i−1][j−1]dp[i][j] = dp[i-1][j] + dp[i-1][j-1]

其中 dp[i−1][j]dp[i-1][j] 表示不选当前物品,dp[i−1][j−1]dp[i-1][j-1] 表示选当前物品。

代码实现

def combination(n, k):
    dp = [[0] * (k + 1) for _ in range(n + 1)]
    dp[0][0] = 1  # 从0个物品中选择0个物品只有1种方式
    
    for i in range(1, n + 1):
        for j in range(0, k + 1):
            dp[i][j] = dp[i-1][j]  # 不选第i个物品
            if j > 0:
                dp[i][j] += dp[i-1][j-1]  # 选第i个物品
    
    return dp[n][k]

解释

  • dp[i][j] 表示从前 ii 个物品中选择 jj 个物品的组合数。
  • 使用状态转移方程递推。

3. 总结

计数类动态规划的核心是通过递推计算出满足某些条件的组合或排列的数量。这类问题常常用来解决涉及选择、路径、子序列、组合等问题。在这些问题中,动态规划通过状态转移方程累加不同的方案数量,从而得到最终的结果。计数类 DP 既可以用于简单的计数问题,也可以用于复杂的组合优化问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值