好的,我随机选择一道线性动态规划领域的算法题目,它不在你提供的已讲题目列表中。题目如下:
“粉刷栅栏”的变种:最多允许两个相邻栅栏同色的方案数
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] = 0dp_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. 算法实现(伪代码)
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 个连续同色”,则需要更多维度的状态来记录末尾连续同色的个数)。