资讯动态

状压DP解蓝桥杯矩阵计数:从2x2约束到行间递推

发布时间:2026/8/28 1:22:13 来源:尧图企业网站定制
1. 问题引入从棋盘到矩阵的计数谜题最近在整理蓝桥杯历年国赛真题时2019年那道“矩阵计数”的题目让我印象很深。它初看像是一道简单的排列组合题但稍微深入就会发现题目给出的约束条件让直接套公式变得几乎不可能。很多刚接触这类问题的朋友第一反应可能是尝试手动枚举或者写个简单的循环暴力破解但当矩阵规模稍微大一点比如题目中可能出现的 5x5 甚至更大这种方法的计算量就会爆炸完全不可行。这道题的核心是给定一个 N x M 的矩阵我们需要统计其中所有可能的 0/1 矩阵即每个格子填0或1的数量并且这个矩阵需要满足一个特定的约束条件矩阵中任意一个边长为 2 的正方形即 2x2 的子矩阵内1 的个数不能为奇数。换句话说每个 2x2 的小方块里1 的个数必须是 0、2 或 4 个。这听起来有点绕但其实它刻画了一种非常特殊的“局部一致性”关系。你可能会想这和棋盘格染色、状态压缩动态规划状压DP有什么关系这正是问题的精妙之处——它把一个全局的、看似复杂的计数问题转化为了相邻行之间状态的递推问题。我最初看到这个题也走了不少弯路。尝试用纯数学的容斥原理发现情况过于复杂想用搜索状态空间又太大。直到把问题重新表述为“每一行的摆放方式如何受上一行制约”才豁然开朗这正是指数级状态用动态规划来处理的经典场景。今天我们就来彻底拆解这道题不仅给出解法更重点分享如何想到这个解法以及在代码实现时有哪些容易踩坑的细节。无论你是正在备赛蓝桥杯还是对算法设计感兴趣相信这篇深入的分析都能给你带来启发。2. 约束的本质将全局条件转化为行间递推关系要解决这个问题我们首先要彻底理解“任意 2x2 子矩阵内 1 的个数为偶数”这个条件到底意味着什么。这是将问题模型化的关键一步很多解法讲得模糊就是因为这一步的转化没讲透。2.1 从 2x2 方块到相邻行的约束考虑一个 N 行 M 列的矩阵。我们一行一行地来构造它。假设我们已经确定了前i-1行的所有格子的值0或1现在要决定第i行的值。约束条件关注的是所有 2x2 的方块。对于一个跨越第i-1行和第i行的 2x2 方块它涉及第i-1行的某一列及其相邻列以及第i行的同一列及其相邻列。具体来说对于第i行的第j列1 j M包含它的、且涉及上一行的 2x2 方块有两个一个是以(i-1, j-1)为左上角的方块另一个是以(i-1, j)为左上角的方块。当我们放置第i行的(i, j)这个格子时我们必须同时考虑这两个方块是否满足“1的个数为偶数”的条件。而这两个方块里第i-1行的两个格子(i-1, j-1)和(i-1, j)的值是已经确定的第i行的(i, j-1)格子的值如果 j1在我们逐列放置时也已经确定了。于是放置(i, j)这个格子的值会受到其左上方三个已确定格子的值的约束。这听起来很复杂但如果我们换一种视角不是逐格放置而是逐行决定整行的状态问题会清晰很多。我们将每一行看作一个整体用一个 M 位的二进制数来表示这一行的摆放方案每一位代表一列1表示该列放10表示放0。那么约束条件“任意 2x2 子矩阵内1的个数为偶数”就可以转化为对于相邻的两行它们的二进制状态必须满足某种特定的兼容性关系。2.2 兼容性关系的数学表达设上一行的状态为prev一个 M 位的二进制数当前行的状态为curr。我们如何判断prev和curr是兼容的呢考虑矩阵中任意相邻的两行i-1和i。对于从第 1 列到第 M-1 列的每一列j我们取一个 2x2 的子矩阵其左上角为(i-1, j)。这个子矩阵包含四个格子(i-1, j),(i-1, j1),(i, j),(i, j1)。约束要求这个子矩阵中 1 的个数是偶数。用二进制位来表示设prev的第j位为a第j1位为bcurr的第j位为c第j1位为d。那么上述条件就是a b c d是偶数。这个等式(a b c d) % 2 0就是相邻两行状态prev和curr在第j列处必须满足的局部约束。注意这个约束是针对每一对相邻的列(j, j1)的。也就是说状态curr必须与状态prev在每一对相邻列上都满足这个奇偶性条件两行才是兼容的。因此我们可以预先计算出所有可能的状态对于 M 列共有2^M种状态但 M 通常不会太大题目中可能 M 5 或 6所以状态数最多 32 或 64 个是可接受的然后对于每一个状态prev找出所有与其兼容的状态curr。这样我们就将原问题转化为了一个在状态图上进行路径计数的问题从第一行的某个状态开始每次转移到下一行的一个兼容状态问走完 N 行一共有多少条不同的路径。这正是动态规划的用武之地。注意这里有一个非常重要的细节初学者很容易忽略。当我们说“所有可能的状态”时指的是所有2^M个 M 位二进制数吗是的但这里没有额外的行内约束。约束只存在于行与行之间。所以第一行可以是任何状态。这简化了初始化的步骤。3. 算法核心状态压缩动态规划状压DP的建模与实现理解了约束如何转化为状态间的兼容性我们就可以用动态规划来系统地计数了。这种方法通常被称为状态压缩动态规划因为我们用一个整数二进制位来压缩表示一行的状态。3.1 动态规划状态定义我们定义dp[i][state]表示当处理完前i行并且第i行的状态为state一个 M 位的二进制整数时满足条件的矩阵有多少种。我们的最终目标是求sum(dp[N][state])对所有可能的state求和因为最后一行可以是任何兼容的状态。3.2 状态转移方程根据前面的分析状态转移来自于上一行。转移方程非常直观dp[i][curr] sum(dp[i-1][prev])其中求和遍历所有与curr兼容的上一行状态prev。这里的关键就是如何高效地判断两个状态prev和curr是否兼容。根据 2.2 节的公式我们需要检查从第 0 列到第 M-2 列假设列索引从0开始的每一对相邻列。3.3 兼容性判断的实现技巧我们可以写一个函数bool is_compatible(int prev, int curr, int M)来实现判断。具体方法是遍历每一列j(0 j M-1)分别取出prev和curr在第j位和第j1位的值计算abcd的奇偶性。一个更高效的方法是预处理兼容性关系。由于总状态数S 2^M不大我们可以用一个S x S的布尔矩阵compatible[prev][curr]来记录任意两个状态是否兼容。在动态规划开始前用双重循环计算出这个矩阵这样在 DP 转移时查询兼容性就是 O(1) 的操作极大地提升了效率。预处理代码如下以 C 为例int S 1 M; // 状态总数 vectorvectorbool compatible(S, vectorbool(S, false)); for (int prev 0; prev S; prev) { for (int curr 0; curr S; curr) { bool ok true; for (int j 0; j M-1; j) { // 提取 prev 和 curr 在第 j 和 j1 位的值 int a (prev j) 1; int b (prev (j1)) 1; int c (curr j) 1; int d (curr (j1)) 1; if ((a b c d) % 2 ! 0) { // 如果是奇数 ok false; break; } } compatible[prev][curr] ok; } }3.4 动态规划过程实现初始化第一行可以是任何状态因为没有上一行来约束它。所以dp[1][state] 1对于所有state(0 state S) 成立。 然后从第二行开始递推vectorvectorlong long dp(N1, vectorlong long(S, 0)); // 初始化第一行 for (int s 0; s S; s) { dp[1][s] 1; } // 递推第2行到第N行 for (int i 2; i N; i) { for (int curr 0; curr S; curr) { long long sum 0; for (int prev 0; prev S; prev) { if (compatible[prev][curr]) { sum dp[i-1][prev]; } } dp[i][curr] sum; } } // 计算结果 long long ans 0; for (int s 0; s S; s) { ans dp[N][s]; }这里使用long long是因为结果可能非常大超出int范围。3.5 空间优化滚动数组注意到dp[i][curr]只依赖于dp[i-1][prev]因此我们可以使用滚动数组来将空间复杂度从 O(N * S) 优化到 O(S)。这是竞赛中的常见技巧。我们只需要两个一维数组dp_curr和dp_prev分别表示当前行和前一行的 DP 值。vectorlong long dp_prev(S, 1); // 第一行所有状态方案数为1 vectorlong long dp_curr(S, 0); for (int i 2; i N; i) { fill(dp_curr.begin(), dp_curr.end(), 0); // 清空当前行 for (int curr 0; curr S; curr) { for (int prev 0; prev S; prev) { if (compatible[prev][curr]) { dp_curr[curr] dp_prev[prev]; } } } swap(dp_curr, dp_prev); // 当前行变成下一轮的前一行 } // 最终答案在 dp_prev 中 long long ans accumulate(dp_prev.begin(), dp_prev.end(), 0LL);使用滚动数组后空间消耗大大减少尤其当 N 很大时优势明显。4. 复杂度分析与算法优化思考在实现算法后我们必须评估其效率并思考是否有优化空间。这对于在竞赛中应对更大数据范围至关重要。4.1 时间复杂度分析我们的算法主要分两步预处理兼容性矩阵双重循环遍历所有状态对(prev, curr)并对每个状态对检查 M-1 列。时间复杂度为 O(S^2 * M)其中 S 2^M。动态规划递推需要处理 N-1 行第一行已初始化对于每一行需要遍历当前状态curr并对每个curr遍历所有前一状态prev进行累加。时间复杂度为 O(N * S^2)。由于 S 是 2^M所以总时间复杂度为 O(N * 4^M * M)。这个复杂度对于 M 很小的情况是可行的。例如如果 M5则 S32S^21024即使 N 达到 1000计算量也在千万级别现代计算机可以在短时间内完成。但如果 M 达到 10S1024S^2 就超过一百万再乘以 N 就可能超时。4.2 针对更大 M 的优化思路原题通常 M 较小但作为思维拓展我们可以考虑如果 M 变大怎么办。O(S^2) 的转移是瓶颈。能否优化观察兼容性条件(abcd) % 2 0。这等价于(ab) % 2 (cd) % 2吗不完全是因为(ab)和(cd)同奇偶时它们的和才是偶数。实际上(abcd)为偶数等价于(ab)与(cd)奇偶性相同。但这对我们优化转移帮助不大。一个更深入的观察是这个约束是按位线性的。(abcd) % 2 0等价于a ⊕ b ⊕ c ⊕ d 0这里 ⊕ 表示异或。这是一个线性方程。对于所有 M-1 个相邻列我们得到了一个由 M-1 个线性方程构成的方程组变量是prev和curr的每一位。这提示我们兼容的状态curr其实是由prev通过一个线性变换决定的。具体来说对于给定的prev我们可以直接推导出哪些curr是合法的。将prev的每一位看作已知量约束方程a ⊕ b ⊕ c ⊕ d 0可以改写为c ⊕ d a ⊕ b。这意味着对于每一对相邻列(j, j1)当前行在这两列的异或值(c ⊕ d)必须等于上一行对应两列的异或值(a ⊕ b)。这给了我们一个逐位递推curr的方法如果我们确定了curr的第一位最左边一列那么根据c0 ⊕ c1 a0 ⊕ a1可以推出c1再根据c1 ⊕ c2 a1 ⊕ a2可以推出c2以此类推。也就是说给定prev合法的curr最多只有 2 种取决于curr第一位的选择是0还是1这是一个巨大的优化。原来我们需要遍历所有 S 个prev和 S 个currS^2 级别现在对于每个prev只需要检查最多 2 个curr。这样预处理兼容性矩阵的复杂度从 O(S^2 * M) 降到了 O(S * M)DP 转移的复杂度也从 O(N * S^2) 降到了 O(N * S)。这对于 M 较大的情况比如 M15S32768是可行的。实操心得在竞赛中如果时间充裕可以先实现通用的 O(S^2) 方法因为它思路直观编码简单对于小规模数据绝对够用。如果提交后发现超时或者题目明确 M 可能较大再考虑实现这种基于线性约束的优化。在考场上正确性优先优化次之。5. 代码实现全解与关键细节剖析理论清晰之后我们来看完整的代码实现。我会提供两个版本的代码基础版通用 O(S^2) 方法和优化版利用线性性质 O(S) 转移。我们假设输入为 N 和 M输出为满足条件的矩阵总数。5.1 基础版实现通用方法#include iostream #include vector #include numeric // for accumulate using namespace std; int main() { int N, M; // 假设从标准输入读取 N 和 M // cin N M; // 为了示例我们假设 N3, M3 N 3; M 3; int S 1 M; // 状态总数 // 1. 预处理兼容性矩阵 vectorvectorbool compatible(S, vectorbool(S, false)); for (int prev 0; prev S; prev) { for (int curr 0; curr S; curr) { bool ok true; for (int j 0; j M-1; j) { int a (prev j) 1; int b (prev (j1)) 1; int c (curr j) 1; int d (curr (j1)) 1; if ((a b c d) % 2 ! 0) { ok false; break; } } compatible[prev][curr] ok; } } // 2. 动态规划使用滚动数组优化空间 vectorlong long dp_prev(S, 1); // 第一行任何状态都是1种 vectorlong long dp_curr(S, 0); for (int i 2; i N; i) { fill(dp_curr.begin(), dp_curr.end(), 0LL); for (int curr 0; curr S; curr) { for (int prev 0; prev S; prev) { if (compatible[prev][curr]) { dp_curr[curr] dp_prev[prev]; } } } swap(dp_curr, dp_prev); } // 3. 统计答案 long long ans accumulate(dp_prev.begin(), dp_prev.end(), 0LL); cout Number of valid matrices: ans endl; // 对于 N3, M3输出应为 36 return 0; }关键细节剖析位运算技巧(state j) 1是提取整数state二进制表示中第j位从0开始最低位为第0位的标准操作。务必熟悉。兼容性判断的循环边界for (int j 0; j M-1; j)这里j最大到M-2因为我们要取j和j1列。初始化dp_prev初始化为1代表第一行任意摆放都只有1种方式即它本身。这是正确的因为第一行没有上一行约束它。结果类型使用long long是必要的。对于 N 和 M 都为 5 的情况结果可能已经很大。滚动数组的swapswap(dp_curr, dp_prev)比重新分配内存更高效。它使得dp_prev始终指向“已完成的前 i 行”的结果。5.2 优化版实现利用线性性质这个版本展示了如何将转移复杂度从 O(S^2) 降到 O(S)。关键在于对于每个prev我们不再遍历所有curr而是直接生成最多2个合法的curr。#include iostream #include vector #include numeric using namespace std; int main() { int N, M; // cin N M; N 3; M 3; int S 1 M; // 1. 对于每个prev预先计算出所有合法的next状态 vectorvectorint legal_next(S); for (int prev 0; prev S; prev) { // 尝试curr的第一位为0和1两种情况 for (int first_bit 0; first_bit 1; first_bit) { int curr first_bit; // 当前状态先确定第一位 bool valid true; // 根据约束方程逐位推导curr的后续位 for (int j 0; j M-1; j) { int a (prev j) 1; int b (prev (j1)) 1; int c (curr j) 1; // 约束: a ^ b ^ c ^ d 0 d a ^ b ^ c int d a ^ b ^ c; // 计算curr的第j1位 // 检查推导出的d是否与已确定的位冲突当j0时curr的第j1位可能已被上一轮推导出 // 实际上我们是从左到右推导所以不会冲突只需要检查边界。 // 将d设置到curr的第j1位 if (d 1) { curr | (1 (j1)); } else { curr ~(1 (j1)); // 清零操作更安全的写法是重新构建curr } } // 注意上面的循环结束后curr的每一位都已根据prev和first_bit确定。 // 但是我们需要验证这个curr是否真的满足所有约束实际上我们的推导过程保证了它满足。 // 不过有一个潜在问题当M较大时curr可能超出S范围吗不会因为推导出的位都在0~M-1范围内。 // 将合法的curr加入列表 legal_next[prev].push_back(curr); // 注意如果first_bit0和1推导出的curr相同可能在某些对称情况下需要去重。 // 但在这个问题中prev固定时first_bit取0和1通常会产生两个不同的curr除非prev有特殊对称性。 // 为简单起见我们先都加入最后在DP累加时不会重复计算因为是从同一个prev转移来的。 } // 简单去重可选但严谨起见最好做 sort(legal_next[prev].begin(), legal_next[prev].end()); legal_next[prev].erase(unique(legal_next[prev].begin(), legal_next[prev].end()), legal_next[prev].end()); } // 2. 动态规划 vectorlong long dp_prev(S, 1); vectorlong long dp_curr(S, 0); for (int i 2; i N; i) { fill(dp_curr.begin(), dp_curr.end(), 0LL); for (int curr 0; curr S; curr) { // 这里我们反过来遍历对于每个curr哪些prev能转移到它 // 但我们的legal_next是prev-next的映射。为了高效我们可以改变循环方式。 // 方式A遍历prev然后更新它的所有next。 for (int prev 0; prev S; prev) { for (int next_state : legal_next[prev]) { dp_curr[next_state] dp_prev[prev]; } } // 注意这种方式会导致dp_curr被多个prev更新是可行的。 // 但更高效的方式是像基础版那样对于每个curr找所有prev。这需要建立反向映射。 // 为了代码清晰我们暂时用方式A其复杂度为 O(N * S * K)其中K是每个prev的平均合法next数2所以是O(N*S)。 } swap(dp_curr, dp_prev); } long long ans accumulate(dp_prev.begin(), dp_prev.end(), 0LL); cout Number of valid matrices (optimized): ans endl; return 0; }优化版关键点推导过程核心是公式d a ^ b ^ c。给定prev的a, b和curr的当前位c就能唯一确定下一位d。我们从第一位开始就可以推导出整个curr。去重first_bit取 0 和 1 可能产生相同的curr虽然不常见但理论上可能。使用sort和unique去重是良好习惯。DP 转移的两种视角我们建立了从prev到next的映射legal_next。在 DP 迭代时既可以遍历prev去更新它的每个next方式A也可以预先建立从next到prev的反向映射然后遍历curr去累加prev。方式A更直接但注意内层循环次数是S * avg(K)由于avg(K) 2所以是 O(S) 级别仍然比 O(S^2) 好。正确性验证对于小规模数据如 N,M 4可以用基础版和优化版分别计算对比结果是否一致这是调试和验证算法正确性的好方法。6. 测试、验证与典型错误排查写完代码并不意味着结束尤其是对于计数问题一个小的边界错误就可能导致结果天差地别。这里分享一套我常用的测试和调试方法。6.1 构造小规模测试用例最可靠的测试是手算小数据与程序输出对比。N1, M1矩阵只有1个格子。任意填0或1没有2x2子矩阵所以所有情况都满足。总方案数 2^1 2。程序应输出2。N1, M2矩阵是1x2。同样没有2x2子矩阵。总方案数 2^2 4。程序应输出4。N2, M2矩阵是2x2刚好构成一个2x2子矩阵。我们需要这个子矩阵中1的个数为偶数。列出所有16种可能每个格子0/1手工数出满足条件的全01种两个1C(4,2)6种四个11种。总共 161 8 种。程序应输出8。N2, M3可以手工推导或者用更可靠的方法写一个简单的暴力枚举程序仅用于小数据验证遍历所有2^(2*3)64种矩阵直接检查每个2x2子矩阵。将结果与你的DP程序对比。暴力验证程序片段用于 N2, M3 等小数据#include iostream using namespace std; int main() { int N2, M3; int total 0; int total_states 1 (N*M); // 总状态数 for (int s 0; s total_states; s) { // 将状态s解码为矩阵 int mat[2][3]; for (int i0; iN; i) { for (int j0; jM; j) { int idx i*M j; mat[i][j] (s idx) 1; } } // 检查所有2x2子矩阵 bool valid true; for (int i0; iN-2 valid; i) { for (int j0; jM-2 valid; j) { int sum mat[i][j] mat[i][j1] mat[i1][j] mat[i1][j1]; if (sum % 2 ! 0) { valid false; } } } if (valid) total; } cout Brute force result: total endl; return 0; }将你的DP程序结果与暴力枚举结果对比如果一致信心会大增。6.2 典型错误与排查点列循环边界错误在兼容性判断中循环应该是for (int j0; j M-1; j)。如果错写成j M会访问越界j1当 jM-1 时越界。这是最常见的错误之一。位索引混淆二进制位索引通常从0开始最低位。(prev j) 1取到的是从右往左数第j位。要确保这个顺序和你心中“矩阵的列”的对应关系是一致的。通常我们可以约定状态整数的最低位第0位对应矩阵的最右边一列或者反过来。只要在整个程序中保持一致即可。不一致会导致结果错误。初始化错误dp[1][state]应该初始化为1表示第一行放置成state这种状态有1种方法。有人错误地初始化为state中1的个数之类的这是不对的。整数溢出这是计数DP的经典陷阱。即使 N 和 M 很小结果也可能增长很快。务必使用long long在C中或更高精度的整数类型。如果题目模一个数也要在每一步加法后取模。去重问题优化版中在优化版中如果不对legal_next[prev]进行去重当first_bit0和1产生相同的curr时会导致从同一个prev到同一个curr的转移被重复计算两次使结果偏大。虽然这种情况不总是发生但为了鲁棒性一定要去重。滚动数组忘记清零在每一轮新的DP迭代开始前dp_curr必须全部清零fill(dp_curr.begin(), dp_curr.end(), 0LL)否则会累加上一轮的数据。6.3 对拍测试对于更复杂的情况可以写一个“数据生成器”生成随机的、但规模较小的 N 和 M比如 N, M 都在1到5之间然后同时运行你的DP程序和暴力枚举程序比较两者的输出。运行几百上千组如果全部一致那你的DP程序基本可以认为是正确的。这是竞赛中验证算法正确性的黄金标准。7. 举一反三同类问题与思维拓展解决了“矩阵计数”我们掌握了用状态压缩DP处理网格递推计数问题的一套方法论。这套方法可以应用到许多类似问题中。7.1 同类问题模式识别当你看到以下特征的问题时就要考虑状压DP了场景在一个网格棋盘上摆放东西填数、放棋子、染色等。约束约束通常是局部的只涉及相邻的有限个格子比如相邻两行、相邻的2x2区域。目标统计满足所有约束的摆放方案总数。规模网格的行数 N 可能较大几十到几百但列数 M 较小通常 10~15因为状态数是 2^M。例如经典铺砖问题用 1x2 或 2x1 的骨牌铺满 N x M 的网格求方案数。约束是“每一行被覆盖的状态由上一行未覆盖的格子决定”。炮兵阵地在 N x M 的网格上放炮兵炮兵攻击范围是上下左右两格求最多能放多少炮兵或方案数。约束是相邻三行之间不能互相攻击。互不侵犯在 N x N 的棋盘上放 K 个国王国王攻击周围8格求方案数。约束是相邻两行状态不能冲突。7.2 从“矩阵计数”到更复杂约束“矩阵计数”的约束是 2x2 子矩阵和偶。我们可以变化约束和为奇数只需将判断条件(abcd) % 2 ! 0改为 1。和等于某个特定值 k比如要求每个 2x2 子矩阵的和恰好为 2。这时兼容性判断就不再是简单的奇偶性而是精确的等式abcd k。注意k 只能是 0,1,2,3,4。对于不同的 k合法状态对(prev, curr)会不同。更大范围的约束比如约束 3x3 子矩阵的和为偶数。这时状态就需要表示连续两行甚至三行的信息状态压缩的维度会变高状态数变为 2^(2M) 或 2^(3M)对 M 的限制就更严格。7.3 优化思路的迁移我们在第4节提到的将约束转化为线性方程并直接推导合法后续状态的方法是一种很强的优化。它适用于约束是线性等式的情况例如奇偶性约束本质是模2加法下的线性方程。对于更复杂的非线性约束比如和等于一个固定值这种优化可能就不适用了只能使用通用的 O(S^2) 转移。这时问题的可解范围就由 M 的大小决定。通常竞赛题会保证 M 足够小使得 O(S^2) 算法可行。7.4 个人实战心得最后分享几点我在解决这类问题时的个人体会画图是王道在理解约束条件时一定要在纸上画一个小的矩阵比如 3x3标上行列手动模拟一下“上一行状态固定时下一行哪些格子能填1哪些不能填”。这个直观的过程能帮你最快地抓住约束的本质比空想公式有效得多。先暴力后优化对于小数据N, M 4先写一个暴力搜索或枚举程序。它的目的有两个一是验证你对题意的理解是否正确你能正确数出结果吗二是为你的DP程序提供可靠的测试用例。状态设计要大胆不要害怕状态数多。只要 M 122^124096 在现代计算机上完全不是问题。先设计出正确但可能稍慢的DP确保逻辑正确。优化比如滚动数组、预处理兼容性往往是后续锦上添花的事情。注意模运算很多计数问题要求结果对一个大质数如1e97取模。务必在每一步加法后就取模防止中间结果溢出。即使你用了long long也可能在多次累加后溢出。调试输出中间状态如果结果不对可以输出dp数组中间几行的值或者输出compatible矩阵看看是否和你手算的小规模情况一致。对于 DP 问题从最基础的情况N1开始验证逐步增加 N是定位错误的好方法。回过头看“矩阵计数”这道题它完美地体现了状压DP的经典思维模式将复杂的全局约束转化为相邻单元行之间的局部约束用二进制状态压缩一行的情况通过预处理状态间的转移关系最终用动态规划在状态空间中进行路径计数。掌握这个套路你就能解决一大类网格计数问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价