真正省时间的关键是填入数字后立即更新受影响格子候选数并空集时回溯;用std::array以位图存候选状态,初始全1,填val时对同行列宫未填格执行candidates[i] &= ~(1

回溯框架里怎么嵌入候选数更新逻辑
不维护候选数的回溯,每填一个格子都要遍历整行、整列、整个宫去检查合法性,效率低。真正省时间的关键是:每次填入数字后,立刻更新受影响格子的候选数集合,且一旦某个格子候选数变为空,就立即回溯。
典型做法是用 std::array<:bitset>, 81></:bitset> 存每个格子的候选状态(bit 0 表示数字 1 是否可用),初始全为 0b111111111;填入 val(值为 1–9)时,对同行、同列、同宫所有未填格,执行 candidates[i] &= ~(1 。
注意:必须在递归前更新、回溯后还原——否则多层递归会污染共享状态。
- 更新操作要封装成函数,比如
remove_candidate(int row, int col, int val),避免手写三重循环出错 - 还原时不能简单赋值原值,得用栈存变更记录(如
vector<tuple>></tuple>记录「位置+被清除的数字」),否则剪枝失效 - 候选数数组建议用指针或引用传入递归函数,别拷贝——81×sizeof(bitset) ≈ 1KB,拷贝开销明显
什么时候该触发剪枝而不是继续递归
候选数剪枝不是等填完才判断,而是在每次更新后立刻检查三个终止条件:
- 存在某个空格的
candidates[i].count() == 0→ 当前路径无解,直接 return false - 存在某个空格的
candidates[i].count() == 1→ 必填,立刻填入并继续更新(即“强制单候选”推进,这步能大幅减少搜索分支) - 所有空格都已填满 → 找到解,return true
第二点容易被忽略:不主动做这个“确定性填充”,就退化成纯回溯,性能差一个数量级。实现上建议用 while 循环反复扫描,直到没有单候选格再进递归分支。
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
std::bitset 和布尔数组在候选数场景下的实际差异
用 bool candidates[81][9] 也能做,但位运算优势明显:
-
candidates[i].count()是 O(1) 统计剩余候选数,而布尔数组得循环 9 次 -
candidates[i] & candidates[j]可快速求交集(用于高级剪枝如隐性唯一候选),布尔数组没这能力 - 内存更紧凑:
bitset占 2 字节,9 个 bool 至少占 9 字节(可能因对齐更多)
不过要注意:MSVC 的 std::bitset::count() 在 debug 模式下可能不内联,影响性能;若追求极致,可预存一个查表数组 popcount[512],用 popcount[candidates[i].to_ullong()] 替代。
常见崩溃和逻辑错误点
这类求解器最常在边界和状态还原上翻车:
- 行列索引算错:C++ 数组是 [row][col],但数独坐标常按 (r,c) 输入,宫号计算写成
(r/3)*3 + c/3就错了,正确是(r/3)*3 + c/3没问题,但遍历时用for (int i = base_r; i 忘了 <code>base_r = (r/3)*3就越界 - 还原候选数时漏掉某个格子:尤其同宫更新有 9 个格子,手写 for 容易少迭代一次
- 递归前没检查是否已填值:对已填格调用
remove_candidate会误清其他格候选数 - 输入含非法数字(0 或 >9)没校验,导致
val-1下标越界
建议在读入后加一层验证,并在每次 remove_candidate 前 assert(val >= 1 && val );调试时把候选数数组打印成 9×9 矩阵,比看内存 dump 直观得多。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










