1. 问题背景与理解
这道LeetCode题目(1411. 给Nx3网格图涂色的方案数)看似简单,却蕴含着不少有趣的组合数学思想。题目要求我们计算用三种颜色给N行3列的网格涂色的方案数,且相邻格子颜色不能相同。这里的"相邻"指的是上下左右四个方向的直接相邻。
我第一次看到这个问题时,以为就是个简单的排列组合题。但当我尝试用暴力枚举法解决时,很快就发现随着N的增加,计算量呈指数级增长。比如当N=5时,理论上有3^15种可能的涂法(每个格子3种选择,共15个格子),直接枚举显然不现实。
2. 基础解法分析
2.1 小规模情况枚举
让我们先从小规模的N开始分析。对于N=1的情况,即单行3列的网格:
- 第一个格子有3种选择
- 第二个格子有2种选择(不能与第一个相同)
- 第三个格子有2种选择(不能与第二个相同)
所以总共有3×2×2=12种涂法。这与题目给出的示例一致。
但当N=2时,情况就复杂多了。第二行的涂法不仅取决于自身的相邻格子,还受第一行的影响。这时候我们就需要更系统的方法。
2.2 状态转移思路
这个问题非常适合用动态规划来解决。我们可以将每一行的涂色模式视为一个"状态",然后计算从一个状态转移到另一个状态的方案数。
具体来说,每一行的涂色模式可以分为两类:
- ABC型:三个格子颜色各不相同,如红绿蓝
- ABA型:两边的格子颜色相同,中间的格子颜色不同,如红绿红
对于N=1时:
- ABC型有6种(3种颜色排列)
- ABA型有6种(3种主色×2种中间色)
3. 动态规划解法实现
3.1 状态定义与转移方程
设:
- dp_abc[n] 表示第n行是ABC型的方案数
- dp_aba[n] 表示第n行是ABA型的方案数
初始条件:
- dp_abc[1] = 6
- dp_aba[1] = 6
转移方程:
一个ABC型的下一行可以是:
- 2种ABC型(如红绿蓝→绿红绿、绿蓝绿)
- 2种ABA型(如红绿蓝→绿红蓝、绿蓝红)
一个ABA型的下一行可以是:
- 2种ABC型(如红绿红→绿红绿、绿蓝绿)
- 3种ABA型(如红绿红→绿红绿、绿蓝绿、蓝绿蓝)
因此转移方程为: dp_abc[n] = 2 * dp_abc[n-1] + 2 * dp_aba[n-1] dp_aba[n] = 2 * dp_abc[n-1] + 3 * dp_aba[n-1]
3.2 Java实现代码
public int numOfWays(int n) { if (n == 0) return 0; long abc = 6; // ABC型 long aba = 6; // ABA型 long mod = 1000000007; for (int i = 2; i <= n; i++) { long new_abc = (2 * abc + 2 * aba) % mod; long new_aba = (2 * abc + 3 * aba) % mod; abc = new_abc; aba = new_aba; } return (int)((abc + aba) % mod); }3.3 复杂度分析
- 时间复杂度:O(N),因为我们只需要进行N次迭代计算
- 空间复杂度:O(1),只使用了常数个变量存储状态
4. 数学优化与矩阵快速幂
4.1 状态转移的矩阵表示
我们可以将状态转移表示为矩阵乘法:
[abc_n] = [2 2][abc_{n-1}] [aba_n] [2 3][aba_{n-1}]
这意味着我们可以使用矩阵快速幂来将时间复杂度优化到O(logN)。
4.2 矩阵快速幂实现
final int MOD = 1000000007; public int numOfWays(int n) { if (n == 0) return 0; if (n == 1) return 12; long[][] transition = {{2, 2}, {2, 3}}; long[][] result = matrixPower(transition, n - 1); long abc = 6, aba = 6; long total = (result[0][0] * abc + result[0][1] * aba) % MOD; total = (total + (result[1][0] * abc + result[1][1] * aba) % MOD) % MOD; return (int)total; } private long[][] matrixPower(long[][] matrix, int power) { long[][] result = {{1, 0}, {0, 1}}; // 单位矩阵 while (power > 0) { if ((power & 1) == 1) { result = multiplyMatrix(result, matrix); } matrix = multiplyMatrix(matrix, matrix); power >>= 1; } return result; } private long[][] multiplyMatrix(long[][] a, long[][] b) { long[][] res = new long[2][2]; for (int i = 0; i < 2; i++) { for (int j = 0; j < 2; j++) { res[i][j] = (a[i][0] * b[0][j] + a[i][1] * b[1][j]) % MOD; } } return res; }5. 边界条件与注意事项
5.1 大数处理
由于结果可能非常大(当N=5000时,结果约为1e2000量级),题目要求对结果取模1000000007。在实现时需要注意:
- 使用long类型存储中间结果,避免int溢出
- 每次运算后立即取模,而不是最后才取模
- 矩阵乘法中的乘法操作可能导致中间结果溢出,需要及时取模
5.2 初始条件
当N=0时,应该返回0,因为没有格子需要涂色。这是一个容易被忽略的边界条件。
5.3 测试用例验证
建议测试以下情况:
- N=1 → 12
- N=2 → 54
- N=3 → 246
- N=7 → 106494
- N=5000 → 30228214
6. 扩展思考
6.1 更一般的m×n网格
如果问题扩展到m×n网格(m不固定为3),解法会有什么变化?
这种情况下,状态数会随着m的增加而急剧增加(对于m列,状态数约为3×2^(m-1))。对于较大的m,可能需要更复杂的动态规划方法或状态压缩技巧。
6.2 颜色数量的变化
如果题目改为使用k种颜色而不是3种,解法需要如何调整?
初始的ABC型和ABA型的数量会变化:
- ABC型:k×(k-1)×(k-2)
- ABA型:k×(k-1)
转移方程中的系数也需要相应调整。
6.3 实际应用场景
这类问题在实际中有多种应用:
- 平面地图着色问题
- 电路板布线设计
- 日程安排中的冲突避免
- 图形学中的纹理生成
7. 个人解题心得
在解决这个问题时,我最初尝试了暴力回溯法,很快就遇到了性能瓶颈。通过分析小规模案例,我发现了状态分类的规律。动态规划的解法虽然高效,但推导状态转移方程需要仔细分析所有可能的情况。
矩阵快速幂的优化是一个惊喜,它将线性时间复杂度降低到了对数级。这提醒我在解决类似问题时,可以考虑是否存在数学上的优化空间。
一个容易犯的错误是忽略了模运算的时机。我在第一次提交时,只在最后做了取模,结果中间计算就溢出了。正确的做法是在每次运算后都立即取模。