“粉刷栅栏”的变种:最多允许两个相邻栅栏同色的方案数
字数 3357 2025-12-08 06:05:02

好的,我随机选择一道线性动态规划领域的算法题目,它不在你提供的已讲题目列表中。题目如下:

“粉刷栅栏”的变种:最多允许两个相邻栅栏同色的方案数


1. 题目描述

假设你需要粉刷一排 n 个栅栏柱,每个栅栏柱可以粉刷 k 种颜色中的一种。但是,为了美观,不允许有超过两个相邻的栅栏柱被粉刷成相同的颜色。换句话说,连续三个栅栏柱不能是同一种颜色。

你需要计算的是,给定 n 个栅栏柱和 k 种颜色,有多少种不同的粉刷方案

示例1:

  • 输入:n = 3, k = 2
  • 输出:6
  • 解释:假设两种颜色是红色(R)和蓝色(B)。合法的方案有:RRB, RBR, RBB, BRR, BRB, BBR。注意 RRRBBB 是不合法的,因为有三个连续相同的颜色。

示例2:

  • 输入:n = 1, k = 1
  • 输出:1
  • 解释:只有一个栅栏,用唯一的一种颜色粉刷。

2. 解题思路分析

这是一个典型的计数类动态规划问题。我们需要找出符合“连续同色不超过2个”的所有序列的数量。

关键点在于如何定义状态。
我们不能仅仅用一个维度(比如 dp[i] 表示粉刷前 i 个栅栏的方案数),因为这个状态无法体现最后两个栅栏的颜色关系,而规则恰恰是关于相邻颜色的。

一个经典的思路是,根据最后两个栅栏的颜色是否相同来定义状态

定义状态数组:

  • dp_same[i]:粉刷完前 i 个栅栏,并且 i 个栅栏与第 i-1 个栅栏颜色相同 的合法方案数。
  • dp_diff[i]:粉刷完前 i 个栅栏,并且 i 个栅栏与第 i-1 个栅栏颜色不同 的合法方案数。

我们的最终答案是 dp_same[n] + dp_diff[n],即考虑第 n 个栅栏所有可能情况的总和。


3. 状态转移方程推导

现在,我们来思考如何从 i-1 的状态推导出 i 的状态。

情况一:求 dp_same[i]

  • dp_same[i] 表示第 i 个和第 i-1 个颜色相同。
  • 既然它们颜色相同,那么第 i-1 个栅栏必须与第 i-2 个栅栏颜色不同。否则就会出现三个连续相同颜色,违反规则。
  • 因此,dp_same[i] 只能由 dp_diff[i-1] 转移而来。
  • 当从 dp_diff[i-1] 转移时,第 i-1 个栅栏有某种颜色,为了让第 i 个与之相同,只有 1 种选择(就是用和第 i-1 个一模一样的颜色)。
  • 所以,状态转移方程为:
    dp_same[i] = dp_diff[i-1] * 1

情况二:求 dp_diff[i]

  • dp_diff[i] 表示第 i 个和第 i-1 个颜色不同。
  • 对于第 i 个栅栏,只要颜色和第 i-1 个不同即可。
  • 这种情况可以从两种前序状态转移过来:
    1. dp_same[i-1] 转移:第 i-1 个和第 i-2 个颜色相同。那么第 i 个只要不和 i-1 同色就行。k 种颜色中有 (k-1) 种选择。
    2. dp_diff[i-1] 转移:第 i-1 个和第 i-2 个颜色不同。那么第 i 个同样只要不和 i-1 同色就行。k 种颜色中有 (k-1) 种选择。
  • 所以,状态转移方程为:
    dp_diff[i] = (dp_same[i-1] + dp_diff[i-1]) * (k-1)

4. 初始化

我们需要确定 i=1i=2 时的状态值。

  • i=1 (第一个栅栏)时:

    • dp_same[1]:第一个栅栏没有前一个栅栏,不存在“相同”的概念。为了计算方便,我们定义其为 0
    • dp_diff[1]:第一个栅栏可以选择 k 种颜色中的任意一种,因为没有“相邻不同”的限制。所以 dp_diff[1] = k
  • i=2 (第二个栅栏)时:

    • 我们可以根据 i=1 的状态和转移方程来计算,也可以用直观方式理解:
    • dp_same[2]:第二个和第一个颜色相同。第一个栅栏有 k 种选择,第二个必须与之相同,只有 1 种选择。所以 dp_same[2] = k * 1 = k
    • dp_diff[2]:第二个和第一个颜色不同。第一个栅栏有 k 种选择,第二个有 (k-1) 种选择。所以 dp_diff[2] = k * (k-1)

我们可以验证一下我们的转移方程:
dp_same[2] = dp_diff[1] * 1 = k * 1 = k
dp_diff[2] = (dp_same[1] + dp_diff[1]) * (k-1) = (0 + k) * (k-1) = k*(k-1)


5. 计算过程示例

让我们用 n=3, k=2 这个例子来手动计算一遍。

  1. 初始化:

    • dp_same[1] = 0
    • dp_diff[1] = k = 2
    • 此时总方案数 total_1 = 0 + 2 = 2 (即:第一个栅栏刷成R或B)。
  2. 计算 i=2

    • dp_same[2] = dp_diff[1] * 1 = 2 * 1 = 2 (即:RR, BB)
    • dp_diff[2] = (dp_same[1] + dp_diff[1]) * (k-1) = (0 + 2) * (2-1) = 2 * 1 = 2 (即:RB, BR)
    • 总方案数 total_2 = 2 + 2 = 4。这也正是两个栅栏所有可能的粉刷方案(RR, RB, BR, BB)。
  3. 计算 i=3

    • dp_same[3] = dp_diff[2] * 1 = 2 * 1 = 2
      • dp_diff[2] (即 RB, BR) 转移而来:
        • RB 出发,第三个必须和第二个(B)相同,得到 RBB
        • BR 出发,第三个必须和第二个(R)相同,得到 BRR
    • dp_diff[3] = (dp_same[2] + dp_diff[2]) * (k-1) = (2 + 2) * (2-1) = 4 * 1 = 4
      • dp_same[2] (即 RR, BB) 转移而来:第三个必须和前一个不同。
        • RR 出发,第三个只能是B,得到 RRB
        • BB 出发,第三个只能是R,得到 BBR
      • dp_diff[2] (即 RB, BR) 转移而来:第三个必须和前一个不同。
        • RB 出发,第三个不能是B,只能是R,得到 RBR
        • BR 出发,第三个不能是R,只能是B,得到 BRB
    • 总方案数 total_3 = dp_same[3] + dp_diff[3] = 2 + 4 = 6。这与我们题目描述中的示例完全吻合。

6. 算法实现(伪代码)

def numWays(n: int, k: int) -> int:
    if n == 0:
        return 0
    if n == 1:
        return k

    # 初始化
    dp_same = [0] * (n + 1)
    dp_diff = [0] * (n + 1)
    dp_same[1] = 0
    dp_diff[1] = k

    for i in range(2, n + 1):
        dp_same[i] = dp_diff[i - 1]
        dp_diff[i] = (dp_same[i - 1] + dp_diff[i - 1]) * (k - 1)

    return dp_same[n] + dp_diff[n]

空间优化:
由于 dp[i] 只依赖于 dp[i-1],我们可以只使用几个变量,将空间复杂度优化到 O(1)

def numWays_optimized(n: int, k: int) -> int:
    if n == 0:
        return 0
    if n == 1:
        return k

    same = 0  # 对应 i=1
    diff = k  # 对应 i=1

    for i in range(2, n + 1):
        prev_same = same
        prev_diff = diff
        same = prev_diff
        diff = (prev_same + prev_diff) * (k - 1)

    return same + diff

7. 总结

这道题的精髓在于状态的定义。通过将问题拆分为“最后两个同色”(same)和“最后两个不同色”(diff)两种子状态,我们成功地将一个关于“三个连续元素”的限制,转化为了关于“两个相邻元素”的、更易于处理的状态转移。

  • 状态定义dp_same[i], dp_diff[i]
  • 转移方程
    • dp_same[i] = dp_diff[i-1]
    • dp_diff[i] = (dp_same[i-1] + dp_diff[i-1]) * (k-1)
  • 初始化dp_same[1]=0, dp_diff[1]=k
  • 最终答案dp_same[n] + dp_diff[n]

这个方法清晰、高效,且可以扩展到更复杂的情况(例如,将规则从“最多两个连续同色”改为“最多 m 个连续同色”,则需要更多维度的状态来记录末尾连续同色的个数)。

好的,我随机选择一道线性动态规划领域的算法题目,它不在你提供的已讲题目列表中。题目如下: “粉刷栅栏”的变种:最多允许两个相邻栅栏同色的方案数 1. 题目描述 假设你需要粉刷一排 n 个栅栏柱,每个栅栏柱可以粉刷 k 种颜色中的一种。但是,为了美观, 不允许有超过两个相邻的栅栏柱被粉刷成相同的颜色 。换句话说,连续三个栅栏柱不能是同一种颜色。 你需要计算的是,给定 n 个栅栏柱和 k 种颜色,有多少种 不同的粉刷方案 。 示例1: 输入: n = 3, k = 2 输出: 6 解释:假设两种颜色是红色(R)和蓝色(B)。合法的方案有: RRB , RBR , RBB , BRR , BRB , BBR 。注意 RRR 和 BBB 是不合法的,因为有三个连续相同的颜色。 示例2: 输入: n = 1, k = 1 输出: 1 解释:只有一个栅栏,用唯一的一种颜色粉刷。 2. 解题思路分析 这是一个典型的 计数类动态规划 问题。我们需要找出符合“连续同色不超过2个”的所有序列的数量。 关键点在于如何定义状态。 我们不能仅仅用一个维度(比如 dp[i] 表示粉刷前 i 个栅栏的方案数),因为这个状态无法体现最后两个栅栏的颜色关系,而规则恰恰是关于相邻颜色的。 一个经典的思路是, 根据最后两个栅栏的颜色是否相同来定义状态 。 定义状态数组: dp_same[i] :粉刷完前 i 个栅栏,并且 第 i 个栅栏与第 i-1 个栅栏颜色相同 的合法方案数。 dp_diff[i] :粉刷完前 i 个栅栏,并且 第 i 个栅栏与第 i-1 个栅栏颜色不同 的合法方案数。 我们的最终答案是 dp_same[n] + dp_diff[n] ,即考虑第 n 个栅栏所有可能情况的总和。 3. 状态转移方程推导 现在,我们来思考如何从 i-1 的状态推导出 i 的状态。 情况一:求 dp_same[i] dp_same[i] 表示第 i 个和第 i-1 个颜色相同。 既然它们颜色相同,那么第 i-1 个栅栏 必须 与第 i-2 个栅栏颜色 不同 。否则就会出现三个连续相同颜色,违反规则。 因此, dp_same[i] 只能由 dp_diff[i-1] 转移而来。 当从 dp_diff[i-1] 转移时,第 i-1 个栅栏有某种颜色,为了让第 i 个与之相同, 只有 1 种选择 (就是用和第 i-1 个一模一样的颜色)。 所以,状态转移方程为: dp_same[i] = dp_diff[i-1] * 1 情况二:求 dp_diff[i] dp_diff[i] 表示第 i 个和第 i-1 个颜色不同。 对于第 i 个栅栏,只要颜色和第 i-1 个不同即可。 这种情况可以从两种前序状态转移过来: 从 dp_same[i-1] 转移:第 i-1 个和第 i-2 个颜色相同。那么第 i 个只要不和 i-1 同色就行。 k 种颜色中有 (k-1) 种选择。 从 dp_diff[i-1] 转移:第 i-1 个和第 i-2 个颜色不同。那么第 i 个同样只要不和 i-1 同色就行。 k 种颜色中有 (k-1) 种选择。 所以,状态转移方程为: dp_diff[i] = (dp_same[i-1] + dp_diff[i-1]) * (k-1) 4. 初始化 我们需要确定 i=1 和 i=2 时的状态值。 当 i=1 (第一个栅栏)时: dp_same[1] :第一个栅栏没有前一个栅栏,不存在“相同”的概念。为了计算方便,我们定义其为 0 。 dp_diff[1] :第一个栅栏可以选择 k 种颜色中的任意一种,因为没有“相邻不同”的限制。所以 dp_diff[1] = k 。 当 i=2 (第二个栅栏)时: 我们可以根据 i=1 的状态和转移方程来计算,也可以用直观方式理解: dp_same[2] :第二个和第一个颜色相同。第一个栅栏有 k 种选择,第二个必须与之相同,只有 1 种选择。所以 dp_same[2] = k * 1 = k 。 dp_diff[2] :第二个和第一个颜色不同。第一个栅栏有 k 种选择,第二个有 (k-1) 种选择。所以 dp_diff[2] = k * (k-1) 。 我们可以验证一下我们的转移方程: dp_same[2] = dp_diff[1] * 1 = k * 1 = k ✅ dp_diff[2] = (dp_same[1] + dp_diff[1]) * (k-1) = (0 + k) * (k-1) = k*(k-1) ✅ 5. 计算过程示例 让我们用 n=3, k=2 这个例子来手动计算一遍。 初始化: dp_same[1] = 0 dp_diff[1] = k = 2 此时总方案数 total_1 = 0 + 2 = 2 (即:第一个栅栏刷成R或B)。 计算 i=2 : dp_same[2] = dp_diff[1] * 1 = 2 * 1 = 2 (即: RR , BB ) dp_diff[2] = (dp_same[1] + dp_diff[1]) * (k-1) = (0 + 2) * (2-1) = 2 * 1 = 2 (即: RB , BR ) 总方案数 total_2 = 2 + 2 = 4 。这也正是两个栅栏所有可能的粉刷方案( RR , RB , BR , BB )。 计算 i=3 : dp_same[3] = dp_diff[2] * 1 = 2 * 1 = 2 从 dp_diff[2] (即 RB , BR ) 转移而来: 从 RB 出发,第三个必须和第二个(B)相同,得到 RBB 。 从 BR 出发,第三个必须和第二个(R)相同,得到 BRR 。 dp_diff[3] = (dp_same[2] + dp_diff[2]) * (k-1) = (2 + 2) * (2-1) = 4 * 1 = 4 从 dp_same[2] (即 RR , BB ) 转移而来:第三个必须和前一个不同。 从 RR 出发,第三个只能是B,得到 RRB 。 从 BB 出发,第三个只能是R,得到 BBR 。 从 dp_diff[2] (即 RB , BR ) 转移而来:第三个必须和前一个不同。 从 RB 出发,第三个不能是B,只能是R,得到 RBR 。 从 BR 出发,第三个不能是R,只能是B,得到 BRB 。 总方案数 total_3 = dp_same[3] + dp_diff[3] = 2 + 4 = 6 。这与我们题目描述中的示例完全吻合。 6. 算法实现(伪代码) 空间优化: 由于 dp[i] 只依赖于 dp[i-1] ,我们可以只使用几个变量,将空间复杂度优化到 O(1) 。 7. 总结 这道题的精髓在于 状态的定义 。通过将问题拆分为“最后两个同色”( same )和“最后两个不同色”( diff )两种子状态,我们成功地将一个关于“三个连续元素”的限制,转化为了关于“两个相邻元素”的、更易于处理的状态转移。 状态定义 : dp_same[i] , dp_diff[i] 转移方程 : dp_same[i] = dp_diff[i-1] dp_diff[i] = (dp_same[i-1] + dp_diff[i-1]) * (k-1) 初始化 : dp_same[1]=0 , dp_diff[1]=k 最终答案 : dp_same[n] + dp_diff[n] 这个方法清晰、高效,且可以扩展到更复杂的情况(例如,将规则从“最多两个连续同色”改为“最多 m 个连续同色”,则需要更多维度的状态来记录末尾连续同色的个数)。