完整代码:https://github.com/huyiqiu/ALNS
1. TSP问题
旅行商问题(Traveling Salesman Problem, TSP)是管理科学和运筹学中一个著名的组合优化问题。这个问题问了这样一个问题:给定一组城市和每对城市之间的距离,访问每个城市一次并返回起始城市的最短路径是什么。
2. 大邻域搜索算法
大邻域搜索算法(Large Neighborhood Search , LNS)是一种元启发式优化算法,LNS算法从一个初始可行解开始,生成一个大邻域的可行解集,这些解与当前解决方案的结构相似,但潜在的差异很大。然后,该算法评估邻域内每个解的质量,并选择最优解作为新的当前解。重复这个过程,直到没有进一步的改进或满足停止标准。LNS与传统邻域搜索算法的主要区别在于邻域的大小。在LNS中,邻域比传统邻域搜索算法中使用的邻域要大得多,来搜索更广泛的解决方案,并可能找到更好的解决方案。较大的邻域搜索有助于跳出局部最优,尽可能接近全局最优解。容易陷入局部最优也是传统邻域搜索算法的主要缺点。
LNS的具体步骤可以根据要解决的问题和算法的具体实现而有所不同。然而,大邻域搜索算法所涉及的步骤概要如下:
步骤1:初始化可行解,初始解通常使用随机生成,也可以使用其他优化算法生成。
步骤2:生成大邻域,生成大邻域的方法通常为“破坏和修复”,破坏步骤可能涉及从当前解决方案中移除随机的客户子集,修复步骤可能涉及以不同的顺序将被移除的客户重新插入到解决方案中。
步骤3:从邻域中选择最优解决方案作为新的当前解。
步骤4:重复步骤2和步骤3直到当前解不再改进或达到预设的算法终止条件。
步骤5:当前解即为LNS算法求得的最终解。
具体步骤和每个步骤如何实现的细节会根据待解决的问题和算法的具体实现而有所不同。
3. 自适应大邻域算法(ALNS)
自适应大邻域算法就是在选择【破坏】和【修复】算子时,假如自适应选择的策略,总的来说就是给算子加入一个打分的环节。每一次迭代都通过轮盘赌选择一个算子进行操作。
4. 代码
4.1 导入tsp文件
NAME: china34
TYPE: TSP
COMMENT: 34 locations in China
DIMENSION: 34
EDGE_WEIGHT_TYPE: WGS
NODE_COORD_SECTION
1 101.74 36.56
2 103.73 36.03
3 106.27 38.47
4 108.95 34.27
5 113.65 34.76
6 117.00 36.65
7 114.48 38.03
8 112.53 37.87
9 111.65 40.82
10 116.41 39.90
11 117.20 39.08
12 123.38 41.80
13 125.35 43.88
14 126.63 45.75
15 121.47 31.23
16 120.19 30.26
17 118.78 32.04
18 117.27 31.86
19 114.31 30.52
20 113.00 28.21
21 115.89 28.68
22 119.30 26.08
23 121.30 25.03
24 114.17 22.32
25 113.55 22.20
26 113.23 23.16
27 110.35 20.02
28 108.33 22.84
29 106.71 26.57
30 106.55 29.56
31 104.06 30.67
32 102.73 25.04
33 91.11 29.97
34 87.68 43.77
EOF
4.2 破坏算子
4.2.1随机破坏
// 从路径中删除一些点
func RandomDestroy(path []int, cnt int) ([]int, []int) {
tmpPath := make([]int, 0)
destroyed := make([]int, 0)
// 生成随机不重复的cnt个点
for i := 0; i < cnt; i ++ {
for {
removeNode := common.RandInt(0, len(path) - 1)
if common.NotIn(destroyed, removeNode) {
destroyed = append(destroyed, removeNode)
break
}
}
}
for _, node := range path {
if common.NotIn(destroyed, node) {
tmpPath = append(tmpPath, node)
}
}
return tmpPath, destroyed
}
4.2.2贪心破坏
func GreedyDestroy(path []int, cnt int) ([]int, []int) {
tmpPath := make([]int, 0)
destroyed := make([]int, 0)
savingMap := make(map[int]float64)
// 计算每个节点的节约值
for _, node := range path {
pathWithoutNode := PathWithoutNode(node, path)
saving := common.CalcTSP(path) - common.CalcTSP(pathWithoutNode)
savingMap[node] = saving
}
// 计算节约值最大的cnt个节点
for i := 0; i < cnt; i ++ {
maxSaving := 0.0
var tobeDestroyed int
for node, saving := range savingMap {
if saving > maxSaving {
maxSaving = saving
tobeDestroyed = node
}
}
destroyed = append(destroyed, tobeDestroyed)
delete(savingMap, tobeDestroyed)
}
// 得到剩余的路径
for _, node := range path {
if common.NotIn(destroyed, node) {
tmpPath = append(tmpPath, node)
}
}
return tmpPath, destroyed
}
4.3修复算子
4.3.1 随机修复
// 将被破坏的点随机插入路径
func RandomRepair(path []int, destroyed []int) []int {
afterRepair := path
for _, node := range destroyed {
afterRepair = RandomInsert(node, afterRepair)
}
return afterRepair
}
贪心修复
func GreedyRepair(path []int, destroyed []int) []int {
afterRepair := path
for _, node := range destroyed {
afterRepair = GreedyInsert(node, afterRepair)
}
return afterRepair
}
4.4 打分
4.4.1 模拟退火接受准则
func (sa *SA) Accept(diff float64) bool {
probability := math.Exp(-diff / sa.NowTemperature)
return common.RandDecimal() < probability
}
4.4.2 打分
// update score
if newValue <= alns.NowValue {
alns.NowPath = newPath
alns.NowValue = newValue
if newValue <= alns.HistoricallyBest {
alns.HistoricallyBest = newValue
alns.BestPath = newPath
alns.DestroyScoreBoard[destroyFuncName] = 1.5
alns.RepairScoreBoard[repairFuncName] = 1.5
} else {
alns.DestroyScoreBoard[destroyFuncName] = 1.2
alns.RepairScoreBoard[repairFuncName] = 1.2
}
} else {
if alns.Sa.Accept(newValue - alns.NowValue) {
alns.NowPath = newPath
alns.NowValue = newValue
alns.DestroyScoreBoard[destroyFuncName] = 0.8
alns.RepairScoreBoard[repairFuncName] = 0.8
} else {
alns.DestroyScoreBoard[destroyFuncName] = 0.5
alns.RepairScoreBoard[repairFuncName] = 0.5
}
}
4.4.3 计算权重
// 计算权重:ρ = λρ + (1 - λ)Ψ
func (alns *ALNS) CalcWeight() {
lambda := constant.WeightCoefficient
// 计算破坏算子权重
for k := range DestroyMap {
alns.DestroyWeights[k] = lambda*alns.DestroyWeights[k] + (1-lambda)*alns.DestroyScoreBoard[k]
}
// 计算修复算子权重
for k := range RepairMap {
alns.RepairWeights[k] = lambda*alns.RepairWeights[k] + (1-lambda)*alns.RepairScoreBoard[k]
}
}
4.5 主循环逻辑
func (alns *ALNS) Run() {
// 初始化路径
alns.InitPath(constant.InitSolutionByGreedy)
for i := 1; i <= alns.SettingIteration; i++ {
// 轮盘赌选择算子
destroyFuncName := common.RouletteSelect(alns.DestroyWeights)
repairFuncName := common.RouletteSelect(alns.RepairWeights)
destroy, repair := DestroyMap[destroyFuncName], RepairMap[repairFuncName]
tmpPath, destroyed := destroy.Func(alns.NowPath, alns.Cut)
newPath := repair.Func(tmpPath, destroyed)
newValue := common.CalcTSP(newPath)
alns.OperatorUsageTimes[destroyFuncName] ++
alns.OperatorUsageTimes[repairFuncName] ++
// 打分
if newValue <= alns.NowValue {
alns.NowPath = newPath
alns.NowValue = newValue
if newValue <= alns.HistoricallyBest {
alns.HistoricallyBest = newValue
alns.BestPath = newPath
alns.DestroyScoreBoard[destroyFuncName] = 1.5
alns.RepairScoreBoard[repairFuncName] = 1.5
} else {
alns.DestroyScoreBoard[destroyFuncName] = 1.2
alns.RepairScoreBoard[repairFuncName] = 1.2
}
} else {
if alns.Sa.Accept(newValue - alns.NowValue) {
alns.NowPath = newPath
alns.NowValue = newValue
alns.DestroyScoreBoard[destroyFuncName] = 0.8
alns.RepairScoreBoard[repairFuncName] = 0.8
} else {
alns.DestroyScoreBoard[destroyFuncName] = 0.5
alns.RepairScoreBoard[repairFuncName] = 0.5
}
}
// 计算权重
alns.CalcWeight()
// update temperature
alns.Sa.NowTemperature *= alns.Sa.CoolingRate
// // print middle data
// fmt.Println("当前迭代次数:", i, " 历史最优解:", alns.HistoricallyBest, " 当前解:", alns.NowValue)
}
}
4.6 算法效果
运行效果受实验参数影响 对于china34这个算例,迭代1000次仅需120ms左右
对于xqf131这个算例,迭代1000次仅需2s左右