1. 问题背景与定义
今天我们来拆解一道经典的状态压缩动态规划问题——LC1931"用三种不同颜色为网格涂色"。这道题看似简单,却蕴含着动态规划中状态压缩的精妙思想。我们先明确题目要求:
给定一个m×n的网格,需要用三种颜色(假设为红、绿、蓝)为每个格子涂色,且相邻(上下左右)格子颜色不能相同。问有多少种合法的涂色方案。
这个问题在实际中有诸多应用场景,比如:
- 地图着色问题(确保相邻区域颜色不同)
- 电路板布线(避免相邻线路干扰)
- 日程排班(相邻时段安排不同活动类型)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 暴力搜索的局限性
最直观的解法是暴力枚举所有可能的涂色方案,然后检查每个方案是否满足相邻颜色不同的条件。对于一个m×n的网格,时间复杂度是O(3^(m×n)),这在m和n稍大时(比如10×10)就会变得完全不可行。
举个例子,对于5×5的网格:
- 总共有3^25 ≈ 847亿种可能的涂色方案
- 即使每秒能检查100万个方案,也需要约24小时才能完成
- 这显然不适用于实际应用
3. 动态规划的思路
动态规划(DP)的核心思想是将问题分解为子问题,并存储子问题的解以避免重复计算。对于网格涂色问题,我们可以逐行考虑:
定义dp[i][c]表示前i行已经涂色,且第i行的颜色配置为c时的合法方案数。其中c是一个表示颜色序列的状态(需要压缩表示)。
关键观察:
- 当前行的涂色只受上一行影响
- 相邻行之间相同列的颜色不能相同
- 同一行内相邻格子的颜色不能相同
4. 状态压缩的实现
状态压缩DP的关键是如何高效表示和转移状态。对于颜色序列,我们可以用三进制数表示:
- 0表示红色
- 1表示绿色
- 2表示蓝色
例如,对于3列的网格:
- "红绿蓝"表示为0×3^0 + 1×3^1 + 2×3^2 = 0+3+18=21
- "蓝绿红"表示为2×3^0 + 1×3^1 + 0×3^2 = 2+3+0=5
状态转移时,我们需要:
- 检查当前行颜色是否合法(相邻不同色)
- 检查与上一行对应列是否不同色
Python实现示例:
python复制def colorTheGrid(m: int, n: int) -> int:
MOD = 10**9 + 7
from itertools import product
# 生成所有合法单行状态
def generate_states():
states = []
for colors in product([0,1,2], repeat=n):
valid = True
for i in range(n-1):
if colors[i] == colors[i+1]:
valid = False
break
if valid:
states.append(colors)
return states
states = generate_states()
state_idx = {s:i for i,s in enumerate(states)}
k = len(states)
# 构建转移矩阵
trans = [[] for _ in range(k)]
for i in range(k):
for j in range(k):
valid = True
for a,b in zip(states[i], states[j]):
if a == b:
valid = False
break
if valid:
trans[i].append(j)
# 初始化DP表
dp = [[0]*k for _ in range(m)]
for i in range(k):
dp[0][i] = 1
# DP过程
for row in range(1, m):
for prev in range(k):
if dp[row-1][prev] == 0:
continue
for curr in trans[prev]:
dp[row][curr] = (dp[row][curr] + dp[row-1][prev]) % MOD
return sum(dp[m-1]) % MOD
5. 优化技巧与注意事项
在实际实现中,有几个关键优化点:
- 状态预处理:提前生成所有合法单行状态,可以避免在DP过程中重复检查
- 转移矩阵:预先计算状态之间的转移关系,加速DP过程
- 滚动数组:由于DP只依赖前一行,可以只用两个一维数组节省空间
常见踩坑点:
- 忘记取模(结果可能非常大)
- 错误的状态表示(比如直接用元组而不用数字,导致哈希效率低)
- 同一行相邻颜色检查不完整
提示:对于n较大的情况(比如n>7),状态数会爆炸式增长(3^7=2187),这时可能需要更高级的优化技巧,如矩阵快速幂。
6. 复杂度分析
设k为合法单行状态数,最坏情况下k=O(2×3^(n-1))(因为每个位置有约2种选择,而不是3种)
时间复杂度:
- 预处理:O(n×3^n)
- DP过程:O(m×k^2)
空间复杂度:
- O(k^2)存储转移关系
- O(k)存储DP状态(使用滚动数组时)
7. 实际应用扩展
这个问题可以扩展为更实际的场景:
- 多颜色版本:如果颜色种类不是3种而是k种,只需调整状态生成部分
- 不规则网格:对于非矩形的相邻关系,可以用图着色问题建模
- 加权版本:不同颜色组合有不同的权重,求最优着色方案
例如,考虑颜色偏好时的修改:
python复制color_cost = {0:1, 1:2, 2:3} # 不同颜色的成本
def generate_states():
states = []
costs = []
for colors in product([0,1,2], repeat=n):
valid = True
cost = 0
for i in range(n-1):
if colors[i] == colors[i+1]:
valid = False
break
cost += color_cost[colors[i]]
if valid:
cost += color_cost[colors[-1]]
states.append(colors)
costs.append(cost)
return states, costs
8. 与其他DP问题的对比
这个问题与以下经典DP问题有相似之处:
- 数位DP:都是处理排列组合问题,利用状态压缩
- 旅行商问题(TSP):状态表示城市访问情况
- 铺砖问题:考虑相邻块的约束关系
关键区别在于:
- 网格涂色更关注颜色约束
- 状态转移基于颜色而非位置
- 约束是局部的(相邻格子)
9. 测试用例设计
验证代码正确性的关键测试用例:
-
边界情况:
- 1×1网格:3种方案
- 1×2网格:3×2=6种
- 2×1网格:同样6种
-
小规模常规情况:
- 2×2网格:
code复制
共6×2=12种(第一行6种,第二行每种对应2种)红绿 红蓝 绿红 绿蓝 蓝红 蓝绿 绿红, 蓝红, 红绿, 红蓝, 绿蓝, 蓝绿
- 2×2网格:
-
对称性检查:
- m×n和n×m的结果应该相同
- 颜色轮换不应影响方案数
10. 性能优化实战
当n=5,m=1000时,基本解法可能超时。我们可以优化:
-
矩阵快速幂:将DP转移表示为矩阵乘法
- 转移矩阵T:T[i][j]=1如果状态i可以转移到j
- 初始向量v:全1向量
- 结果=T^(m-1) × v
-
对称性利用:颜色排列对称,减少状态数
- 将颜色排列视为等价(如红绿蓝≡蓝绿红)
- 需要额外记录等价类的大小
优化后的矩阵快速幂实现:
python复制def matrix_mult(A, B, MOD):
n = len(A)
res = [[0]*n for _ in range(n)]
for i in range(n):
for k in range(n):
if A[i][k] == 0:
continue
for j in range(n):
res[i][j] = (res[i][j] + A[i][k] * B[k][j]) % MOD
return res
def matrix_pow(mat, power, MOD):
n = len(mat)
result = [[0]*n for _ in range(n)]
for i in range(n):
result[i][i] = 1
while power > 0:
if power % 2 == 1:
result = matrix_mult(result, mat, MOD)
mat = matrix_mult(mat, mat, MOD)
power //= 2
return result
def colorTheGridFast(m, n):
# 生成状态和转移矩阵(同上)
# ...
if m == 1:
return len(states)
trans_mat = [[0]*k for _ in range(k)]
for i in range(k):
for j in trans[i]:
trans_mat[i][j] = 1
mat = matrix_pow(trans_mat, m-1, MOD)
total = 0
for i in range(k):
for j in range(k):
total = (total + mat[i][j]) % MOD
return total
这个优化将时间复杂度从O(m×k^2)降到了O(log m × k^3),对于大的m值非常有效。
