“粉刷栅栏”问题的经典变种
字数 3202 2025-12-10 23:29:47

好的,我注意到你的列表中没有关于**“粉刷栅栏”问题的经典变种**。那么,我们来讲解一个与它密切相关,但约束条件略有变化且更具一般性的线性动态规划问题。


线性动态规划:粉刷栅栏的进阶版——最多允许k个相邻栅栏同色的总方案数

问题描述

你有一段长度为 n 的栅栏,需要为每一段栅栏柱子涂上颜色。总共有 m 种不同的颜色可供选择。给你一个整数 k (k >= 2),要求任意连续 k 段栅栏中,不能全部是同一种颜色

换句话说,你涂色的方案中,不允许出现长度恰好等于或大于 k 的连续同色栅栏段。

题目要求:计算给 n 段栅栏涂色的总方案数。由于结果可能很大,请将结果对 10^9 + 7 取模。

示例:

  • 输入: n = 3, m = 2, k = 3
  • 解释: 有3段栅栏,2种颜色(比如红、蓝),不允许连续3段同色。
  • 所有涂色方案:
    1. 红红蓝
    2. 红蓝红
    3. 红蓝蓝
    4. 蓝红红
    5. 蓝红蓝
    6. 蓝蓝红
  • 输出: 6 (注意:方案“红红红”和“蓝蓝蓝”因连续3段同色而被禁止)。

问题分析与思路构建

这是一个典型的计数类动态规划问题。关键限制是“最长连续同色段”的长度不能达到 k

1. 状态定义
我们不能只定义 dp[i] 为前 i 段栅栏的方案数,因为当前的选择依赖于最后一段的“连续同色长度”。
因此,我们需要用两个维度来精确描述状态:

  • i: 已经涂完的前 i 段栅栏。
  • j: 以第 i 段栅栏结尾的连续同色段的长度。显然,1 <= j < k(因为长度一旦达到 k 就是非法状态,我们根本不会去计算它)。

我们定义:
dp[i][j] = 涂完前 i 段栅栏,且最后一段(第 i 段)所属的连续同色段长度为 j合法方案总数

2. 状态转移方程
我们考虑如何从 dp[i-1][*] 转移到 dp[i][j]

  • 情况A:第 i 段颜色与第 i-1 段颜色不同。

    • 这种情况下,无论 i-1 段之前连续同色长度是多少,到了 i 段,由于颜色变了,连续同色长度会“重置”为 1
    • 对于 dp[i][1],它可以由所有合法的 dp[i-1][*] 状态转移而来,转移的方式是:为第 i 段选择一种与第 i-1不同的颜色。
    • 有多少种选择?总共有 m 种颜色,其中一种被第 i-1 段用掉了,所以有 (m - 1) 种选择。
    • 因此转移方程为:
      dp[i][1] += dp[i-1][j] * (m - 1), 对所有 1 <= j < k 求和。
  • 情况B:第 i 段颜色与第 i-1 段颜色相同。

    • 这种情况下,第 i 段的连续同色长度 j 等于第 i-1 段的连续同色长度 j-11
    • 前提是 j 必须大于 1(因为要和前一段相同),并且 j 必须小于 k(否则非法)。
    • 转移时,第 i 段的颜色选择是唯一的(必须和第 i-1 段相同)。
    • 因此转移方程为:
      dp[i][j] = dp[i-1][j-1], 对于所有 2 <= j < k

3. 边界条件 (初始状态)

  • 对于第一段栅栏 (i=1):
    • 我们可以涂任何颜色。
    • 涂完后,以第一段结尾的连续同色段长度就是 1
    • 所以 dp[1][1] = m (m种颜色,每种都产生一个j=1的状态)。
    • 对于 j>1dp[1][j] = 0

4. 最终答案
我们需要求出所有涂完 n 段栅栏 (i = n) 的合法方案。这些方案分布在 dp[n][j] 中,其中 j1k-1
所以:
answer = sum(dp[n][j]) for j = 1 to k-1

5. 复杂度分析

  • 状态数量:O(n * k)
  • 每个状态转移:dp[i][1] 需要 O(k) 时间求和(但可以通过维护前缀和优化到 O(1)),dp[i][j] (j>1) 的转移是 O(1)
  • 总时间复杂度:朴素实现为 O(n * k),优化后为 O(n)
  • 空间复杂度:O(n * k),可以滚动数组优化到 O(k)

循序渐进的计算过程(以示例 n=3, m=2, k=3 为例)

  1. 初始化:

    • dp[1][1] = m = 2 (方案:段1红,段1蓝)。
    • dp[1][2] = 0 (因为只有一段,不可能连续长度为2)。
  2. 计算 i=2:

    • 计算 dp[2][1]: 第2段与第1段颜色不同。
      • 来源:dp[1][1] = 2
      • 不同颜色选择:m-1 = 1 (假设第1段是红,第2段只能蓝;反之亦然)。
      • 所以 dp[2][1] = dp[1][1] * 1 = 2 * 1 = 2
      • 对应方案:[红, 蓝] 和 [蓝, 红]。
    • 计算 dp[2][2]: 第2段与第1段颜色相同。
      • 来源:dp[1][1] = 2 (因为 j=2j-1=1 转移来)。
      • 相同颜色没有选择,直接继承。
      • 所以 dp[2][2] = dp[1][1] = 2
      • 对应方案:[红, 红] 和 [蓝, 蓝]。
    • 状态表(i=2):
      dp[2][1] = 2, dp[2][2] = 2
  3. 计算 i=3:

    • 计算 dp[3][1]: 第3段与第2段颜色不同。
      • 来源:dp[2][1] + dp[2][2] = 2 + 2 = 4
      • 不同颜色选择:m-1 = 1
      • 所以 dp[3][1] = 4 * 1 = 4
      • 方案:在i=2的所有4种方案后,加上一个与第2段不同的颜色。
        例如 [红,蓝] 后加 -> [红,蓝,红]
        具体是:[红,蓝,红], [蓝,红,蓝], [红,红,蓝], [蓝,蓝,红]
    • 计算 dp[3][2]: 第3段与第2段颜色相同,且连续长度j=2
      • 来源:dp[2][1] = 2
      • dp[3][2] = dp[2][1] = 2
      • 方案:从[红,蓝]变成[红,蓝,蓝],从[蓝,红]变成[蓝,红,红]
        注意:从[红,红]不能转移到dp[3][2]吗?可以,但那会导致连续长度j=3[红,红,红]),而k=3,这是非法的。我们的状态j限制在1k-1,所以根本不会计算dp[3][3],因此在i=3时,dp[2][2]不能作为dp[3][2]的来源,因为jj-1=2来,而j=3是非法的。在我们的转移方程中,只有dp[i-1][j-1]j<k时才成立。当j=2j-1=1,来源是dp[i-1][1],完全正确。
        为了更清晰:dp[3][2] = dp[2][1] = 2
    • 状态表(i=3):
      dp[3][1] = 4, dp[3][2] = 2
  4. 计算最终答案:

    • answer = dp[3][1] + dp[3][2] = 4 + 2 = 6
    • 与手动列举的结果一致。

优化与代码实现思路(Python伪代码)

我们可以用滚动数组将空间优化到 O(k)

def numWays(n: int, m: int, k: int) -> int:
    MOD = 10**9 + 7
    if k == 1: # 不允许任何相邻同色,退化为基础问题
        return m * pow(m-1, n-1, MOD) % MOD if n > 1 else m
    if n == 0:
        return 0
    if n == 1:
        return m

    # dp[j] 表示“当前段”结尾连续同色长度为j的方案数
    dp = [0] * k
    # 初始化 i=1
    dp[0] = m # 这里我们让索引从0开始,dp[0]代表j=1,dp[k-2]代表j=k-1

    for i in range(2, n+1):
        new_dp = [0] * k
        # 情况B:与上一段颜色相同 (j > 1)
        for j in range(1, k-1): # j的范围是2到k-1,对应索引是1到k-2
            new_dp[j] = dp[j-1] % MOD
        # 情况A:与上一段颜色不同 (j = 1)
        total_prev = sum(dp) % MOD # 上一轮所有合法状态的和
        new_dp[0] = total_prev * (m - 1) % MOD
        dp = new_dp

    return sum(dp) % MOD

思考延伸
这个模型是线性DP中处理“连续元素限制”的经典范式。通过增加一个维度(结尾连续长度j)来记录与当前决策相关的历史信息,从而能够判断新加入元素是否会违反约束。这种方法可以推广到很多类似场景,例如:

  • 最多允许连续 k 个1的二进制字符串计数。
  • 字符串中不能出现连续 k 个相同字符的排列计数问题。
好的,我注意到你的列表中没有关于** “粉刷栅栏”问题的经典变种** 。那么,我们来讲解一个与它密切相关,但约束条件略有变化且更具一般性的线性动态规划问题。 线性动态规划:粉刷栅栏的进阶版——最多允许 k 个相邻栅栏同色的总方案数 问题描述 你有一段长度为 n 的栅栏,需要为每一段栅栏柱子涂上颜色。总共有 m 种不同的颜色可供选择。给你一个整数 k ( k >= 2 ),要求 任意连续 k 段栅栏中,不能全部是同一种颜色 。 换句话说,你涂色的方案中,不允许出现长度 恰好等于或大于 k 的连续同色栅栏段。 题目要求:计算给 n 段栅栏涂色的 总方案数 。由于结果可能很大,请将结果对 10^9 + 7 取模。 示例: 输入: n = 3 , m = 2 , k = 3 解释: 有3段栅栏,2种颜色(比如红、蓝),不允许连续3段同色。 所有涂色方案: 红红蓝 红蓝红 红蓝蓝 蓝红红 蓝红蓝 蓝蓝红 输出: 6 (注意:方案“红红红”和“蓝蓝蓝”因连续3段同色而被禁止)。 问题分析与思路构建 这是一个典型的计数类动态规划问题。关键限制是“最长连续同色段”的长度不能达到 k 。 1. 状态定义 我们不能只定义 dp[i] 为前 i 段栅栏的方案数,因为当前的选择依赖于最后一段的“连续同色长度”。 因此,我们需要用 两个维度 来精确描述状态: i : 已经涂完的前 i 段栅栏。 j : 以第 i 段栅栏结尾的连续同色段的长度 。显然, 1 <= j < k (因为长度一旦达到 k 就是非法状态,我们根本不会去计算它)。 我们定义: dp[i][j] = 涂完前 i 段栅栏,且最后一段(第 i 段)所属的连续同色段 长度为 j 的 合法方案总数 。 2. 状态转移方程 我们考虑如何从 dp[i-1][*] 转移到 dp[i][j] 。 情况A:第 i 段颜色与第 i-1 段颜色不同。 这种情况下,无论 i-1 段之前连续同色长度是多少,到了 i 段,由于颜色变了,连续同色长度会“重置”为 1 。 对于 dp[i][1] ,它可以由 所有 合法的 dp[i-1][*] 状态转移而来,转移的方式是:为第 i 段选择一种与第 i-1 段 不同 的颜色。 有多少种选择?总共有 m 种颜色,其中一种被第 i-1 段用掉了,所以有 (m - 1) 种选择。 因此转移方程为: dp[i][1] += dp[i-1][j] * (m - 1) , 对所有 1 <= j < k 求和。 情况B:第 i 段颜色与第 i-1 段颜色相同。 这种情况下,第 i 段的连续同色长度 j 等于第 i-1 段的连续同色长度 j-1 加 1 。 前提是 j 必须大于 1 (因为要和前一段相同),并且 j 必须小于 k (否则非法)。 转移时,第 i 段的颜色选择是唯一的(必须和第 i-1 段相同)。 因此转移方程为: dp[i][j] = dp[i-1][j-1] , 对于所有 2 <= j < k 。 3. 边界条件 (初始状态) 对于第一段栅栏 ( i=1 ): 我们可以涂任何颜色。 涂完后,以第一段结尾的连续同色段长度就是 1 。 所以 dp[1][1] = m (m种颜色,每种都产生一个 j=1 的状态)。 对于 j>1 , dp[1][j] = 0 。 4. 最终答案 我们需要求出所有涂完 n 段栅栏 ( i = n ) 的合法方案。这些方案分布在 dp[n][j] 中,其中 j 从 1 到 k-1 。 所以: answer = sum(dp[n][j]) for j = 1 to k-1 。 5. 复杂度分析 状态数量: O(n * k) 。 每个状态转移: dp[i][1] 需要 O(k) 时间求和(但可以通过维护前缀和优化到 O(1) ), dp[i][j] (j>1) 的转移是 O(1) 。 总时间复杂度:朴素实现为 O(n * k) ,优化后为 O(n) 。 空间复杂度: O(n * k) ,可以滚动数组优化到 O(k) 。 循序渐进的计算过程(以示例 n=3, m=2, k=3 为例) 初始化 : dp[1][1] = m = 2 (方案:段1红,段1蓝)。 dp[1][2] = 0 (因为只有一段,不可能连续长度为2)。 计算 i=2 : 计算 dp[2][1] : 第2段与第1段颜色不同。 来源: dp[1][1] = 2 。 不同颜色选择: m-1 = 1 (假设第1段是红,第2段只能蓝;反之亦然)。 所以 dp[2][1] = dp[1][1] * 1 = 2 * 1 = 2 。 对应方案:[ 红, 蓝] 和 [ 蓝, 红 ]。 计算 dp[2][2] : 第2段与第1段颜色相同。 来源: dp[1][1] = 2 (因为 j=2 由 j-1=1 转移来)。 相同颜色没有选择,直接继承。 所以 dp[2][2] = dp[1][1] = 2 。 对应方案:[ 红, 红] 和 [ 蓝, 蓝 ]。 状态表( i=2 ): dp[2][1] = 2 , dp[2][2] = 2 。 计算 i=3 : 计算 dp[3][1] : 第3段与第2段颜色不同。 来源: dp[2][1] + dp[2][2] = 2 + 2 = 4 。 不同颜色选择: m-1 = 1 。 所以 dp[3][1] = 4 * 1 = 4 。 方案:在 i=2 的所有4种方案后,加上一个与第2段不同的颜色。 例如 [红,蓝] 后加 红 -> [红,蓝,红] 。 具体是: [红,蓝,红] , [蓝,红,蓝] , [红,红,蓝] , [蓝,蓝,红] 。 计算 dp[3][2] : 第3段与第2段颜色相同,且连续长度 j=2 。 来源: dp[2][1] = 2 。 dp[3][2] = dp[2][1] = 2 。 方案:从 [红,蓝] 变成 [红,蓝,蓝] ,从 [蓝,红] 变成 [蓝,红,红] 。 注意 :从 [红,红] 不能转移到 dp[3][2] 吗?可以,但那会导致连续长度 j=3 ( [红,红,红] ),而 k=3 ,这是非法的。我们的状态 j 限制在 1 到 k-1 ,所以根本不会计算 dp[3][3] ,因此在 i=3 时, dp[2][2] 不能作为 dp[3][2] 的来源,因为 j 从 j-1=2 来,而 j=3 是非法的。在我们的转移方程中,只有 dp[i-1][j-1] 当 j<k 时才成立。当 j=2 , j-1=1 ,来源是 dp[i-1][1] ,完全正确。 为了更清晰: dp[3][2] = dp[2][1] = 2 。 状态表( i=3 ): dp[3][1] = 4 , dp[3][2] = 2 。 计算最终答案 : answer = dp[3][1] + dp[3][2] = 4 + 2 = 6 。 与手动列举的结果一致。 优化与代码实现思路(Python伪代码) 我们可以用滚动数组将空间优化到 O(k) 。 思考延伸 : 这个模型是线性DP中处理“连续元素限制”的经典范式。通过增加一个维度(结尾连续长度 j )来记录与当前决策相关的历史信息,从而能够判断新加入元素是否会违反约束。这种方法可以推广到很多类似场景,例如: 最多允许连续 k 个1的二进制字符串计数。 字符串中不能出现连续 k 个相同字符的排列计数问题。