题目链接1931. 用三种不同颜色为网格涂色 - 力扣(LeetCode)
一、题目分析
题目描述
给定一个 m
行 n
列的网格,每个格子需涂红、绿、蓝三种颜色之一。要求:
- 同一行内相邻格子颜色不同;
- 相邻行的同一列格子颜色不同。
求满足条件的涂色方案数,结果对10^9+7
取模。
核心约束
- 行内相邻颜色互异 → 每行的颜色序列是合法排列(无相邻重复)。
- 列间相邻颜色互异 → 相邻行的同一列颜色不同。
二、解题思路:状态压缩动态规划
1. 状态表示:用三进制数压缩行状态
- 每行状态:用三进制数
mask
表示一行的颜色分布。例如,m=2
时,mask=5
(三进制为12
)表示该行颜色为[红, 蓝]
。 - 合法行筛选:枚举所有
0~3^m-1
的mask
,检查是否满足行内无相邻同色。合法的mask
及其颜色序列存入哈希表valid
。
2. 相邻行关系预处理
- 相邻行合法性判断:对于两个合法行
mask1
和mask2
,逐列检查颜色是否相同。若所有列颜色均不同,则mask1
和mask2
可作为相邻行。 - 构建邻接表:用哈希表
adjacent
存储每个mask
可转移到的下一行mask
列表,便于后续动态规划快速查询。
3. 动态规划转移
- 状态定义:
f[i][mask]
表示前i
行且第i
行状态为mask
时的方案数。 - 初始化:第一行所有合法
mask
的方案数为1
(f[0][mask] = 1
)。 - 状态转移:对于第
i
行(i≥1
),遍历所有合法mask2
,其方案数等于所有可转移至mask2
的前一行mask1
的方案数之和(f[i][mask2] = sum(f[i-1][mask1])
,其中mask1
与mask2
相邻合法)。 - 空间优化:由于每次转移仅依赖前一行状态,可用一维数组
f
滚动更新,节省空间。
三、代码实现详解
1. 筛选合法行状态
unordered_map<int, vector<int>> valid;
int mask_end = pow(3, m);
for (int mask = 0; mask < mask_end; ++mask) {
vector<int> color;
int mm = mask;
// 分解三进制每一位,得到颜色序列
for (int i = 0; i < m; ++i) {
color.push_back(mm % 3);
mm /= 3;
}
// 检查行内相邻颜色是否不同
bool check = true;
for (int i = 0; i < m - 1; ++i) {
if (color[i] == color[i + 1]) {
check = false;
break;
}
}
if (check) {
valid[mask] = move(color);
}
}
- 三进制分解:通过取模和除法提取每一位颜色(
0
、1
、2
分别代表红、绿、蓝)。 - 合法性检查:遍历行内相邻元素,确保无同色相邻。
2. 预处理相邻行关系
unordered_map<int, vector<int>> adjacent;
for (const auto& [mask1, color1] : valid) {
for (const auto& [mask2, color2] : valid) {
bool check = true;
// 检查每一列颜色是否不同
for (int i = 0; i < m; ++i) {
if (color1[i] == color2[i]) {
check = false;
break;
}
}
if (check) {
adjacent[mask1].push_back(mask2); // mask1 下一行可以是 mask2
}
}
}
- 双重循环:遍历所有合法行对,逐列检查颜色是否冲突。
- 邻接表存储:
adjacent[mask1]
存储所有可接在mask1
后的合法行mask2
。
3. 动态规划计算方案数
vector<int> f(mask_end, 0);
// 初始化第一行:所有合法 mask 方案数为 1
for (const auto& [mask, _] : valid) {
f[mask] = 1;
}
for (int i = 1; i < n; ++i) {
vector<int> g(mask_end, 0);
for (const auto& [mask2, _] : valid) {
// 遍历所有能转移到 mask2 的前一行 mask1
for (int mask1 : adjacent[mask2]) { // 注意:这里存储的是 mask2 的前驱 mask1
g[mask2] = (g[mask2] + f[mask1]) % mod;
}
}
f = move(g); // 滚动数组优化空间
}
// 累加所有合法行的方案数
int ans = 0;
for (int num : f) {
ans = (ans + num) % mod;
}
return ans;
- 初始化:第一行所有合法状态的方案数为
1
。 - 状态转移:对于当前行状态
mask2
,其方案数等于所有前驱状态mask1
(即满足mask1
可转移至mask2
)的方案数之和。 - 取模操作:每次加法后取模,避免整数溢出。
四、复杂度分析
时间复杂度
- 合法行筛选:
O(3^m * m)
,枚举所有3^m
个状态,每个状态检查m
次相邻颜色。 - 邻接表构建:
O(V^2 * m)
,其中V
为合法行数量(最多3×2^{m-1}
,即每行颜色交替排列的情况),每次检查m
列颜色。 - 动态规划:
O(n×V^2)
,每层循环遍历V
个状态,每个状态遍历其前驱(最多V
个)。
空间复杂度
- 合法行存储:
O(V×m)
,存储每个合法行的颜色序列。 - 邻接表:
O(V^2)
,存储状态转移关系。 - 动态规划数组:
O(V)
,使用滚动数组优化后仅需一维数组。
五、优化与扩展
1. 状态压缩优化
- 行状态可仅用整数
mask
表示,无需存储颜色序列(邻接表检查时通过位运算分解mask
得到颜色)。修改后可节省O(V×m)
的空间。
2. 预处理三进制位
- 提前计算每个
mask
的三进制各位值,避免重复分解mask
,提升邻接表构建效率。
3. 适用于更大 m
的情况
- 当
m
较大时(如m=10
),合法行数量V
可能达到3×2^9=1536
,此时邻接表构建和动态规划的复杂度为O(1536^2×n)
,对于n=1e5
仍可接受。但m
超过12
时,3^m
超过百万,需进一步优化(如矩阵快速幂优化动态规划转移)。
六、总结
本题通过状态压缩将行状态转化为整数,结合动态规划和邻接表预处理,高效解决了行列约束下的涂色问题。核心思路是:
- 状态压缩:用三进制数表示行颜色,快速筛选合法行。
- 邻接表预处理:提前存储合法状态转移关系,避免动态规划时重复判断。
- 滚动数组优化:利用动态规划的无后效性,节省空间复杂度。
该解法适用于 m
较小(如 m≤12
)的场景,是状态压缩类问题的经典应用。对于更大规模的问题,可进一步结合矩阵快速幂优化转移过程,将时间复杂度从 O(n×V^2)
降至 O(V^3×logn)
。
完整代码
#include <iostream>
#include <vector>
#include <unordered_map>
#include <cmath>
using namespace std;
constexpr int mod = 1000000007;
class Solution {
public:
int colorTheGrid(int m, int n) {
// 预处理所有合法的行状态(mask及其对应的颜色数组)
unordered_map<int, vector<int>> validMasks;
int maxMask = pow(3, m); // 3^m的最大mask值
// 枚举所有可能的mask,筛选合法行
for (int mask = 0; mask < maxMask; ++mask) {
vector<int> colors;
int temp = mask;
bool isValid = true;
// 分解mask的三进制每一位,得到颜色数组
for (int i = 0; i < m; ++i) {
int color = temp % 3;
colors.push_back(color);
temp /= 3;
// 检查当前颜色与前一个是否相同(行内相邻约束)
if (i > 0 && colors[i] == colors[i-1]) {
isValid = false;
}
}
if (isValid) {
validMasks[mask] = move(colors);
}
}
// 预处理相邻行的合法转移关系(邻接表)
unordered_map<int, vector<int>> adjList;
vector<int> allValidMasks;
for (const auto& [mask, _] : validMasks) {
allValidMasks.push_back(mask); // 存储所有合法mask,方便遍历
}
// 双重循环遍历所有合法mask对,构建邻接表
for (int i = 0; i < allValidMasks.size(); ++i) {
int mask1 = allValidMasks[i];
const vector<int>& colors1 = validMasks[mask1];
for (int j = 0; j < allValidMasks.size(); ++j) {
int mask2 = allValidMasks[j];
const vector<int>& colors2 = validMasks[mask2];
bool isAdjValid = true;
// 检查每一列颜色是否不同(列间相邻约束)
for (int k = 0; k < m; ++k) {
if (colors1[k] == colors2[k]) {
isAdjValid = false;
break;
}
}
if (isAdjValid) {
adjList[mask1].push_back(mask2);
}
}
}
// 动态规划:使用滚动数组优化空间
vector<int> dp(maxMask, 0);
// 初始化第一行:所有合法mask的方案数为1
for (int mask : allValidMasks) {
dp[mask] = 1;
}
// 逐行计算后续行的方案数
for (int step = 1; step < n; ++step) {
vector<int> nextDp(maxMask, 0);
// 遍历当前行的所有合法mask(作为下一行)
for (int currMask : allValidMasks) {
// 遍历所有可以转移到currMask的前一行mask(即邻接表中的前驱)
for (int prevMask : adjList[currMask]) { // 注意:这里存储的是反向边(currMask的前驱是prevMask)
nextDp[currMask] = (nextDp[currMask] + dp[prevMask]) % mod;
}
}
dp = move(nextDp); // 滚动数组更新
}
// 累加所有合法行的方案数
int result = 0;
for (int count : dp) {
result = (result + count) % mod;
}
return result;
}
};
// 测试示例
int main() {
Solution solution;
// 示例1:m=1, n=1 → 3种
cout << solution.colorTheGrid(1, 1) << endl; // 输出:3
// 示例2:m=2, n=2 → 12种
cout << solution.colorTheGrid(2, 2) << endl; // 输出:12
// 示例3:m=3, n=1 → 3×2×2=12种
cout << solution.colorTheGrid(3, 1) << endl; // 输出:12
return 0;
}