粉刷房子问题是一个经典的动态规划(DP)练习题,但当我们面对"k种颜色"的扩展版本时,很多初学者会感到无从下手。实际上,这个问题完美展示了如何将看似复杂的DP问题拆解为可管理的子问题。
想象你面前有n栋排成一排的房子,每栋房子可以用k种不同颜色中的一种来粉刷。限制条件是相邻两栋房子不能刷同一种颜色。我们需要计算出所有满足条件的粉刷方案的总成本最小值。
这个问题的暴力解法时间复杂度是O(k^n),显然不可行。而通过动态规划,我们可以将其优化到O(nk^2)。但更精妙的是,通过一些观察和技巧,还能进一步优化到O(nk)。
最直观的DP解法是定义dp[i][j]表示粉刷前i栋房子,且第i栋房子使用颜色j时的最小总成本。状态转移方程为:
dp[i][j] = cost[i][j] + min(dp[i-1][m]) (其中m ≠ j)
这意味着对于第i栋房子的每种颜色选择j,我们需要考虑前i-1栋房子所有非j颜色的最小成本。
python复制def minCostII(costs):
if not costs: return 0
n, k = len(costs), len(costs[0])
dp = [[0]*k for _ in range(n)]
dp[0] = costs[0]
for i in range(1, n):
for j in range(k):
dp[i][j] = costs[i][j] + min(dp[i-1][m] for m in range(k) if m != j)
return min(dp[-1])
观察状态转移方程可以发现,dp[i]只依赖于dp[i-1],因此可以将空间复杂度从O(nk)优化到O(k):
python复制def minCostII(costs):
if not costs: return 0
n, k = len(costs), len(costs[0])
prev = costs[0]
for i in range(1, n):
curr = [0]*k
for j in range(k):
curr[j] = costs[i][j] + min(prev[m] for m in range(k) if m != j)
prev = curr
return min(prev)
在计算min(dp[i-1][m] for m != j)时,我们实际上是在寻找前一轮结果中除某个特定颜色外的最小值。如果我们能预先计算出前一轮的最小值和次小值,就可以避免每次都重新计算。
重要提示:当当前颜色j与前一轮最小值对应的颜色相同时,我们必须使用次小值;否则都可以使用最小值。
python复制def minCostII(costs):
if not costs: return 0
n, k = len(costs), len(costs[0])
prev_min = prev_sec_min = 0
prev_color = -1
for i in range(n):
curr_min = curr_sec_min = float('inf')
curr_color = -1
for j in range(k):
# 计算当前颜色j的成本
cost = costs[i][j]
if j == prev_color:
cost += prev_sec_min
else:
cost += prev_min
# 更新当前轮的最小和次小值
if cost < curr_min:
curr_sec_min = curr_min
curr_min = cost
curr_color = j
elif cost < curr_sec_min:
curr_sec_min = cost
prev_min, prev_sec_min, prev_color = curr_min, curr_sec_min, curr_color
return prev_min
当k=1时,如果房子数量n>1,问题无解(因为相邻房子颜色必须不同)。代码中需要特殊处理:
python复制if k == 1:
return sum(c[0] for c in costs) if n == 1 else -1
当costs数组为空时,应该返回0:
python复制if not costs: return 0
这类问题在实际中有很多应用场景:
有效的测试用例应该包含:
在实际测试中(n=1000,k=100):
这种优化在k较大时效果尤为明显,因为将内层循环的O(k)查找优化为了O(1)的常数时间查询。
这个问题的优雅之处在于,它展示了DP问题不仅可以通过状态设计来解决,还可以通过精细的观察进一步优化。在实际面试或竞赛中,能够从O(nk^2)优化到O(nk)往往就是区分普通和优秀解决方案的关键。