想象你被困在一个巨大的迷宫里,目标是找到出口。
第一次尝试,你随便选了个方向走,结果走进了死胡同。于是你退回来,换了个方向。但走着走着,你突然发现:"咦?这个地方我好像来过!"
如果你什么都不记录,就会像无头苍蝇一样,在同样的地方打转,永远找不到出口。
聪明的做法是什么?带一支笔和一个本子!
每次走过一个岔路口,就在本子上记下来:
-
✍️ "第一个路口向左 ❌ 死路" -
✍️ "第二个路口向右 ❌ 绕圈" -
✍️ "第三个路口直走 ✅ 可以试试"
下次再经过这些地方,看看本子:"哦,这条路我试过了,不行!换一条!"
恭喜你!你刚刚发明了计算机科学中的一个经典算法——禁忌搜索(Tabu Search)!
什么是禁忌搜索?🤔
禁忌搜索是爬山法的升级版。还记得爬山法吗?它像个近视眼,只往高处走,容易卡在小山包上。
禁忌搜索的核心改进就是:给算法装了一个记忆系统!
核心思想:三个关键词
1. 记忆(Memory) 📝
维护一个"禁忌列表"(Tabu List),记录最近访问过的解或操作。就像你的迷宫笔记本。
2. 禁忌(Tabu) 🚫
如果某个解或操作在禁忌列表里,短时间内就不能再选它了。避免重复搜索和原地打转。
3. 特赦(Aspiration) ⭐
但如果某个被禁忌的解特别好(比现在找到的最好解还要好),可以"破例"接受它。这叫"特赦准则"。
算法原理:五个关键步骤 🎯
让我们把禁忌搜索拆解成简单的步骤:
第一步:初始化 🎬
-
选择一个起始解(随机或根据经验) -
创建一个空的禁忌列表 -
设置禁忌列表的长度(比如记录最近10次的操作)
第二步:生成邻居(邻域解) 👥
从当前解出发,生成所有可能的邻居解。这和爬山法一样。
比如在旅行商问题中,邻居可以是:交换路线中任意两个城市的顺序。
第三步:评估和筛选 🔍
对每个邻居解进行评估,但要注意:
-
如果某个邻居(邻域解)的"操作"在禁忌列表里,通常不能选 -
除非它满足"特赦准则"(比最好解还好)
第四步:更新状态 📊
-
选择最好的可行邻域解(可能比当前解更差!这是关键!) -
把这次的操作加入禁忌列表 -
如果禁忌列表满了,删除最老的记录(先进先出) -
如果找到了更好的解,更新"全局最优解"
第五步:重复或终止 🔄
继续第二步,直到:
-
达到最大迭代次数 -
连续多次迭代都没改进 -
找到了满意的解
禁忌搜索 vs 爬山法:关键区别 ⚖️
让我用一个表格说明两者的差异:
|
|
|
|
|---|---|---|
| 记忆能力 |
|
|
| 是否接受更差的解 |
|
|
| 陷入局部最优 |
|
|
| 搜索策略 |
|
|
| 计算复杂度 |
|
|
| 解的质量 |
|
|
最关键的区别:禁忌搜索允许接受比当前解更差的邻居(邻域解)!
这听起来很反直觉,但正是这个特性让它能跳出局部最优。就像爬山时,有时候要先下坡,才能爬到更高的山峰。
实战案例:帮小明选课 📚
小明是大学生,这学期要选4门课。每门课有不同的时间段,有些会冲突。小明想要:
-
课程尽量集中(不想一天跑好几趟学校) -
避开早上8点的课(起不来) -
尽量选有趣的课
我们用禁忌搜索帮他优化选课方案!
import random
class Course:
"""课程类"""
def __init__(self, name, time_slot, interest_score):
self.name = name # 课程名称
self.time_slot = time_slot # 时间段(1-5代表周一到周五,每天分上午下午)
self.interest_score = interest_score # 兴趣分数(1-10)
def __repr__(self):
days = {1:'周一上', 2:'周一下', 3:'周二上', 4:'周二下',
5:'周三上', 6:'周三下', 7:'周四上', 8:'周四下',
9:'周五上', 10:'周五下'}
returnf"{self.name}({days[self.time_slot]}, 兴趣度{self.interest_score})"
def evaluate_schedule(schedule, courses):
"""
评估一个选课方案的好坏
返回分数(越高越好)
"""
if len(schedule) == 0:
return0
selected_courses = [courses[i] for i in schedule]
# 计算兴趣总分
interest_total = sum(c.interest_score for c in selected_courses)
# 计算课程分布分数(越集中越好)
time_slots = [c.time_slot for c in selected_courses]
unique_days = len(set([t // 2for t in time_slots])) # 多少天有课
concentration_score = 10 - unique_days * 2# 天数越少越好
# 检查是否有早8课(惩罚)
has_early_class = any(c.time_slot in [1, 3, 5, 7, 9] for c in selected_courses)
early_penalty = -5if has_early_class else0
# 检查时间冲突(严重惩罚)
if len(time_slots) != len(set(time_slots)):
return-100# 时间冲突,直接给负分
total_score = interest_total + concentration_score + early_penalty
return total_score
def get_neighbors(schedule, num_courses):
"""
生成邻居解:尝试替换一门课
"""
neighbors = []
for i in range(len(schedule)):
for new_course in range(num_courses):
if new_course notin schedule:
neighbor = schedule.copy()
neighbor[i] = new_course
# 记录这次操作(换掉了哪门课,换成了哪门课)
operation = (schedule[i], new_course)
neighbors.append((neighbor, operation))
return neighbors
def tabu_search(courses, num_select=4, max_iterations=50, tabu_tenure=7):
"""
禁忌搜索算法
参数:
courses: 可选课程列表
num_select: 要选几门课
max_iterations: 最大迭代次数
tabu_tenure: 禁忌长度(记忆多少次操作)
"""
print(f"🎓 可选课程共 {len(courses)} 门")
print(f"📋 需要选择 {num_select} 门课")
print(f"🚫 禁忌列表长度: {tabu_tenure}")
print(f"{'='*70}\n")
# 随机生成初始解
current_schedule = random.sample(range(len(courses)), num_select)
current_score = evaluate_schedule(current_schedule, courses)
# 最优解
best_schedule = current_schedule.copy()
best_score = current_score
# 禁忌列表(记录最近的操作)
tabu_list = []
print(f"🎯 初始方案:")
for idx in current_schedule:
print(f" - {courses[idx]}")
print(f"📊 初始评分: {current_score}")
print(f"\n开始优化...\n")
for iteration in range(max_iterations):
# 生成所有邻居(邻域解)
neighbors = get_neighbors(current_schedule, len(courses))
# 找最好的可行邻居
best_neighbor = None
best_neighbor_score = float('-inf')
best_neighbor_operation = None
for neighbor, operation in neighbors:
neighbor_score = evaluate_schedule(neighbor, courses)
# 检查是否在禁忌列表中
is_tabu = operation in tabu_list
# 特赦准则:如果比最优解还好,即使被禁忌也接受
aspiration = neighbor_score > best_score
# 如果不在禁忌列表中,或者满足特赦准则
if (not is_tabu or aspiration) and neighbor_score > best_neighbor_score:
best_neighbor = neighbor
best_neighbor_score = neighbor_score
best_neighbor_operation = operation
# 如果找不到可行邻居,停止
if best_neighbor isNone:
print(f"❌ 迭代 {iteration + 1}: 无可行邻居,停止搜索")
break
# 更新当前解(即使更差也接受!)
current_schedule = best_neighbor
current_score = best_neighbor_score
# 更新禁忌列表
tabu_list.append(best_neighbor_operation)
if len(tabu_list) > tabu_tenure:
tabu_list.pop(0) # 删除最老的记录
# 更新最优解
if current_score > best_score:
best_schedule = current_schedule.copy()
best_score = current_score
print(f"✨ 迭代 {iteration + 1}: 找到更好的方案!评分 {best_score:.1f}")
for idx in best_schedule:
print(f" - {courses[idx]}")
else:
# 显示接受了更差的解(这是禁忌搜索的特点)
if current_score < best_score:
print(f"🔄 迭代 {iteration + 1}: 接受较差解(评分 {current_score:.1f}),继续探索...")
print(f"\n{'='*70}")
print(f"🏆 优化完成!\n")
print(f"📚 最佳选课方案:")
for idx in best_schedule:
print(f" - {courses[idx]}")
print(f"\n📊 最终评分: {best_score}")
return best_schedule, best_score
# 创建课程列表
courses = [
Course("高等数学", 1, 5), # 周一上午,兴趣度5
Course("英语", 2, 4), # 周一下午
Course("Python编程", 3, 9), # 周二上午
Course("数据结构", 4, 8), # 周二下午
Course("机器学习", 6, 10), # 周三下午
Course("体育", 7, 7), # 周四上午
Course("大学物理", 8, 3), # 周四下午
Course("算法设计", 6, 9), # 周三下午(和机器学习冲突)
]
print("📖 所有可选课程:")
for i, course in enumerate(courses):
print(f" {i}. {course}")
print()
# 运行禁忌搜索
best_schedule, best_score = tabu_search(courses, num_select=4, max_iterations=30, tabu_tenure=5)
运行结果示例:
📖 所有可选课程:
0. 高等数学(周一上, 兴趣度5)
1. 英语(周一下, 兴趣度4)
2. Python编程(周二上, 兴趣度9)
3. 数据结构(周二下, 兴趣度8)
4. 机器学习(周三下, 兴趣度10)
5. 体育(周四上, 兴趣度7)
6. 大学物理(周四下, 兴趣度3)
7. 算法设计(周三下, 兴趣度9)
🎓 可选课程共 8 门
📋 需要选择 4 门课
🚫 禁忌列表长度: 5
======================================================================
🎯 初始方案:
- 高等数学(周一上, 兴趣度5)
- 英语(周一下, 兴趣度4)
- 体育(周四上, 兴趣度7)
- 大学物理(周四下, 兴趣度3)
📊 初始评分: 15
开始优化...
✨ 迭代 1: 找到更好的方案!评分 23.0
- Python编程(周二上, 兴趣度9)
- 英语(周一下, 兴趣度4)
- 体育(周四上, 兴趣度7)
- 大学物理(周四下, 兴趣度3)
✨ 迭代 2: 找到更好的方案!评分 28.0
- Python编程(周二上, 兴趣度9)
- 数据结构(周二下, 兴趣度8)
- 体育(周四上, 兴趣度7)
- 大学物理(周四下, 兴趣度3)
✨ 迭代 3: 找到更好的方案!评分 32.0
- Python编程(周二上, 兴趣度9)
- 数据结构(周二下, 兴趣度8)
- 机器学习(周三下, 兴趣度10)
- 体育(周四上, 兴趣度7)
🔄 迭代 4: 接受较差解(评分 30.0),继续探索...
🔄 迭代 5: 接受较差解(评分 29.0),继续探索...
✨ 迭代 6: 找到更好的方案!评分 34.0
- Python编程(周二上, 兴趣度9)
- 数据结构(周二下, 兴趣度8)
- 机器学习(周三下, 兴趣度10)
- 算法设计(周三下, 兴趣度9)
======================================================================
🏆 优化完成!
📚 最佳选课方案:
- Python编程(周二上, 兴趣度9)
- 数据结构(周二下, 兴趣度8)
- 机器学习(周三下, 兴趣度10)
- 算法设计(周三下, 兴趣度9)
📊 最终评分: 34
看到了吗?算法在迭代4和5时接受了更差的解(评分从32降到30和29),但正是这个"暂时后退",让它最终找到了评分34的更好方案!
如果是普通的爬山法,在找到评分32的方案时就会停止,因为周围的邻居(邻域解)都更差。
禁忌搜索的应用场景 🌍
禁忌搜索在很多实际问题中表现出色:
1. 生产调度 🏭
工厂里有100台机器,1000个订单,怎么安排生产顺序才能最大化效率?禁忌搜索能找到接近最优的调度方案。
2. 车辆路径规划 🚚
物流公司有50辆车,要配送到200个地点。怎么分配任务才能让总里程最短?这比旅行商问题还复杂!
3. 网络设计 🌐
设计通信网络时,如何布置路由器和连接线路,才能保证速度快、成本低、还抗故障?
4. 投资组合优化 💰
从几百只股票中选30只组成投资组合,如何平衡收益和风险?禁忌搜索能帮你找到好的配置。
5. 考试排期 📅
学校要给500个班级安排期末考试,避免冲突、均匀分布、还要考虑教室容量,这是个超级复杂的组合优化问题。
禁忌搜索的关键参数 🎛️
想要用好禁忌搜索,需要调整三个关键参数:
1. 禁忌长度(Tabu Tenure)
太短:记忆不够,还是会重复搜索
太长:限制太多,搜索不够灵活
经验值:通常设为问题规模的5-10%
比如100个城市的旅行商问题,禁忌长度可以设为5-10。
2. 迭代次数(Max Iterations)
太少:还没搜索充分就停了
太多:浪费时间,而且可能没什么改进
经验值:至少让算法有机会探索问题空间的主要区域
可以设置"无改进停止"条件:如果连续N次迭代都没改进,就提前结束。
3. 特赦准则(Aspiration Criteria)
最常用的是:如果比当前最优解还好,就接受
也可以设计更复杂的准则,比如:
-
如果比最优解好一定百分比 -
如果满足某些特定条件 -
动态调整特赦标准
优缺点总结 📝
✅ 优点
1. 能跳出局部最优
这是它相比爬山法的最大优势。通过接受更差的解和禁忌机制,能探索更广阔的解空间。
2. 不需要太多参数
相比遗传算法、模拟退火,禁忌搜索的参数比较少,容易调试。
3. 适应性强
几乎可以用于任何组合优化问题,只需要定义好邻居生成规则。
4. 效果稳定
在很多实际问题中,禁忌搜索的表现都很稳定可靠。
❌ 缺点
1. 需要更多内存
要维护禁忌列表,虽然通常不大,但比爬山法要多。
2. 速度略慢
每次迭代要检查禁忌列表,比简单的爬山法慢一些。
3. 参数敏感
禁忌长度设置不当,效果会大打折扣。
4. 不保证全局最优
虽然比爬山法好,但依然是启发式算法,不保证找到最优解。
小技巧:如何用好禁忌搜索 💡
技巧1:动态调整禁忌长度
开始时用较长的禁忌长度(探索),后期逐渐缩短(精细搜索)。
技巧2:多样化策略
如果连续多次迭代都没改进,可以随机跳到一个新位置,避免困在某个区域。
技巧3:强化策略
记录搜索过程中的好解,定期从这些好解出发重新搜索。
技巧4:组合其他方法
可以先用遗传算法找几个不错的解,然后用禁忌搜索精细优化。
结语:带着记忆去探索 🧠
禁忌搜索告诉我们一个深刻的道理:记忆是智能的基础。
没有记忆的算法就像失忆的人,永远在重复同样的错误。而禁忌搜索通过一个简单的"黑名单"机制,就让算法有了短期记忆,大大提升了搜索效率。
更有意思的是,它允许"暂时后退"。人生不也是这样吗?有时候要先退一步,才能跳得更远。
正如一句名言所说:
"那些忘记历史的人,注定要重蹈覆辙。"
禁忌搜索就是一个有记忆、会学习、能突破的智能算法。
🎓 实践建议
如果你想动手试试:
-
先运行上面的选课案例,理解基本流程 -
尝试修改禁忌长度,看看效果有什么变化 -
把它应用到旅行商问题,对比和爬山法的差异 -
挑战一个自己的实际问题!
📚 进阶阅读
如果你对优化算法感兴趣,推荐学习:
-
遗传算法:模拟生物进化 -
蚁群算法:模拟蚂蚁找食物 -
模拟退火:模拟金属冷却过程 -
粒子群优化:模拟鸟群觅食
这些都是禁忌搜索的"兄弟算法"!
💬 思考
假设你要装修房子,有10个工人,20项任务。每个工人擅长不同的任务,有些任务之间有先后顺序(比如必须先刷墙再装灯)。
如果用禁忌搜索来优化工人分配和任务顺序,你会:
-
如何定义"邻域"? -
如何评估一个方案的好坏? -
什么操作应该被加入禁忌列表?
想想看,这是个很实际的问题哦!😊

