网格涂色问题:动态规划与状态压缩的巧妙结合

题目链接1931. 用三种不同颜色为网格涂色 - 力扣(LeetCode)

一、题目分析

题目描述

给定一个 m 行 n 列的网格,每个格子需涂红、绿、蓝三种颜色之一。要求:

  1. 同一行内相邻格子颜色不同
  2. 相邻行的同一列格子颜色不同
    求满足条件的涂色方案数,结果对 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 的方案数为 1f[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);
    }
}
  • 三进制分解:通过取模和除法提取每一位颜色(012 分别代表红、绿、蓝)。
  • 合法性检查:遍历行内相邻元素,确保无同色相邻。

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 超过百万,需进一步优化(如矩阵快速幂优化动态规划转移)。

六、总结

本题通过状态压缩将行状态转化为整数,结合动态规划和邻接表预处理,高效解决了行列约束下的涂色问题。核心思路是:

  1. 状态压缩:用三进制数表示行颜色,快速筛选合法行。
  2. 邻接表预处理:提前存储合法状态转移关系,避免动态规划时重复判断。
  3. 滚动数组优化:利用动态规划的无后效性,节省空间复杂度。

该解法适用于 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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

.wei-upup

如若对您有用,盼您赏个鼓励~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值