Heuristic

什么是启发式算法(Heuristic Algorithm)?

启发式算法是一类在解决复杂问题时利用经验规则和启发式信息进行搜索的算法。这些算法并不保证找到最优解,但在很多情况下能找到一个较好的解,且计算效率较高。启发式算法广泛应用于组合优化、人工智能、搜索问题等领域。

特点

  1. 不保证最优解

    • 启发式算法的目标是找到一个足够好的解,而不是最优解。因为在实际应用中,找到最优解往往耗时过长,启发式算法则在较短时间内找到一个可以接受的解决方案。
    • 例如,在旅行商问题中找到一个近似最优的路径,而不是穷举所有可能的路径以找到最优解。
  2. 速度快

    • 启发式算法通常能够在较短的时间内找到可接受的解决方案,适用于需要快速响应的问题。它们使用基于经验的规则减少了搜索空间,提高了效率。
    • 例如,贪心算法在每一步选择当前最优解,避免了复杂的全局搜索。
  3. 利用领域知识

    • 启发式算法往往依赖于特定领域的知识或经验规则,以指导搜索过程,提高解的质量。这使得它们在某些特定问题上表现得特别好。
    • 例如,使用领域知识设计算法规则来优化物流配送中的路径规划。

常见的启发式算法

  1. 贪心算法(Greedy Algorithm)

    基本思想:贪心算法在每一步都做出当前看来最优的选择,不考虑整体情况和后续影响。它通过局部最优选择来期望达到全局最优。

    公式
    设问题的解由若干步骤组成,每一步都有一个可选的集合 C i C_i Ci ,贪心算法选择当前步骤中最优的元素 g i ∈ C i g_i \in C_i giCi ,最终得到的解 S = { g 1 , g 2 , . . . , g n } S = \{g_1, g_2, ..., g_n\} S={g1,g2,...,gn}

    算法步骤

    1. 初始化解集合 S S S 为空。
    2. 对于每个步骤 i i i ,从候选集合 C i C_i Ci 中选择最优元素 g i g_i gi 并加入 S S S
    3. 重复步骤2,直到所有步骤完成。
    4. 返回解集合 S S S

    应用场景:活动选择问题、单源最短路径(Dijkstra算法)、Huffman编码。

    示例代码

    def greedy_activity_selection(activities):
        activities.sort(key=lambda x: x[1])
        selected = [activities[0]]
        last_end_time = activities[0][1]
    
        for i in range(1, len(activities)):
            if activities[i][0] >= last_end_time:
                selected.append(activities[i])
                last_end_time = activities[i][1]
        
        return selected
    
    activities = [(1, 4), (3, 5), (0, 6), (5, 7), (8, 9), (5, 9)]
    selected_activities = greedy_activity_selection(activities)
    print("Selected activities:", selected_activities)
    
  2. 局部搜索(Local Search)

    基本思想:局部搜索算法从一个初始解出发,通过对当前解的局部修改来寻找更优解,适用于优化问题和搜索问题。

    公式

    • 局部搜索的目标是找到一个局部最优解 x ∗ x^* x
    • 满足 f ( x ∗ ) ≥ f ( x ) f(x^*) \geq f(x) f(x)f(x)
    • 对于所有在邻域 N ( x ∗ ) N(x^*) N(x) 中的解 x x x

    算法步骤

    1. 选择一个初始解 x x x
    2. 重复以下步骤直到满足终止条件:
      a. 在当前解的邻域中选择一个新解 x ′ x' x
      b. 如果新解 x ′ x' x 比当前解 x x x 更优,则更新当前解 x x x
    3. 返回当前解 x x x

    应用场景:爬山算法、模拟退火、禁忌搜索。

    示例代码(爬山算法)

    import random
    
    def hill_climbing(objective_function, solution_space, iterations=1000):
        current_solution = random.choice(solution_space)
        current_value = objective_function(current_solution)
    
        for _ in range(iterations):
            neighbor = random.choice(solution_space)
            neighbor_value = objective_function(neighbor)
    
            if neighbor_value > current_value:
                current_solution = neighbor
                current_value = neighbor_value
        
        return current_solution
    
    def objective_function(x):
        return -(x ** 2) + 5
    
    solution_space = [i for i in range(-10, 11)]
    best_solution = hill_climbing(objective_function, solution_space)
    print("Best solution:", best_solution)
    
  3. 进化算法(Evolutionary Algorithm)

    基本思想:进化算法模拟自然选择和遗传变异,通过选择、交叉和变异等操作逐步优化解。

    公式
    进化算法通过以下步骤迭代生成下一代个体:
    P t + 1 = mutate ( crossover ( select ( P t ) ) ) P_{t+1} = \text{mutate}(\text{crossover}(\text{select}(P_t))) Pt+1=mutate(crossover(select(Pt)))
    其中 P t P_t Pt 为第 t t t 代的种群, select \text{select} select crossover \text{crossover} crossover mutate \text{mutate} mutate 分别为选择、交叉和变异操作。

    算法步骤

    1. 初始化种群 P 0 P_0 P0
    2. 评估种群中个体的适应度。
    3. 重复以下步骤直到满足终止条件:
      a. 选择适应度较高的个体进行繁殖。
      b. 通过交叉操作生成新的个体。
      c. 通过变异操作引入随机变化。
      d. 更新种群 P t P_t Pt
    4. 返回最优个体。

    应用场景:遗传算法(Genetic Algorithm)、差分进化(Differential Evolution)。

    举个例子:假设我们有一个寻宝游戏,目标是在一个未知的大山里找到隐藏的宝藏。每个参赛者代表一个“个体”,他们手中的地图和指南针代表他们的“基因”或“解”。我们的任务是找到最佳的路径到达宝藏所在地,这就是我们的“优化问题”。

    1. 初始化:每个参赛者(个体)随机选择一个起点,并带着他们各自的地图和指南针(初始解)开始寻宝。
    2. 评估:每走一段距离,我们就评估一下参赛者离宝藏的距离(适应度)。离宝藏越近,适应度就越高。
    3. 选择:在所有的参赛者中,我们选择那些离宝藏较近的参赛者(适应性好的个体)作为“父母”,让他们有更多的机会参与下一代的繁殖。
    4. 变异和重组:这些被选中的“父母”会进行“交配”(重组),同时他们的“基因”(地图和指南针)也可能会发生一些微小的变化(变异),比如发现一条新的路径或者调整指南针的方向。这样,我们就得到了新一代的参赛者(子代)。
    5. 迭代:重复上述的评估、选择和变异重组过程,直到我们找到宝藏或者满足某个终止条件(比如达到了最大的搜索次数)。

    示例代码(遗传算法)

    import random
    
    def genetic_algorithm(population, fitness_fn, mutation_fn, crossover_fn, generations=100):
        for _ in range(generations):
            population = sorted(population, key=fitness_fn, reverse=True)
            next_generation = population[:2]
    
            for _ in range(len(population) // 2 - 1):
                parents = random.sample(population[:10], 2)
                offspring_a, offspring_b = crossover_fn(parents[0], parents[1])
                next_generation += [mutation_fn(offspring_a), mutation_fn(offspring_b)]
            
            population = next_generation
    
        return max(population, key=fitness_fn)
    
    def fitness_fn(individual):
        return sum(individual)
    
    def mutation_fn(individual):
        index = random.randint(0, len(individual) - 1)
        individual[index] = 1 - individual[index]
        return individual
    
    def crossover_fn(parent_a, parent_b):
        index = random.randint(1, len(parent_a) - 1)
        return parent_a[:index] + parent_b[index:], parent_b[:index] + parent_a[index:]
    
    population = [[random.randint(0, 1) for _ in range(10)] for _ in range(20)]
    best_individual = genetic_algorithm(population, fitness_fn, mutation_fn, crossover_fn)
    print("Best individual:", best_individual)
    
  4. 蚁群算法(Ant Colony Optimization)

    基本思想:蚁群算法模拟蚂蚁寻找食物的过程,通过信息素的更新和传播来指导搜索路径。蚂蚁通过释放和感知信息素,逐步找到较优路径。

    蚂蚁找食物过程类比:设想一个场景,有一群蚂蚁从蚁巢出发去寻找远处的食物源。蚁巢和食物源之间有多条路径可供选择,但其中只有一条是最短的。蚂蚁在行走过程中会释放信息素,并且能够感知其他蚂蚁释放的信息素浓度。

    1. 初始化阶段:
      a. 所有的路径上都没有信息素,或者信息素浓度相同。
      b. 一群蚂蚁从蚁巢出发,随机选择路径前往食物源。
    2. 路径选择与信息素释放:
      a. 蚂蚁在行走过程中会根据当前路径上的信息素浓度和自身的启发式信息(如距离等)来选择下一步的行走方向。信息素浓度越高的路径,被选择的概率越大。
      b. 当蚂蚁找到食物后,它们会沿着原路返回蚁巢,并在经过的路径上释放信息素。较短的路径上蚂蚁往返的频率更高,因此信息素的积累也更快。
    3. 正反馈机制:
      a. 随着时间的推移,较短路径上的信息素浓度越来越高,而其他路径上的信息素由于挥发而逐渐减少。
      b. 后来的蚂蚁在选择路径时,更倾向于选择信息素浓度高的路径,从而形成一种正反馈机制,使得越来越多的蚂蚁选择最短的路径。
    4. 算法收敛:
      a. 经过多轮迭代后,几乎所有的蚂蚁都会选择最短的路径,此时算法收敛,找到最优解。

    公式
    每只蚂蚁 k k k 从城市 i i i 移动到城市 j j j 的概率为:
    P i j k = [ τ i j ] α ⋅ [ η i j ] β ∑ l ∈ allowed [ τ i l ] α ⋅ [ η i l ] β P_{ij}^k = \frac{[\tau_{ij}]^\alpha \cdot [\eta_{ij}]^\beta}{\sum_{l \in \text{allowed}} [\tau_{il}]^\alpha \cdot [\eta_{il}]^\beta} Pijk=lallowed[τil]α[ηil]β[τij]α[ηij]β
    其中 τ i j \tau_{ij} τij 是边 i j ij ij 上的信息素浓度, η i j \eta_{ij} ηij 是启发式信息(通常为距离的倒数), α \alpha α β \beta β 是调节参数。

    算法步骤

    1. 初始化信息素矩阵。
    2. 重复以下步骤直到满足终止条件:
      a. 每只蚂蚁从起始城市出发,构建一条路径。
      b. 根据路径长度更新信息素矩阵。
      c. 信息素挥发。
    3. 返回最佳路径。

    应用场景:旅行商问题(TSP)、车辆路径问题(VRP)。

    示例代码

    import numpy as np
    
    def ant_colony_optimization(dist_matrix, n_ants, n_iterations, decay, alpha=1, beta=1):
        n_cities = len(dist_matrix)
        pheromones = np.ones((n_cities, n_cities))
        best_path = None
        best_length = float('inf')
    
        for _ in range(n_iterations):
            all_paths = []
            for _ in range(n_ants):
                path = [np.random.randint(0, n_cities)]
                while len(path) < n_cities:
                    probabilities = compute_probabilities(path[-1], path, pheromones, dist_matrix, alpha, beta)
                    next_city = np.random.choice(range(n_cities), p=probabilities)
                    path.append(next_city)
                all_paths.append((path, compute_length(path, dist_matrix)))
    
            for path, length in all_paths:
                if length < best_length:
                    best_length = length
                    best_path = path
                update_pheromones(pheromones, path, length, decay)
    
        return best_path, best_length
    
    def compute_probabilities(current_city, visited, pheromones, dist_matrix, alpha, beta):
        pheromone = np.copy(pheromones[current_city])
        pheromone[visited] = 0
        heuristic = 1 / (dist_matrix[current_city] + 1e-10)
        heuristic[visited] = 0
        probability = (pheromone ** alpha) * (heuristic ** beta)
        return probability / probability.sum()
    
    def compute_length(path, dist_matrix):
        length = 0
        for i in range(len(path) - 1):
            length += dist_matrix[path[i], path[i + 1]]
        length += dist_matrix[path[-1], path[0]]
        return length
    
    def update_pheromones(pheromones, path, length, decay):
        for i in range(len(path) - 1):
            pheromones[path[i], path[i + 1]] += 1 / length
        pheromones[path[-1], path[0]] += 1 / length
        pheromones *= decay
    
    # 示例距离矩阵
    dist_matrix = np.array([
        [0, 2, 9, 10],
        [1, 0, 6, 4],
        [15, 7, 0, 8],
        [6, 3, 12, 0]
    ])
    
    best_path, best_length = ant_colony_optimization(dist_matrix, 10, 100, 0.5)
    print("Best path:", best_path)
    print("Best path length:", best_length)
    
  5. 模拟退火(Simulated Annealing)

    基本思想:模拟退火算法模拟物理退火过程,通过逐渐降低“温度”来减少解的随机性,逐步收敛到一个较优解。算法开始时允许较大的解空间搜索,随着温度降低逐渐收敛到局部最优解。

    固体退火过程类比

    1. 加温过程:
      想象一个金属块(固体),我们将其加热到非常高的温度。在这个过程中,金属内部的粒子(如原子或分子)会开始剧烈振动,变得非常无序,内能增大。这就像是我们在解决问题时,开始时尝试了很多不同的方法和思路,处于一个“混乱”的状态。
    2. 冷却过程:
      随后,我们逐渐降低金属的温度。随着温度的降低,金属内部的粒子开始逐渐有序排列,内能逐渐减小,直到最后达到常温下的稳定状态,即基态,此时内能最小。这就像是我们在解决问题时,通过不断尝试和调整,逐渐找到了一个较为稳定和优化的解。

    模拟退火算法过程

    1. 初始化:
      在模拟退火算法中,我们从一个较高的“温度”(即初始控制参数)和一个随机的“初始解”开始。这个初始解可以看作是我们解决问题的起点。
    2. 迭代过程:
      a. 在每个“温度”下,算法会进行多次迭代,尝试产生新的解(即新的粒子排列方式或新的解决方案)。每次迭代都会计算新解与当前解之间的“能量差”(即目标函数的差值)。
      b. 如果新解的能量更低(即目标函数值更优),那么新解将无条件地被接受为当前解。
      c. 如果新解的能量更高(即目标函数值更差),那么新解将按照一定的概率被接受。这个概率与当前的“温度”有关,温度越高,接受差解的概率越大;温度越低,接受差解的概率越小。
    3. 降温过程:
      随着迭代的进行,“温度”会按照预定的方式逐渐降低(如指数衰减)。这意味着算法在搜索过程中会逐渐变得更加“挑剔”,更倾向于接受能量更低(即更优)的解。
    4. 终止条件:
      当“温度”降低到某个预定的“终止温度”时,算法停止迭代。此时得到的解就是算法找到的近似最优解。

    公式
    模拟退火算法通过接受劣解来避免局部最优,接受概率由Metropolis准则决定:
    P ( E , E ′ , T ) = { 1 if  E ′ < E e E − E ′ T if  E ′ ≥ E P(E, E', T) = \begin{cases} 1 & \text{if } E' < E \\ e^{\frac{E - E'}{T}} & \text{if } E' \geq E \end{cases} P(E,E,T)={1eTEEif E<Eif EE
    其中 E E E E ′ E' E 分别是当前解和新解的能量, T T T 是当前温度。

    算法步骤

    1. 选择一个初始解 x x x 和初始温度 T T T
    2. 重复以下步骤直到满足终止条件:
      a. 生成一个新解 x ′ x' x
      b. 计算能量差 $ \Delta E = E(x’) - E(x) $ 。
      c. 如果 $ \Delta E < 0 $ 或者 $ e^{-\Delta E / T} > \text{随机数} $ ,则接受新解 x ′ x' x
      d. 降低温度 T T T
    3. 返回当前解 x x x

    应用场景:组合优化问题、图像处理中的能量最小化问题。

    举个例子:假设我们有一个迷宫问题,目标是找到从起点到终点的最短路径。我们可以使用模拟退火算法来寻找这条路径:

    1. 初始化:随机选择一条从起点到终点的路径作为初始解。
    2. 迭代过程:在每次迭代中,我们随机选择路径上的一段进行微调(比如改变一小段的方向),然后计算新路径的长度。
      a. 如果新路径更短,就接受新路径。
      b. 如果新路径更长,但当前温度较高,我们仍然有一定的概率接受这条更长的路径(为了探索更多可能性)。
    3. 降温过程:随着迭代的进行,我们逐渐降低“温度”,使得算法更倾向于接受更短的路径。
    4. 终止条件:当温度降低到足够低时,算法停止迭代,此时得到的路径就是算法认为的最短路径(或近似最短路径)。

    示例代码

    import math
    import random
    
    def simulated_annealing(objective_function, solution_space, initial_temp, cooling_rate, iterations=1000):
        current_solution = random.choice(solution_space)
        current_value = objective_function(current_solution)
        temperature = initial_temp
    
        for _ in range(iterations):
            new_solution = random.choice(solution_space)
            new_value = objective_function(new_solution)
            delta = new_value - current_value
    
            if delta > 0 or math.exp(delta / temperature) > random.random():
                current_solution = new_solution
                current_value = new_value
    
            temperature *= cooling_rate
    
        return current_solution
    
    def objective_function(x):
        return -(x ** 2) + 5
    
    solution_space = [i for i in range(-10, 11)]
    best_solution = simulated_annealing(objective_function, solution_space, initial_temp=10, cooling_rate=0.95)
    print("Best solution:", best_solution)
    

应用场景

  1. 组合优化问题

    • 启发式算法常用于解决NP难问题,如旅行商问题、背包问题、排课问题等。在这些问题中,穷举法的计算量过大,而启发式算法通过简化搜索空间,快速找到近似最优解。
    • 例如,在背包问题中,启发式算法可以快速找到一个接近最优的物品选择方案,避免了穷举所有可能组合的计算复杂度。
  2. 路径规划

    • 在机器人路径规划、物流配送中的路径优化、导航系统中的最短路径搜索等场景中,启发式算法能够快速生成可行路径,并在复杂环境中有效应对动态变化。
    • 例如,利用蚁群算法优化物流配送路径,减少配送时间和成本。
  3. 机器学习和数据挖掘

    • 在特征选择、超参数优化、聚类分析等领域,启发式算法提供了一种快速找到较优解的手段,提升了模型性能和训练效率。
    • 例如,使用遗传算法进行超参数优化,可以在较短时间内找到接近最优的参数组合,提高模型的预测准确性。
  4. 图像处理和计算机视觉

    • 在图像分割、目标检测、模式识别等任务中,启发式算法通过简化搜索过程和优化参数,提升了算法的实时性和准确性。
    • 例如,利用模拟退火算法进行图像分割,能够在复杂图像中快速找到合理的分割边界。

例子:旅行商问题(TSP)

旅行商问题(TSP)是一个经典的组合优化问题,要求找到一条经过所有给定城市且总路程最短的路径。虽然TSP是NP难问题,但可以使用启发式算法找到较优的解。

贪心算法示例

import numpy as np

def greedy_tsp(dist_matrix):
    n = len(dist_matrix)
    visited = [False] * n
    path = []
    current_city = 0
    path.append(current_city)
    visited[current_city] = True

    while len(path) < n:
        next_city = None
        min_dist = float('inf')
        for city in range(n):
            if not visited[city] and dist_matrix[current_city][city] < min_dist:
                min_dist = dist_matrix[current_city][city]
                next_city = city
        path.append(next_city)
        visited[next_city] = True
        current_city = next_city
    
    path.append(0)  # Return to the starting city
    return path

# 示例距离矩阵
dist_matrix = np.array([
    [0, 2, 9, 10],
    [1, 0, 6, 4],
    [15, 7, 0, 8],
    [6, 3, 12, 0]
])

path = greedy_tsp(dist_matrix)
print("Traveling Salesman Path:", path)

这个示例展示了如何使用贪心算法解决TSP问题。尽管贪心算法不保证找到最优解,但能在较短时间内找到一个可接受的路径。

综上详细描述了启发式算法及其应用的内容,Enjoy!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐