自适应大邻域算法(ALNS)求解TSP问题,golang实现

完整代码: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左右

  • 12
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的自适应大领域搜索算法的示例代码: ```python def adaptive_search(start, goal, heuristic_func, max_depth=10, max_width=5): # 初始化搜索队列 queue = [(start, 0, [])] # 初始化已访问集合 visited = set([start]) # 初始化当前搜索深度 depth = 0 # 开始搜索 while queue: # 获取队列中的下一个节点 node, cost, path = queue.pop(0) # 检查是否到达目标节点 if node == goal: return (path + [node], cost) # 检查是否达到搜索深度限制 if depth < max_depth: # 拓展当前节点的邻居节点 neighbors = get_neighbors(node) for neighbor in neighbors: # 检查邻居节点是否已经访问过 if neighbor not in visited: # 计算邻居节点的启发式函数值 h = heuristic_func(neighbor, goal) # 将邻居节点加入队列 queue.append((neighbor, cost + 1 + h, path + [node])) # 将邻居节点标记为已访问 visited.add(neighbor) # 如果队列长度超过了搜索宽度限制,则按启发式函数值排序并截断队列 if len(queue) > max_width: queue.sort(key=lambda x: x[1]) queue = queue[:max_width] # 如果队列为空,则增加搜索深度 if not queue: depth += 1 # 没有找到路径 return None ``` 在这个示例代码中,`adaptive_search` 函数使用了自适应大领域搜索算法来搜索从起点到目标节点的路径。该算法的关键在于根据当前搜索深度和搜索宽度限制来动态调整搜索策略,以更好地适应不同的搜索场景。具体来说,当搜索深度较浅时,算法会优先探索当前节点的邻居节点,并将未访问的邻居节点加入搜索队列。当搜索深度较深时,为了避免搜索空间过大,算法会限制搜索宽度,并仅保留启发式函数值较小的前几个节点。这样可以在保证搜索质量的同时,提高搜索效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值