大数跨境
0
0

禁忌搜索算法:带着"黑名单"去优化

禁忌搜索算法:带着"黑名单"去优化 Python数智工坊
2025-12-01
1
导读:想象你被困在一个巨大的迷宫里,目标是找到出口。第一次尝试,你随便选了个方向走,结果走进了死胡同。



想象你被困在一个巨大的迷宫里,目标是找到出口。

第一次尝试,你随便选了个方向走,结果走进了死胡同。于是你退回来,换了个方向。但走着走着,你突然发现:"咦?这个地方我好像来过!"

如果你什么都不记录,就会像无头苍蝇一样,在同样的地方打转,永远找不到出口。

聪明的做法是什么?带一支笔和一个本子!

每次走过一个岔路口,就在本子上记下来:

  • ✍️ "第一个路口向左 ❌ 死路"
  • ✍️ "第二个路口向右 ❌ 绕圈"
  • ✍️ "第三个路口直走 ✅ 可以试试"

下次再经过这些地方,看看本子:"哦,这条路我试过了,不行!换一条!"

恭喜你!你刚刚发明了计算机科学中的一个经典算法——禁忌搜索(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 [13579for 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("高等数学"15),      # 周一上午,兴趣度5
    Course("英语"24),          # 周一下午
    Course("Python编程"39),    # 周二上午
    Course("数据结构"48),      # 周二下午
    Course("机器学习"610),     # 周三下午
    Course("体育"77),          # 周四上午
    Course("大学物理"83),      # 周四下午
    Course("算法设计"69),      # 周三下午(和机器学习冲突)
]

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:组合其他方法
可以先用遗传算法找几个不错的解,然后用禁忌搜索精细优化。

结语:带着记忆去探索 🧠

禁忌搜索告诉我们一个深刻的道理:记忆是智能的基础。

没有记忆的算法就像失忆的人,永远在重复同样的错误。而禁忌搜索通过一个简单的"黑名单"机制,就让算法有了短期记忆,大大提升了搜索效率。

更有意思的是,它允许"暂时后退"。人生不也是这样吗?有时候要先退一步,才能跳得更远。

正如一句名言所说:

"那些忘记历史的人,注定要重蹈覆辙。"

禁忌搜索就是一个有记忆、会学习、能突破的智能算法。


🎓 实践建议

如果你想动手试试:

  1. 先运行上面的选课案例,理解基本流程
  2. 尝试修改禁忌长度,看看效果有什么变化
  3. 把它应用到旅行商问题,对比和爬山法的差异
  4. 挑战一个自己的实际问题!

📚 进阶阅读

如果你对优化算法感兴趣,推荐学习:

  • 遗传算法:模拟生物进化
  • 蚁群算法:模拟蚂蚁找食物
  • 模拟退火:模拟金属冷却过程
  • 粒子群优化:模拟鸟群觅食

这些都是禁忌搜索的"兄弟算法"!


💬 思考

假设你要装修房子,有10个工人,20项任务。每个工人擅长不同的任务,有些任务之间有先后顺序(比如必须先刷墙再装灯)。

如果用禁忌搜索来优化工人分配和任务顺序,你会:

  1. 如何定义"邻域"?
  2. 如何评估一个方案的好坏?
  3. 什么操作应该被加入禁忌列表?

想想看,这是个很实际的问题哦!😊


【声明】内容源于网络
0
0
Python数智工坊
从上海回归二线城市深耕多年,央企算法工程师。专注数据分析、机器学习、运筹优化、可视化、AI实战! 公众号回复 :数据分析,免费领取 价值满满的 20G 数据科学与AI学习资料包!用数据思维,优化你的技术人生。
内容 605
粉丝 0
Python数智工坊 从上海回归二线城市深耕多年,央企算法工程师。专注数据分析、机器学习、运筹优化、可视化、AI实战! 公众号回复 :数据分析,免费领取 价值满满的 20G 数据科学与AI学习资料包!用数据思维,优化你的技术人生。
总阅读68
粉丝0
内容605