定义
启发式算法是基于直观或经验构造的算法,在可接受的计算时间和空间条件下,给出待解决优化问题的一个可行解。 Remember:启发式算法并不保证找到最优解,只是在有限资源下找到还不错的解。经典的启发式算法包括模拟退火、遗传算法、蚁群算法、神经网络等。 启发式算法的共同的目标:求NP-hard组合优化问题的全局最优解,NP-hard问题的限制它们只能以启发式的算法去求解实际问题。 常见的启发式算法都有其实际背景,但启发式算法的目标并非尽可能贴近其来源,而是高效解决要解决的问题。
模拟退火算法
概述
模拟退火算法得益于材料统计力学的研究成果。 统计力学表明材料中粒子的不同结构对应于粒子的不同能量水平。在高温条件下,粒子的能量较高,可以自由运动和重新排列。在低温条件下,粒子能量较低。 如果从高温开始,非常缓慢地降温(这个过程被称为退火),粒子就可以在每个温度下达到热平衡。当系统完全被冷却时,最终形成处于低能状态的晶体。 模拟退火算法的思想是:为了不被局部最优解困住,需要以一定概率跳出当前位置,暂时接受一个不太好的解。在搜索最优解的过程中逐渐降温,初期跳出去的概率比较大, 进行广泛搜索;后期跳出去的概率比较小,尽量收敛到较优解。
解空间就是所有valid解的集合,每次生成的解必须valid。
初始解可以随意选取,但比较好的初始解可以帮助算法尽快收敛 。可先随机生成少量几个序列,选一个比较小的;或者采用贪心算法选择初始解 。
目标函数根据我们要优化的目标确定。
如何生成新的解
新解的生成是最体现creativity的地方,可以自己提出如何将当前解变换为新解,而且保证新解是valid,并且和旧解的差异不要太大
例如: 任意选择两个标号,调换位置
任意选择两个标号,颠倒它们中间的序列
任意选择三个标号,将前两个标号中间的序列放到第三个标号之后 等等
例题
旅行商问题(Traveling Salesman Problem, TSP)是一个经典的组合优化问题。假设有一个销售员需要访问一系列城市,并且每个城市只访问一次。销售员需要从某一个城市出发,访问其他所有城市后再回到出发城市。问题是在所有可能的访问路径中,找出总距离最短的路径。
模型建立
- 决策变量:假设销售员有n个城市需要访问,决策变量为访问城市的顺序。
- 目标函数:目标是最小化总距离,即所有城市间距离的总和。
- 约束条件:每个城市只能访问一次,且必须回到出发城市。
模拟退火算法步骤
- 初始化:随机生成一个初始解,并计算其目标函数值。
- 迭代:重复以下步骤直到满足停止准则:
- 产生新解:通过改变当前解的一部分来生成新解。
- 评价新解:计算新解的目标函数值。
- 接受准则:如果新解的目标函数值小于当前解,则接受新解;否则,根据模拟退火算法接受准则决定是否接受新解。
- 温度更新:按照预设的温度下降策略更新温度。
- 输出:当满足停止准则时,输出当前解作为最优解。
import math
import random
import matplotlib.pyplot as plt
# 生成城市的坐标
def generate_cities(num_cities):
return [(random.uniform(0, 1000), random.uniform(0, 1000)) for _ in range(num_cities)]
# 计算路径长度
def calculate_total_distance(path, cities):
total_distance = 0
for i in range(len(path)):
from_city = cities[path[i]]
to_city = cities[path[(i + 1) % len(path)]] # 下一个城市,如果是最后一个城市则回到起点
total_distance += math.sqrt((from_city[0] - to_city[0]) ** 2 + (from_city[1] - to_city[1]) ** 2)
return total_distance
# 生成新路径
def generate_new_path(path):
new_path = path[:]
left = random.randint(0, len(path) - 1)
right = random.randint(0, len(path) - 1)
new_path[left:right] = reversed(new_path[left:right])
return new_path
# 模拟退火算法
def simulated_annealing(cities, initial_temperature, cooling_rate, final_temperature):
current_path = list(range(len(cities)))
random.shuffle(current_path)
current_distance = calculate_total_distance(current_path, cities)
best_path = current_path[:]
best_distance = current_distance
temperature = initial_temperature
while temperature > final_temperature:
new_path = generate_new_path(current_path)
new_distance = calculate_total_distance(new_path, cities)
if new_distance < current_distance or random.uniform(0, 1) < math.exp(
(current_distance - new_distance) / temperature):
current_path = new_path
current_distance = new_distance
if new_distance < best_distance:
best_path = new_path[:]
best_distance = new_distance
temperature *= cooling_rate
return best_path, best_distance
# 主函数
def main():
num_cities = 20
cities = generate_cities(num_cities)
initial_temperature = 10000
cooling_rate = 0.99
final_temperature = 1
best_path, best_distance = simulated_annealing(cities, initial_temperature, cooling_rate, final_temperature)
# 打印结果
print("Best path:", best_path)
print("Best distance:", best_distance)
# 可视化结果
x = [cities[i][0] for i in best_path + [best_path[0]]]
y = [cities[i][1] for i in best_path + [best_path[0]]]
plt.plot(x, y, marker='o')
plt.title("TSP Path")
plt.xlabel("X")
plt.ylabel("Y")
plt.show()
if __name__ == "__main__":
main()
这段代码首先定义了一个生成城市坐标的函数generate_cities
,然后定义了计算路径长度的函数calculate_total_distance
,以及生成新路径的函数generate_new_path
。核心的模拟退火算法在simulated_annealing
函数中实现。最后,在main
函数中,我们初始化参数,运行模拟退火算法,并打印和可视化结果。
模拟退火算法的结果依赖于初始参数,如初始温度、冷却率和最终温度。这些参数可能需要根据具体问题进行调整以获得更好的结果。
遗传算法
概述
遗传算法是一种基于自然选择原理和自然遗传机制的搜索(寻优)算法,它是模拟自然界中的生命进化机制,在人工系统中实现特定目标的优化 突变和基因重组是进化的原因,遗传算法是通过群体搜索技术,根据适者生存的原则逐代进化,最终得到准最优解。
操作包括:初始群体的产生、求每一个体的适应度、根据适者生存的原则选择优良个体、 被选出的优良个体两两配对,通过随机交叉其染色体的基因并随机变异某些染色体的基因生成下一代群体,按此方法使群体逐代进化,直到满足进化终止条件。
如何选择参数
遗传算法中要选择的参数很多:种群数量M、变异概率、生成子代的数量、迭代次数 。但很遗憾,参数的选择没有固定标准,只能自己试 。种群数量M越大、迭代次数越多、生成的子代越多,当然更有希望找到最优解,但相应的计算资源消耗也会增大,只能在可接受范围内进行选择 。
如何选择初始种群
其实可以随便初始化,但是较好的初始种群可以帮助更快收敛 。例如随机生成若干个选最好的、贪心算法等
如何交配产生子代
交配方法是最能体现creativity的地方,应该尽量继承父代,但也要进行足够的调整 。例如: 选择父亲的一个标号t,在母亲那里找到它后面的全部数字,并依序取出 。把父亲标号t后面的部分接到母亲后面 。把母亲取出来的数字接到父亲后面 如何突变产生新的解 。
突变就是根据自己的解生成一个新的解,方法可以和模拟退火中的方法相同。
例题
旅行商问题(TSP, Traveling Salesman Problem)
问题描述: 假设有一个旅行商需要从自己的城市出发,访问一系列指定的城市,每个城市只能访问一次,最后返回出发的城市。问题是要找出一条最短的路径,使得旅行商能够访问所有城市并返回起点。
问题特点:
- 解决方案空间大: 如果有N个城市,那么可能的路径总数是(N-1)!。
- 评估困难: 对于每一个潜在的解决方案(即一条路径),需要计算其总长度,这在城市数量多时非常耗时。
- 最优解难以直接确定: 由于解决方案空间巨大,很难通过穷举法找到最优解。
遗传算法的应用:
- 编码: 每个潜在解可以被编码为一个排列,代表城市的访问顺序。
- 初始种群: 随机生成一定数量的排列作为初始种群。
- 适应度函数: 对于每个个体(即路径),计算其总长度,并据此定义适应度函数,通常路径越短,适应度越高。
- 选择: 根据适应度选择个体进行繁殖,适应度高的个体有更大的机会被选中。
- 交叉: 随机选择两个个体,并交换它们的部分路径以产生新的路径。
- 变异: 以一定的概率随机改变某个个体的路径,以保持种群的多样性。
- 终止条件: 可以设定最大迭代次数或适应度阈值作为算法的终止条件。
import numpy as np
import random
import matplotlib.pyplot as plt
# 遗传算法解决旅行商问题(TSP)
# 城市数量
num_cities = 10
# 创建随机的城市坐标
cities = np.random.rand(num_cities, 2)
# 计算两个城市之间的距离
def distance(city1, city2):
return np.sqrt((city1[0] - city2[0])**2 + (city1[1] - city2[1])**2)
# 计算路径的总长度
def path_length(path):
total_distance = 0
for i in range(num_cities):
total_distance += distance(cities[path[i]], cities[path[(i+1) % num_cities]])
return total_distance
# 生成初始种群
def generate_initial_population(pop_size):
return [random.sample(range(num_cities), num_cities) for _ in range(pop_size)]
# 适应度函数(路径长度越短,适应度越高)
def fitness(path):
return 1 / path_length(path)
# 选择函数(轮盘赌选择)
def select(population, fitnesses):
total_fitness = sum(fitnesses)
pick = random.uniform(0, total_fitness)
current = 0
for i in range(len(fitnesses)):
current += fitnesses[i]
if current > pick:
return population[i]
# 交叉函数(部分映射交叉)
def crossover(parent1, parent2):
start, end = sorted(random.sample(range(num_cities), 2))
child = [None]*num_cities
child[start:end] = parent1[start:end]
for i in range(num_cities):
if parent2[i] not in child:
for j in range(num_cities):
if child[j] is None:
child[j] = parent2[i]
break
return child
# 变异函数(交换两个城市的位置)
def mutate(path, mutation_rate):
for i in range(num_cities):
if random.random() < mutation_rate:
j = random.randint(0, num_cities - 1)
path[i], path[j] = path[j], path[i]
# 遗传算法主函数
def genetic_algorithm(pop_size, generations, mutation_rate):
population = generate_initial_population(pop_size)
for generation in range(generations):
fitnesses = [fitness(path) for path in population]
new_population = []
for _ in range(pop_size):
parent1 = select(population, fitnesses)
parent2 = select(population, fitnesses)
child = crossover(parent1, parent2)
mutate(child, mutation_rate)
new_population.append(child)
population = new_population
best_path = population[np.argmax(fitnesses)]
return best_path, path_length(best_path)
# 运行遗传算法
best_path, best_path_length = genetic_algorithm(pop_size=100, generations=500, mutation_rate=0.01)
print(best_path, best_path_length)
# 绘制城市坐标
plt.figure(figsize=(8, 6))
plt.scatter(cities[:,0], cities[:,1], color='blue')
plt.title("城市坐标")
# 标注城市编号
for i, city in enumerate(cities):
plt.text(city[0], city[1], str(i), color='red', fontsize=12)
# 绘制旅行商的路径
path = np.array(best_path)
path = np.append(path, path[0]) # 返回起点
plt.plot(cities[path,0], cities[path,1], color='green', marker='o')
# 显示图形
plt.grid(True)
plt.show()
运行结果: [8, 7, 1, 2, 0, 5, 3, 9, 6, 4] 3.8794508852581986
使用遗传算法解决旅行商问题后,我们找到了一条近似最优的路径和对应的路径长度。在这个例子中,城市数量设置为10,算法运行了500代,每代有100个个体,变异率为0.01。
找到的近似最优路径为:
[8, 7, 1, 2, 0, 5, 3, 9, 6, 4]
对应的路径长度大约为: 3.8794508852581986
这表明旅行商按照这个顺序访问城市,能够以大约4.192的距离完成整个旅程。需要注意的是,由于遗传算法是一种启发式算法,它可能不会每次都找到绝对的最优解,但通常能找到相当接近最优解的解。