🎯 本章要解决什么?给各种“填坑”问题找个通用解法
想象以下场景:
- 教务处老师:排课表(课程不冲突、老师不跑场、教室够用);
- 宿舍管理员:调宿舍(不打呼噜的在一起、不同班的混住、上下铺分配);
- 装修队长:买材料(颜色搭配、预算不超、尺寸合适);
- 数独爱好者:填数字(每行每列每宫1-9不重复);
这些看似不同的问题,其实都是同一类问题: 有一堆“坑”(变量)要填,每个“坑”有可选“土”(值域),填的时候要满足一堆“施工要求”(约束)。
本章教你:如何系统性地“填坑”,而不是靠蒙。
一、CSP是什么?拆解“填坑三要素”
📦 三个核心零件
任何CSP问题都包含这三样:
| 零件 | 专业术语 | 中国化比喻 | 数独例子 |
|---|---|---|---|
| 要填的坑 | 变量 | 待决定的事项 | 81个格子 |
| 能用的土 | 值域 | 每个坑能填的材料 | 每个格子可填1-9 |
| 施工规范 | 约束 | 填坑时必须遵守的规矩 | 行/列/宫不重复 |
🏗️ 形式化表示(以排课表为例)
变量:{语文课, 数学课, 英语课, 物理课...}
值域:每个课的可选时间 = {周一1-2节, 周一3-4节...}
约束:
1. 语文老师不能同时上两门课(不同课时间不能相同)
2. 教室容量要够(课的学生数 ≤ 教室容量)
3. 体育课不能排在上午第一节(特定值限制)
关键特点:
- 不关心顺序:先排数学还是先排语文,最终结果一样;
- 只关心最终分配:每个“坑”都填上合适的“土”;
- 约束可能很复杂:两个变量之间(A≠B),或多个变量之间(A+B+C=10);
二、约束传播:提前排除“肯定填不对的土”
🎯 核心思想:别等到最后才发现矛盾
就像玩扫雷:
- 笨方法:随便点,点到雷就死;
- 聪明方法:根据数字推算哪些肯定是雷,先插旗;
📐 一致性检查的四个层次
1. 节点一致性(NC):每个坑自己的要求。
“体育课不能排上午第一节”
→ 从体育课的值域里直接删除“周一第1节”
检查:每个变量单独看,值域里的值都满足自己的一元约束。
2. 弧一致性(AC):两个坑之间的互相限制。
“张老师不能同时上语文和数学”
语文课选了“周一1-2节”
→ 数学课的值域里删除“周一1-2节”
检查:对于变量X的每个可能取值,变量Y都至少有一个值能与它配对成功。
AC-3算法(最常用):
def AC_3(所有约束):
队列 = 所有约束边 # 如(语文课,数学课)
while 队列非空:
(X,Y) = 队列.pop()
if 修订(X的域, 基于Y): # 删掉X中无法和Y任何值配对的值
for Z in X的所有邻居: # 所有和X有约束的变量
队列.add((Z,X)) # Z可能也要重新检查
生活比喻:
- 你妈:“周末要么去姥姥家,要么去奶奶家”;
- 你爸:“周末必须去奶奶家”;
- 弧一致性检查:
- 你的值域{姥姥家,奶奶家};
- 你爸的值域{奶奶家};
- 检查:你能选“姥姥家”吗?不能,因为和你爸的“奶奶家”冲突 → 删除“姥姥家”;
- 结果:你只能选“奶奶家”;
3. 路径一致性(PC):通过第三个坑传递的限制。
A说:“我和B不同班”
B说:“我和C同班”
虽然没有直接约束,但可推出:A和C一定不同班!
检查:对于每对值(Xi,Xj),存在一条路径通过其他变量使得约束都能满足。
4. k一致性:更广义的“连坐检查”
- 1一致性 = 节点一致性;
- 2一致性 = 弧一致性;
- 3一致性 = 路径一致性;
- k一致性:任意k-1个变量赋值后,第k个变量都能找到兼容值;
三、回溯搜索:系统性地“试错填坑”
🔍 基本回溯:一条路走到黑,不行就回头
def 回溯搜索(当前分配):
if 所有变量都分配完了:
return 当前分配
选一个未分配的变量X
for 每个值 in X的值域:
if 值满足所有与已分配变量的约束:
把(X=值)加入当前分配
结果 = 回溯搜索(当前分配)
if 结果不是失败:
return 结果
从当前分配中移除(X=值) # 回溯!
return 失败
比喻:就像走迷宫用手摸着右墙走:
- 一直往前走;
- 碰壁就退回到上一个岔路口;
- 换另一条路;
- 重复直到出口;
✨ 智能回溯:别傻退,找到“真凶”再退
问题:基本回溯经常“冤枉好人”。
你填数独:
第1步:A格填1
第2步:B格填2
...
第50步:Z格发现无值可填
基本回溯:退到第49步 → 不对 → 退到48步 → ... 一直退到第1步
但实际上,可能第30步的某个选择就错了!
向后看(Backjumping):直接跳到矛盾源头。
发现Z格无值可填时:
检查:Z和哪些已赋值变量有约束?
发现:Z只和P、Q、R三个格有约束
而P、Q、R是在第30步赋值的
→ 直接跳回第30步重试,跳过中间的20步!
约束学习:吃一堑长一智。
第一次尝试:A=1, B=2 → 失败
学习到:(A=1且B=2)是死路
第二次尝试时,看到A=1,就直接不让B=2了
四、变量和值排序:先填哪个坑?先用哪种土?
📊 变量排序:从“最难搞的坑”开始
原则:先填限制最多的变量(最少剩余值启发式)。
排课表时:
- 体育课:只能排下午(值域小)
- 语文课:全天都可排(值域大)
先排体育课!排好了它,其他课的选择就少了。
度启发式:先填涉及约束最多的变量。
数独中:
- 角落的格子:只受2行2列影响(约束少)
- 中间的格子:受2行2列2宫影响(约束多)
先填中间的!
🎨 值排序:先试“最可能成功的土”
原则:先试给邻居留最多选择的值(最少约束值启发式)。
你选宿舍:
- 选1楼:楼上楼下都还能住人
- 选3楼:只有2楼和4楼受影响
先选1楼,给后面同学留更多选择。
五、局部搜索:当“全盘规划”太慢时
🔄 从“一次性填完”到“边改边优化”
适用场景:
- 课表大体可用,但有少量冲突;
- 数独填完了,但有几处错误;
🎯 最小冲突启发式
def 最小冲突算法():
随机给所有变量赋值(可能违反约束)
for 迭代很多次:
if 所有约束都满足:
return 解
选一个冲突的变量X # 比如时间冲突的课
给X换一个值,使得冲突最少 # 换个时间,让冲突课变少
比喻:调解宿舍矛盾
- 初始随机分配(A和B打呼噜的住一起了);
- 发现冲突:A被B吵得睡不着;
- 尝试调整:
- 把A换到C宿舍:C也打呼噜 → 冲突数不变;
- 把A换到D宿舍:D不打呼噜 → 冲突减少;
- 选择减少冲突最多的调整
优点:特别适合几乎已经解完的问题微调。
六、问题结构:利用“地形”加速求解
🌳 树状结构CSP:有福了!
如果约束关系形成一棵树:
A — B — C — D # 线性链
而不是:
A — B — C
\ /
D # 有环
好消息:树状CSP可以在O(nd²)时间内解决(n变量,d值域大小) 方法:拓扑排序后,从叶子开始向前或向后传播。
✂️ 割集调整:把环切成树
原问题:A—B—C—A(三角形,有环)
选一个变量作为“牺牲品”:比如固定A=某个值
剩下:B—C(变成链,可快速解)
然后尝试A的其他值...
比喻:解连环套
- 硬解:每个环都试;
- 聪明:剪断一个环,解开链,再接回去;
🧩 树分解:把大图拆成小树
复杂约束图 → 拆成几个簇
每个簇内部变量多但簇间形成树
分别解每个簇,再组合
七、实战算法选择指南
| 问题特征 | 推荐算法 | 生活比喻 |
|---|---|---|
| 小规模(<100变量) | 回溯+约束传播 | 手工排课表,先排限制多的课 |
| 约束很紧(值域小) | 约束传播为主 | 数独最后几个格,用排除法 |
| 约束较松(值域大) | 回溯搜索为主 | 选选修课,很多时间可选 |
| 近似解也可接受 | 局部搜索(最小冲突) | 调宿舍微调,解决个别矛盾 |
| 约束关系是树状 | 拓扑排序+前向检查 | 流水线工序安排 |
| 有对称性 | 打破对称性(如固定一个值) | 分配相同教室时,先定一个 |
🎮 经典案例:数独求解器怎么工作
🧠 人类解法 vs 算法解法
人类:用“这个格子只能填5”、“这行缺3和8”等启发式 算法:把人类启发式形式化
🔢 数独的约束传播
- 节点一致性:已知数字的格子,值域固定;
- 弧一致性(AC-3):
格子(1,1)=5 → 第1行所有其他格子删除5 → 第1列所有其他格子删除5 → 左上宫所有其他格子删除5 - 递归回溯:当传播不动时,选一个格子猜数字。
⚡ 优化技巧
- MRV:先填可选数字最少的格子;
- 向前检查:赋值后立即更新邻居的值域;
- 智能回溯:冲突时直接跳到相关格子;
💡 CSP思维:从“填坑”到“设计坑”
逆向应用:设计容易解的问题
如果你想设计一个好解的数独:
- 给的初始数字要能充分约束(用约束传播能推到很多格);
- 避免需要大量猜测回溯;
- 最好有唯一解;
CSP的哲学启示
- 约束不是敌人,是向导:约束越紧,搜索空间越小;
- 顺序很重要:先处理最难的部分;
- 局部冲突可以局部解决:不一定全盘重来;
- 结构决定难度:树状问题简单,全连接问题难;
最后一句大实话:
生活中很多问题都是CSP:找对象(互相看对眼)、组团队(技能互补)、安排假期(家人时间都合适)。
学会系统性地“填坑”,而不是凭感觉乱试,成功率会高很多。
3万+

被折叠的 条评论
为什么被折叠?



