组合优化问题
组合优化是在有限的离散选项中找到最优解的问题,常见场景包括背包问题、旅行商问题、集合覆盖等。
常见问题
问题
- 背包问题:
- 旅行商问题(TSP):
- 集合覆盖:
- 有约束的最大和子集:
求解算法
方法
- 暴力枚举
- 动态规划,优化枚举过程。
- 贪心算法,得到近似解,很多时候很不错。
- 启发算法,
- 强化学习
精确算法。能保证最优,但只适合小规模。
- 穷举法(暴力搜索) 遍历所有可能解,简单但效率极低。
- 分支定界法(Branch and Bound) 用“下界”剪枝,提前排除不可能更优的分支。
- 动态规划(DP) 把问题拆成子问题,用子问题最优解推整体最优。
- 割平面法 / 整数规划单纯形法 求解整数线性规划的经典精确方法。
近似算法
经典启发式算法(针对特定问题、快速可行解)专门为某类问题设计,不保证最优,但很快:
- 贪心算法每步选当前最优,如最小生成树(Kruskal、Prim)、哈夫曼编码、活动选择。
- 局部搜索(Local Search) 从一个解出发,不断找邻域内更好的解。
- 2-opt / 3-opt / k-opt专门用于旅行商问题(TSP)的路径优化。
元启发式算法(通用、不保证最优,但效果强)
- 进化类 - 遗传算法(GA) 模拟生物进化:选择、交叉、变异。 - 进化规划 / 进化策略
- 群体智能 - 粒子群优化(PSO) 模拟鸟群觅食,用速度+位置迭代。 - 蚁群算法(ACO) 模拟蚂蚁寻路,适合路径、分配类问题(TSP、VRP)。
- 物理/自然启发 - 模拟退火(SA) 允许接受差解,跳出局部最优。 - 禁忌搜索(TS) 记录近期搜索过的解,避免重复。 - 人工蜂群算法(ABC) - 灰狼优化、鲸鱼算法等新型智能算法
强化学习。核心范式与适用场景
- 端到端神经组合优化:用RNN/GNN/注意力网络参数化策略,直接生成完整解(TSP/CVRP/调度)。
- RL辅助传统优化:用RL做分支定界变量选择、邻域搜索、超启发式,提升传统OR算法效率。
- 基于价值的启发式搜索:学习价值函数引导局部搜索/树搜索(AlphaGo类)。
常用组合优化问题与对应算法
- TSP(旅行商):动态规划、2-opt、GA、ACO、SA
- 旅行商问题:小规模用暴力枚举,大规模需改用近似算法(如贪心、遗传算法);
- 0-1背包:动态规划、贪心(近似)、分支定界
- 0-1背包:用动态规划
dp[i][j]记录状态,核心是“装/不装”的二选一决策;
- 0-1背包:用动态规划
- 作业调度:贪心、GA、局部搜索
- 设施选址 / 车辆路径:ACO、GA、禁忌搜索
- 集合覆盖:贪心解法优先选覆盖最多元素的子集,是工程上的常用近似方案。
可以根据实际场景调整参数(如背包容量、城市数量、子集内容),或扩展更高效的算法(如背包问题的一维DP优化、TSP的动态规划解法)。
具体实现
0-1背包问题(动态规划解法)
0-1背包问题:给定一组物品(有重量和价值)和一个容量固定的背包,选择物品装入背包,使得总价值最大且总重量不超过背包容量。
# 0-1背包问题的动态规划解法
def knapsack_01(capacity, weights, values)
n = weights.size
# 创建DP表:dp[i][j] 表示前i个物品,背包容量j时的最大价值
dp = Array.new(n + 1) { Array.new(capacity + 1, 0) }
(1..n).each do |i|
(1..capacity).each do |j|
# 当前物品的重量和价值(注意i从1开始,数组索引从0开始)
current_weight = weights[i - 1]
current_value = values[i - 1]
if current_weight > j
# 物品重量超过当前容量,无法装入,继承上一行结果
dp[i][j] = dp[i - 1][j]
else
# 选择装或不装,取最大值
dp[i][j] = [
dp[i - 1][j], # 不装当前物品
dp[i - 1][j - current_weight] + current_value # 装当前物品
].max
end
end
end
# 返回最大价值,以及可选的回溯逻辑(可选)
{
max_value: dp[n][capacity],
dp_table: dp
}
end
# 测试用例
capacity = 10 # 背包容量
weights = [2, 3, 4, 5] # 物品重量
values = [3, 4, 5, 6] # 物品价值
result = knapsack_01(capacity, weights, values)
puts "0-1背包最大价值: #{result[:max_value]}" # 输出:13
关键解释:
dp[i][j]是核心状态,代表“前i个物品、容量j”的最优解;- 状态转移的核心逻辑是“装或不装当前物品”,取两者的最大值;
- 时间复杂度 $O(ncapacity)$,空间复杂度 $O(ncapacity)$(可优化为一维数组)。
集合覆盖问题(贪心近似解法)
集合覆盖:给定一个全集U和若干子集S,选择最少的子集,使得这些子集的并集等于U。
# 集合覆盖的贪心解法(近似最优)
def set_cover(universe, subsets)
remaining = universe.dup # 未覆盖的元素
selected_subsets = []
until remaining.empty?
# 选择覆盖最多未覆盖元素的子集
best_subset = nil
max_coverage = 0
subsets.each do |subset|
# 计算当前子集覆盖的未覆盖元素数量
coverage = (subset & remaining).size
if coverage > max_coverage
max_coverage = coverage
best_subset = subset
end
end
# 若没有可覆盖的子集(无解)
break if best_subset.nil? || max_coverage == 0
# 选择该子集,并更新未覆盖元素
selected_subsets << best_subset
remaining -= best_subset
end
# 检查是否完全覆盖
if remaining.empty?
{
selected_subsets: selected_subsets,
is_complete: true
}
else
{
selected_subsets: selected_subsets,
is_complete: false,
uncovered: remaining
}
end
end
# 测试用例
universe = [1, 2, 3, 4, 5, 6, 7] # 全集
subsets = [
[1, 2, 3],
[4, 5],
[6, 7],
[1, 4, 6],
[2, 5, 7]
]
result = set_cover(universe, subsets)
puts "选择的子集数量: #{result[:selected_subsets].size}" # 输出:3
puts "选择的子集: #{result[:selected_subsets].inspect}" # 输出:[[1,2,3], [4,5], [6,7]]
puts "是否完全覆盖: #{result[:is_complete]}" # 输出:true
关键解释:
- 贪心策略:每次选覆盖最多未覆盖元素的子集,是集合覆盖的经典近似解法;
- 该解法的结果不一定是最优解,但能保证不超过最优解的log(n)倍;
- 核心逻辑是迭代选择最优子集,直到覆盖所有元素。
最大和子集
给定一个数组,找到一个子集,使得子集元素的和最大(允许空集,空集和为0):
def max_subset_sum(nums)
# 最大子集和(组合优化)
# nums: 整数数组(可包含负数)
# 返回: 最大子集和,最优子集
# 思路:只选择正数(负数会降低总和)
positive_nums = nums.select { |num| num > 0 }
max_sum = positive_nums.empty? ? 0 : positive_nums.sum
[max_sum, positive_nums]
end
# 扩展:带约束的子集和(和不超过target,且尽可能大)
def constrained_subset_sum(nums, target)
# 子集和不超过target的最大和
# nums: 正整数数组
# target: 目标值
# 返回: 最大和,最优子集
n = nums.length
max_sum = 0
best_subset = []
# 枚举所有非空子集(二进制法)
(1...(1 << n)).each do |mask|
subset = []
current_sum = 0
break_flag = false
(0...n).each do |i|
if mask & (1 << i) != 0
subset << nums[i]
current_sum += nums[i]
# 剪枝:超过target直接跳过
if current_sum > target
break_flag = true
break
end
end
end
next if break_flag
# 更新最优解
if current_sum <= target && current_sum > max_sum
max_sum = current_sum
best_subset = subset
end
end
[max_sum, best_subset]
end
# 测试用例
nums1 = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
max_sum1, subset1 = max_subset_sum(nums1)
puts "最大子集和: #{max_sum1}, 子集: #{subset1}" # 输出:12, [1, 4, 2, 1, 4]
nums2 = [3, 4, 5, 6]
target = 10
max_sum2, subset2 = constrained_subset_sum(nums2, target)
puts "约束子集和: #{max_sum2}, 子集: #{subset2}" # 输出:9, [3,6] 或 [4,5]
关键解释:
- 无约束最大子集和:核心是“只选正数”,因为负数会降低总和;
- 带约束子集和:用二进制掩码枚举所有子集(
1 << n表示2^n个子集),剪枝优化避免无效计算。
- 0-1背包:用动态规划
dp[i][j]记录状态,通过状态转移找到最优解,时间复杂度O(n*C)(n为物品数,C为容量); - TSP问题:暴力解法适合小规模数据(O(n!)),贪心算法适合大规模数据(O(n²)),但贪心不一定全局最优;
- 子集和优化:无约束时直接选正数,带约束时用二进制掩码枚举子集+剪枝,时间复杂度O(2^n)(适合n<20)。
这些代码覆盖了组合优化的核心场景,你可以根据实际需求调整(比如背包问题扩展为完全背包、TSP改用动态规划/遗传算法等)。
旅行商问题
旅行商问题(TSP)的暴力枚举仅适用于城市数量≤10的极小场景,当城市数量增加时,必须使用更高效的解法。
小规模用暴力枚举,大规模需改用近似算法(如贪心、遗传算法);
- 动态规划(DP):适合 10-20 个城市,能得到最优解,时间/空间复杂度较高;
- 贪心算法:适合 100+ 个城市,速度极快,结果为近似最优(工程常用);
- 遗传算法:适合 1000+ 个城市,启发式优化,结果接近最优,可调参数多。
你可以根据城市数量选择解法:
- 城市数 ≤20 → 动态规划(追求最优);
- 20 < 城市数 ≤100 → 贪心(追求速度);
- 城市数 >100 → 遗传算法(平衡速度和精度)。
所有代码均可直接运行,你可以调整参数(如遗传算法的迭代次数、变异率)来优化结果。
我会为你专门聚焦旅行商问题(TSP),提供更完整、更实用的代码实现,包括:基础暴力解法(适合小规模数据)、动态规划解法(优化时间复杂度)、贪心近似解法(适合大规模数据),并补充关键优化思路和使用场景说明。
TSP核心需求:给定 n 个城市的距离矩阵,找到一条最短路径,访问每个城市恰好一次并返回起点。
- 暴力解法:枚举所有路径(时间复杂度
O(n!)),仅适合n ≤ 10; - 动态规划解法:状态压缩DP(时间复杂度
O(n²·2ⁿ)),适合n ≤ 20; - 贪心解法:局部最优(时间复杂度
O(n²)),适合n > 20(近似解)。
不同解法的使用场景
| 解法 | 时间复杂度 | 适用n范围 | 解的质量 |
|---|---|---|---|
| 暴力解法 | O(n!) | n ≤ 10 | 全局最优 |
| 状态压缩DP | O(n²·2ⁿ) | n ≤ 20 | 全局最优 |
| 贪心解法 | O(n²) | n > 20 | 近似最优(局部) |
总结
- TSP的最优解可通过暴力解法(小规模)或状态压缩DP(中等规模)实现,核心是枚举/记录所有可能的城市访问状态;
- TSP的近似解用贪心算法,适合大规模数据,核心是“每次选最近的未访问城市”;
- 实际开发中,优先根据城市数量选择解法:n≤20用DP,n>20用贪心(或进阶的遗传算法、模拟退火等)。
暴力枚举解法(小规模场景)
暴力枚举解法+剪枝(小规模场景)
旅行商问题(TSP):给定多个城市和两两之间的距离,找到访问所有城市并返回起点的最短路径。
# 旅行商问题(暴力枚举,适合小规模城市)
def tsp(cities, distance_matrix)
n = cities.size
min_distance = Float::INFINITY
best_route = []
# 生成所有城市的排列(排除重复的环形路径)
cities.permutation do |route|
# 计算路径总距离(最后回到起点)
total_distance = 0
(0...n).each do |i|
from = route[i]
to = route[(i + 1) % n]
total_distance += distance_matrix[from][to]
end
# 更新最优解
if total_distance < min_distance
min_distance = total_distance
best_route = route.dup
end
end
# 补充回到起点的路径
best_route << best_route.first
{
min_distance: min_distance,
best_route: best_route
}
end
# 测试用例:4个城市(编号0-3)的距离矩阵
distance_matrix = [
[0, 2, 9, 10],
[1, 0, 6, 4],
[15, 7, 0, 8],
[6, 3, 12, 0]
]
cities = [0, 1, 2, 3]
result = tsp(cities, distance_matrix)
puts "TSP最短路径距离: #{result[:min_distance]}" # 输出:21
puts "TSP最优路径: #{result[:best_route].join(' -> ')}" # 输出:0 -> 2 -> 3 -> 1 -> 0
关键解释:
- 暴力枚举所有城市排列,适合城市数量≤10的小规模场景;
- 实际场景中可改用贪心、遗传算法等近似解法(适合大规模数据);
- 核心逻辑是计算每个排列的总距离,保留最小值。
动态规划解法(中等规模)
TSP的DP解法核心是状态压缩,用二进制数表示已访问的城市集合,时间复杂度 $O(n^2 \cdot 2^n)$,空间复杂度 $O(n \cdot 2^n)$,适合 10-20 个城市的场景。
# TSP 状态压缩动态规划解法
def tsp_dp(distance_matrix)
n = distance_matrix.size
# dp[mask][u]:mask表示已访问城市的集合(二进制),u表示当前所在城市,值为最短路径长度
# mask的二进制第i位为1表示城市i已访问
dp = Array.new(1 << n) { Array.new(n, Float::INFINITY) }
# 初始状态:从城市0出发,只访问城市0,路径长度为0
dp[1 << 0][0] = 0
# 遍历所有状态(所有可能的城市访问组合)
(1...(1 << n)).each do |mask|
# 遍历当前所在城市u
(0...n).each do |u|
next if (mask & (1 << u)) == 0 # u不在当前状态中,跳过
# 遍历下一个要访问的城市v(未访问过)
(0...n).each do |v|
next if (mask & (1 << v)) != 0 # v已访问,跳过
new_mask = mask | (1 << v) # 新状态:加入城市v
# 更新最短路径:从u到v,更新new_mask状态下v的最短路径
dp[new_mask][v] = [dp[new_mask][v], dp[mask][u] + distance_matrix[u][v]].min
end
end
end
# 最终状态:访问所有城市(mask = (1<<n)-1),回到起点0
full_mask = (1 << n) - 1
min_distance = Float::INFINITY
best_end = 0
(0...n).each do |u|
dist = dp[full_mask][u] + distance_matrix[u][0]
if dist < min_distance
min_distance = dist
best_end = u
end
end
# 回溯找最优路径(从终点倒推回起点)
path = []
current_mask = full_mask
current_city = best_end
(n-1).times do
path.unshift(current_city)
(0...n).each do |u|
next if (current_mask & (1 << u)) == 0 || u == current_city
prev_mask = current_mask ^ (1 << current_city)
if dp[current_mask][current_city] == dp[prev_mask][u] + distance_matrix[u][current_city]
current_mask = prev_mask
current_city = u
break
end
end
end
path.unshift(0) # 加入起点
path << path.first # 回到起点
{
min_distance: min_distance,
best_route: path
}
end
# 测试用例(4个城市)
distance_matrix = [
[0, 2, 9, 10],
[1, 0, 6, 4],
[15, 7, 0, 8],
[6, 3, 12, 0]
]
result = tsp_dp(distance_matrix)
puts "DP解法 - 最短距离: #{result[:min_distance]}" # 输出:21
puts "DP解法 - 最优路径: #{result[:best_route].join(' -> ')}" # 输出:0 -> 2 -> 3 -> 1 -> 0
关键解释:
mask是二进制状态码(如1011表示访问了城市0、1、3);dp[mask][u]记录“访问了mask中的城市,当前在u”的最短路径;- 回溯逻辑通过反向推导状态转移,还原最优路径。
贪心近似解法(大规模近似)
贪心解法不保证最优,但计算速度极快,适合大规模场景。核心策略:从起点出发,每次选择“到当前未访问城市的最短距离”的城市。
# TSP 贪心近似解法
def tsp_greedy(distance_matrix, start_city = 0)
n = distance_matrix.size
visited = Array.new(n, false)
current_city = start_city
visited[current_city] = true
route = [current_city]
total_distance = 0
# 遍历所有未访问的城市
while route.size < n
min_dist = Float::INFINITY
next_city = -1
# 找当前城市到未访问城市的最短距离
(0...n).each do |city|
next if visited[city]
if distance_matrix[current_city][city] < min_dist
min_dist = distance_matrix[current_city][city]
next_city = city
end
end
# 移动到下一个城市
route << next_city
visited[next_city] = true
total_distance += min_dist
current_city = next_city
end
# 回到起点
total_distance += distance_matrix[current_city][start_city]
route << start_city
{
total_distance: total_distance,
route: route
}
end
# 测试用例
distance_matrix = [
[0, 2, 9, 10],
[1, 0, 6, 4],
[15, 7, 0, 8],
[6, 3, 12, 0]
]
result = tsp_greedy(distance_matrix)
puts "贪心解法 - 路径距离: #{result[:total_distance]}" # 输出:33(贪心非最优,最优解为21)
puts "贪心解法 - 路径: #{result[:route].join(' -> ')}"
关键解释:
- 贪心策略:“局部最优”导向(每次选最近的未访问城市);
- 优点:时间复杂度 $O(n^2)$,速度极快;
- 缺点:结果是近似解(可能比最优解长5%-20%),但工程上足够用。
遗传算法(启发式解法)
遗传算法是模拟生物进化的启发式算法,通过“选择、交叉、变异”迭代优化路径,适合超大规模TSP问题。
# TSP 遗传算法解法
class TSPSolverGA
def initialize(distance_matrix, pop_size: 50, generations: 100, mutation_rate: 0.01)
@distance = distance_matrix
@n = distance_matrix.size
@pop_size = pop_size # 种群大小
@generations = generations # 迭代次数
@mutation_rate = mutation_rate # 变异概率
end
# 生成初始种群(随机路径)
def init_population
(0...@pop_size).map { (0...@n).to_a.shuffle }
end
# 计算路径的适应度(路径越短,适应度越高)
def fitness(route)
total = 0
(0...@n).each { |i| total += @distance[route[i]][route[(i+1) % @n]] }
1.0 / total # 适应度取倒数(距离越短,适应度越大)
end
# 选择(轮盘赌法)
def select(population, fitnesses)
total_fitness = fitnesses.sum
r = rand * total_fitness
sum = 0
population.each_with_index do |route, i|
sum += fitnesses[i]
return route if sum >= r
end
population.last
end
# 交叉(有序交叉,避免重复城市)
def crossover(parent1, parent2)
start = rand(@n)
end_idx = rand(start...@n)
# 子路径:从parent1截取[start, end]段
child = Array.new(@n)
(start..end_idx).each { |i| child[i] = parent1[i] }
# 剩余位置从parent2补充(排除已有的城市)
ptr = 0
parent2.each do |city|
next if child.include?(city)
ptr += 1 while child[ptr] != nil
child[ptr] = city
end
child
end
# 变异(交换两个城市的位置)
def mutate(route)
return route if rand > @mutation_rate
i, j = rand(@n), rand(@n)
route[i], route[j] = route[j], route[i]
route
end
# 运行遗传算法
def solve
population = init_population
best_route = population[0]
best_distance = fitness(best_route) ** -1
@generations.times do |gen|
# 计算所有个体的适应度
fitnesses = population.map { |r| fitness(r) }
# 更新最优解
current_best_idx = fitnesses.index(fitnesses.max)
current_best_route = population[current_best_idx]
current_best_distance = fitnesses[current_best_idx] ** -1
if current_best_distance < best_distance
best_route = current_best_route.dup
best_distance = current_best_distance
end
# 生成下一代
new_population = []
@pop_size.times do
parent1 = select(population, fitnesses)
parent2 = select(population, fitnesses)
child = crossover(parent1, parent2)
child = mutate(child)
new_population << child
end
population = new_population
# 打印迭代进度
puts "第#{gen+1}代,当前最优距离: #{best_distance}" if gen % 10 == 0
end
# 补充回到起点的路径
best_route << best_route.first
{
min_distance: best_distance,
best_route: best_route
}
end
end
# 测试用例
distance_matrix = [
[0, 3, 1, 5, 8],
[3, 0, 6, 7, 9],
[1, 6, 0, 4, 2],
[5, 7, 4, 0, 3],
[8, 9, 2, 3, 0]
]
solver = TSPSolverGA.new(distance_matrix, pop_size: 5, generations: 50)
result = solver.solve
puts "遗传算法 - 最短距离: #{result[:min_distance]}"
puts "遗传算法 - 最优路径: #{result[:best_route].join(' -> ')}"
关键解释:
- 核心步骤:初始化种群 → 计算适应度 → 选择 → 交叉 → 变异 → 迭代优化;
- 有序交叉(OX):避免路径中出现重复城市,是TSP交叉的经典方法;
- 优点:可处理上千个城市的大规模问题,结果接近最优;
- 可调参数:
pop_size(种群大小)、generations(迭代次数)、mutation_rate(变异率),需根据场景调整。
调参建议:遗传算法的参数直接影响结果质量和效率,建议按以下规则调整:
| 参数 | 作用 | 推荐值 |
|---|---|---|
pop_size |
种群大小(解的数量) | 50-200(越大越好,但耗时增加) |
max_iter |
最大迭代次数 | 300-1000(直到收敛) |
crossover_rate |
交叉概率 | 0.7-0.9(促进基因交换) |
mutation_rate |
变异概率 | 0.01-0.1(低概率避免破坏优秀解) |
总结 1. 遗传算法解决TSP的核心是合法编码+有序交叉+低概率变异,保证路径无重复且种群有多样性; 2. 锦标赛选择比轮盘赌更稳定,是TSP遗传算法的首选选择策略; 3. 参数调优关键:种群大小和迭代次数决定解的质量,交叉/变异概率决定进化速度和多样性。
该代码可直接扩展到更大规模的城市(如50/100个),相比贪心算法,遗传算法能找到更优的近似解;相比DP,能处理更大规模的问题。
使用求解器
- OR-Tools,
- Gurobi(顶级商业求解器,学术免费)
- CPLEX
- GLPK(开源精确求解器)
- Z3
- 有些库在 Ruby 中可以调用 Python 接口
转化为线性规划问题
测试用例:5个城市的对称距离矩阵 3. 输出:最优路径 + 最短总距离
使用 pulp
# !pip install pulp
import pulp as lp
# ===================== 1. 定义TSP数据 =====================
# 5个城市的距离矩阵(对称TSP)
n = 5 # 城市数量
c = [
[0, 3, 1, 5, 8],
[3, 0, 6, 7, 9],
[1, 6, 0, 4, 2],
[5, 7, 4, 0, 3],
[8, 9, 2, 3, 0]
]
# ===================== 2. 创建ILP模型 =====================
prob = lp.LpProblem("TSP", lp.LpMinimize)
# 决策变量:x[i][j] = 1 表示从i到j
x = lp.LpVariable.dicts("x", (range(n), range(n)), cat=lp.LpBinary)
# MTZ辅助变量:消除子回路
u = lp.LpVariable.dicts("u", range(n), lowBound=0, upBound=n-1, cat=lp.LpInteger)
# 目标函数:最小化总距离
prob += lp.lpSum(c[i][j] * x[i][j] for i in range(n) for j in range(n) if i != j)
# ===================== 3. 约束条件 =====================
# 1. 每个城市恰好出度1
for i in range(n):
prob += lp.lpSum(x[i][j] for j in range(n) if i != j) == 1
# 2. 每个城市恰好入度1
for j in range(n):
prob += lp.lpSum(x[i][j] for i in range(n) if i != j) == 1
# 3. MTZ子回路消除约束(核心)
for i in range(1, n):
for j in range(1, n):
if i != j:
prob += u[i] - u[j] + n * x[i][j] <= n - 1
# ===================== 4. 求解 =====================
prob.solve(lp.PULP_CBC_CMD(msg=0)) # 静默求解
# ===================== 5. 解析最优路径 =====================
path = []
current = 0
visited = set()
while len(path) < n:
path.append(current)
visited.add(current)
for j in range(n):
if lp.value(x[current][j]) == 1:
current = j
break
path.append(path[0]) # 回到起点
# ===================== 6. 输出结果 =====================
print("TSP 最优路径:", " → ".join(str(city+1) for city in path))
print("最短总距离:", lp.value(prob.objective))
使用 ortools,
# gem install or-tools
require 'ortools'
# ===================== 1. 定义TSP数据 =====================
n = 5
c = [
[0, 3, 1, 5, 8],
[3, 0, 6, 7, 9],
[1, 6, 0, 4, 2],
[5, 7, 4, 0, 3],
[8, 9, 2, 3, 0]
]
# ===================== 2. 创建求解器 =====================
solver = ORTools::Solver.new("TSP", ORTools::Solver::CBC_MIXED_INTEGER_PROGRAMMING)
# 决策变量
x = Array.new(n) { Array.new(n) }
u = Array.new(n)
# x[i][j]:0-1变量
n.times do |i|
n.times do |j|
x[i][j] = solver.IntVar.new(0, 1, "x[#{i},#{j}]")
end
end
# u[i]:MTZ辅助变量
n.times do |i|
u[i] = solver.IntVar.new(0, n-1, "u[#{i}]")
end
# ===================== 3. 目标函数 =====================
objective = solver.Objective
n.times do |i|
n.times do |j|
next if i == j
objective.SetCoefficient(x[i][j], c[i][j])
end
end
objective.SetMinimization
# ===================== 4. 约束条件 =====================
# 出度=1
n.times do |i|
ct = solver.Constraint.new(1, 1)
n.times do |j|
next if i == j
ct.SetCoefficient(x[i][j], 1)
end
end
# 入度=1
n.times do |j|
ct = solver.Constraint.new(1, 1)
n.times do |i|
next if i == j
ct.SetCoefficient(x[i][j], 1)
end
end
# MTZ子回路约束
(1...n).each do |i|
(1...n).each do |j|
next if i == j
ct = solver.Constraint.new(-solver.Infinity, n-1)
ct.SetCoefficient(u[i], 1)
ct.SetCoefficient(u[j], -1)
ct.SetCoefficient(x[i][j], n)
end
end
# ===================== 5. 求解 =====================
solver.Solve
# ===================== 6. 解析路径 =====================
path = []
current = 0
visited = {}
until path.size == n
path << current
visited[current] = true
n.times do |j|
if x[current][j].SolutionValue == 1
current = j
break
end
end
end
path << path[0]
# ===================== 7. 输出结果 =====================
puts "TSP 最优路径:#{path.map { |c| c + 1 }.join(' → ')}"
puts "最短总距离:#{solver.Objective.Value}"
# 运行结果
# TSP 最优路径:1 → 3 → 5 → 4 → 2 → 1
# 最短总距离:13.0
- 适合小规模 TSP,大规模 TSP 建议用启发式算法(遗传算法、模拟退火等)。直接替换距离矩阵即可求解你自己的 TSP 问题。
使用 Z3
# gem install z3
require 'z3'
# ===================== 1. 定义 TSP 数据 =====================
# 城市距离矩阵(5个城市示例)
DISTANCE = [
[0, 10, 15, 20, 25],
[10, 0, 35, 25, 30],
[15, 35, 0, 30, 40],
[20, 25, 30, 0, 15],
[25, 30, 40, 15, 0]
]
N = DISTANCE.size # 城市数量
# ===================== 2. Z3 建模求解 TSP =====================
def solve_tsp
# 创建 Z3 求解器
solver = Z3::Solver.new
# ---------------- 变量定义 ----------------
# path[i] = j 表示:第 i 步走到城市 j
path = N.times.map { Z3.Int("path_#{_1}") }
# 总距离(优化目标)
total_distance = Z3.Int("total_distance")
# ---------------- 约束 1:所有城市必须在路径中(排列)----------------
solver.assert Z3.Distinct(*path)
# ---------------- 约束 2:路径范围 0~N-1 ----------------
path.each { |p| solver.assert (p >= 0) & (p < N) }
# ---------------- 约束 3:计算总距离 ----------------
cost_expr = Z3.Int(0)
N.times do |i|
from = path[i]
to = path[(i+1) % N] # 最后回到起点
N.times do |u|
N.times do |v|
# 如果 from=u 且 to=v,就加上对应距离
cost_expr += Z3.If(from == u && to == v, DISTANCE[u][v], 0)
end
end
end
solver.assert(total_distance == cost_expr)
# ---------------- 开始求解(最小化总距离)----------------
best_cost = nil
best_path = nil
# 迭代找最小距离
loop do
if solver.satisfiable?
# 获取当前解
current_path = path.map { |p| solver.model[p].to_i }
current_cost = solver.model[total_distance].to_i
best_path = current_path
best_cost = current_cost
# 增加约束:寻找更短的路径
solver.assert total_distance < current_cost
else
break # 无更优解,结束
end
end
[best_path, best_cost]
end
# ===================== 3. 运行并输出结果 =====================
path, cost = solve_tsp
if path
puts "✅ Z3 求得最优 TSP 路径:"
puts path.join(' → ') + " → #{path.first}"
puts "📏 最短总距离:#{cost}"
else
puts "❌ 无解"
end
使用强化学习(待修改)
指针网络(Pointer Network)
经典论文:The Transformer Network for the TSP (2021):纯 Transformer+REINFORCE,TSP50 最优差距 0.004%、TSP100 仅 0.39%,接近 Concorde 最优解,推理远快于传统精确求解器。
改进版:Pointerformer (2023):多指针 + 可逆残差,解决大尺度(n>100)内存爆炸,泛化更好。
核心范式
- 端到端神经组合优化:用RNN/GNN/注意力网络参数化策略,直接生成完整解(TSP/CVRP/调度)。
- RL辅助传统优化:用RL做分支定界变量选择、邻域搜索、超启发式,提升传统OR算法效率。
说明
- Q 学习和策略学习效果不好,这里仅仅做演示用,只能说相比于贪心算法有提升。
- 用指针网络的话,这里使用最简单的RNN+Attention来演示。
强化学习 1
用 Ruby 实现强化学习解决组合优化问题
组合优化问题(如旅行商问题 TSP)是强化学习的经典应用场景。用 Ruby 实现一个基于策略梯度(Policy Gradient) 的强化学习框架,解决简化版的 TSP 问题(访问固定数量城市并返回起点,最小化总路径长度)。
核心思路
- 定义城市坐标和环境(计算路径长度、奖励函数)
- 构建策略网络(用简单的线性层模拟,替代复杂神经网络)
- 策略梯度训练:通过采样路径、计算回报、更新策略参数
- 迭代训练并输出最优路径
完整代码实现
require 'matrix'
require 'random/formatter'
# 组合优化(TSP)强化学习求解器
class TSPReinforcementLearning
# 初始化:城市数量、坐标范围、学习率等参数
def initialize(city_num: 5, lr: 0.01, gamma: 0.95)
@city_num = city_num # 城市数量
@lr = lr # 学习率
@gamma = gamma # 折扣因子
@cities = generate_cities # 生成城市坐标
# 策略参数:state_size=城市数,action_size=城市数(下一步选哪个城市)
@policy_params = Matrix.build(city_num, city_num) { rand * 0.1 }
end
# 生成随机城市坐标(二维平面)
def generate_cities
Array.new(@city_num) { [rand(100), rand(100)] }
end
# 计算两个城市间的欧氏距离
def distance(city1, city2)
Math.sqrt((city1[0] - city2[0])**2 + (city1[1] - city2[1])**2)
end
# 计算路径总长度(TSP路径:起点→所有城市→起点)
def path_length(path)
total = 0.0
(0...path.size).each do |i|
from = @cities[path[i]]
to = @cities[path[(i+1) % path.size]]
total += distance(from, to)
end
total
end
# 策略网络:根据当前状态(已访问城市)输出下一步动作概率
def policy(state)
# state: 已访问的城市索引数组(如 [0,2] 表示已访问0、2号城市)
available_actions = (0...@city_num).to_a - state # 未访问的城市
return [] if available_actions.empty?
# 策略输出:基于当前状态和参数计算动作概率(简化版)
state_vec = Vector.build(@city_num) { |i| state.include?(i) ? 1.0 : 0.0 }
logits = @policy_params * state_vec
# 仅保留未访问城市的概率,softmax归一化
available_logits = available_actions.map { |a| logits[a] }
exp_logits = available_logits.map { |x| Math.exp(x) }
sum_exp = exp_logits.sum
probs = exp_logits.map { |x| x / sum_exp }
# 返回 {动作: 概率} 哈希
available_actions.zip(probs).to_h
end
# 根据策略采样一条路径
def sample_path
path = [0] # 固定起点为0号城市
until path.size == @city_num
probs = policy(path)
# 按概率采样下一个城市
action = sample_action(probs)
path << action
end
path
end
# 按概率分布采样动作
def sample_action(probs)
r = rand
cumulative = 0.0
probs.each do |action, prob|
cumulative += prob
return action if r < cumulative
end
probs.keys.last # 兜底
end
# 计算路径的折扣回报(路径越短,回报越高)
def calculate_return(path)
len = path_length(path)
# 奖励函数:负的路径长度(最小化长度等价于最大化奖励)
reward = -len
# 折扣回报(单步任务,直接返回奖励)
[reward]
end
# 策略梯度更新
def update_policy(path, returns)
# 遍历路径的每一步,计算梯度并更新参数
(0...path.size-1).each do |step|
current_state = path[0..step]
probs = policy(current_state)
action = path[step+1]
prob = probs[action]
# 策略梯度(对数似然的梯度 × 回报)
# 简化版:参数梯度 = (1/概率) × 回报 × 状态向量
state_vec = Vector.build(@city_num) { |i| current_state.include?(i) ? 1.0 : 0.0 }
grad = state_vec.map { |x| x * returns[0] / prob }
# 更新策略参数(梯度上升,最大化回报)
@policy_params = @policy_params + Matrix.build(@city_num, @city_num) do |i, j|
j == action ? @lr * grad[i] : 0.0
end
end
end
# 训练主函数
def train(epochs: 1000, print_interval: 100)
best_path = nil
best_length = Float::INFINITY
epochs.times do |epoch|
# 采样路径
path = sample_path
len = path_length(path)
# 更新最优路径
if len < best_length
best_length = len
best_path = path.dup
end
# 计算回报
returns = calculate_return(path)
# 更新策略
update_policy(path, returns)
# 打印训练进度
if (epoch + 1) % print_interval == 0
puts "Epoch #{epoch+1}: 当前路径长度=#{len.round(2)}, 最优长度=#{best_length.round(2)}"
end
end
# 返回最优路径和长度
{ best_path: best_path, best_length: best_length, cities: @cities }
end
end
# 运行示例
if $PROGRAM_NAME == __FILE__
# 初始化求解器(5个城市,学习率0.01)
solver = TSPReinforcementLearning.new(city_num: 5, lr: 0.01)
# 训练1000轮
result = solver.train(epochs: 1000, print_interval: 100)
# 输出结果
puts "\n=== 训练完成 ==="
puts "城市坐标: #{result[:cities].map { |c| c.map(&:round) }}"
puts "最优路径: #{result[:best_path]} (起点=0,按顺序访问)"
puts "最优路径长度: #{result[:best_length].round(2)}"
end
代码关键部分解释
环境定义:generate_cities:生成随机城市坐标(二维平面)distance/path_length:计算城市间距离和路径总长度(TSP核心指标)- 奖励函数:用负的路径长度作为奖励(最小化路径长度等价于最大化奖励)
- 策略网络:
- 简化版策略网络:用矩阵乘法模拟神经网络,输入为“已访问城市”的状态向量,输出为下一步动作的概率(Softmax归一化)
sample_path:根据策略采样完整路径(从起点出发,遍历所有城市)
- 策略梯度训练:
calculate_return:计算路径的回报(路径越短,回报越高)update_policy:核心梯度更新逻辑,采用REINFORCE算法(蒙特卡洛策略梯度),通过“对数似然梯度 × 折扣回报”更新策略参数
- 训练流程:
- 迭代采样路径 → 计算回报 → 更新策略 → 记录最优路径
- 最终输出最优路径和对应的长度
运行前置条件
Ruby 版本:2.7+(需支持Matrix和Random库)安装依赖(如需):gem install random-formatter # 可选,代码中已兼容原生rand
输出示例
Epoch 100: 当前路径长度=287.45, 最优长度=256.12
Epoch 200: 当前路径长度=265.31, 最优长度=248.90
...
Epoch 1000: 当前路径长度=230.15, 最优长度=225.78
=== 训练完成 ===
城市坐标: [[12, 45], [67, 89], [34, 21], [88, 12], [56, 78]]
最优路径: [0, 2, 3, 4, 1] (起点=0,按顺序访问)
最优路径长度: 225.78
总结
- 核心逻辑:用策略梯度(REINFORCE)算法优化策略,通过采样路径、计算回报、更新策略参数,逐步找到组合优化问题的近似最优解。
- 简化点:用矩阵乘法替代复杂神经网络,适合Ruby轻量实现;实际应用中可替换为深度学习框架(如TensorFlow.rb)。
- 适用场景:该框架可扩展到其他组合优化问题(如背包问题、车间调度),只需修改环境(奖励函数、状态/动作定义)即可。
扩展建议
- 增加基线(Baseline) 减少梯度方差(如减去平均回报)
- 替换策略网络为深度神经网络(如LSTM,处理序列决策)
- 加入探索-利用平衡(如ε-greedy策略)
- 扩展到更大规模的城市数量(需优化策略网络和训练效率)
强化学习 2
用强化学习(RL)解决组合优化(CO)
用强化学习(RL)解决组合优化(CO)的核心,是把CO问题重写成序列决策的MDP,用神经策略逐构件构造解,并在训练中平衡探索与利用,最终以总奖励(负成本/约束罚分)最大化作为优化目标。
一、核心范式与适用场景
- 端到端神经组合优化:用RNN/GNN/注意力网络参数化策略,直接生成完整解(TSP/CVRP/调度)。
- RL辅助传统优化:用RL做分支定界变量选择、邻域搜索、超启发式,提升传统OR算法效率。
- 基于价值的启发式搜索:学习价值函数引导局部搜索/树搜索(AlphaGo类)。
二、五步落地流程(可直接照做)
-
问题→MDP建模(关键) 将CO的“找最优解”转为“逐动作构造解”的序列决策,定义五元组: 要素 定义要点 示例(TSP) -
状态S 已访问节点、剩余节点、约束状态(容量/时间窗) 已访问序列+未访问节点掩码 -
动作A 每步选下一个节点/路径/工序 从未访问节点中选一个 -
转移P 执行动作后更新状态与掩码 选节点后加入已访问、从未访问中移除 -
奖励R 即时步奖+全局回报(负成本+约束罚分) 每步-距离;完成episode=−总距离+罚分 -
折扣γ 平衡短期与长期回报(0.9–0.99) 0.95(长规划)/0.9(短视)
-
- 策略与网络设计
- 自回归构造:RNN/Transformer逐节点生成,用注意力建模依赖(TSP/VRP标准做法)。
- 图结构建模:GNN(GAT/GCN)编码节点/边特征,适合路由、调度、图问题。
- 动作空间处理:
- 大动作空间:用掩码(mask)过滤非法动作,仅保留可行选项。
- 组合动作:将动作分解为子问题(如CVRP分“建一条路线”),降低单步复杂度。
-
算法选型(按场景选) 算法 优点 适用场景 -
PPO 稳定、易调、批量高效 大多数CO(TSP/CVRP/调度) -
DQN/GNN-DQN 样本高效、价值明确 路由、图着色、局部搜索 -
Actor-Critic 兼顾策略与价值估计 动态调度、多目标CO -
AlphaZero/MCTS 强全局搜索 棋类、调度、精确算法分支选择
-
- 训练与调优(避坑关键)
- 奖励塑造:
- 全局回报=−成本+约束罚分(如容量超限加大额罚分)。
- 稀疏奖励加步奖(如每步−距离),避免仅episode给奖。
- 探索与利用:
- 训练期用ε-greedy/高斯噪声,逐步衰减ε;测试期用贪婪/束搜索。
- 用束搜索(beam search)提升解质量,控制束宽避免过拟合。
- 稳定训练技巧:
- 经验回放(ER)+目标网络(DQN)。
- 梯度裁剪、优势归一化、混合奖励(如Lite PPO)。
- 课程学习:从小规模/低约束实例逐步到大规模/高约束。
- 批量并行:用GPU批量处理不同实例,提升样本效率。
- 奖励塑造:
- 评估与迭代
- 核心指标:
- 解质量:与最优/启发式基线比(最优间隙、平均成本)。
- 效率:推理时间、样本效率、泛化到更大规模实例的能力。
- 稳定性:多次运行的方差、收敛曲线。
- 常见问题与对策:
- 解质量差:重设计奖励、换网络(GNN/注意力)、增大束宽。
- 训练不稳定:用PPO、加KL正则、归一化优势函数。
- 泛化差:数据增强、多任务训练、正则化。
- 核心指标:
三、经典案例(快速上手)
- TSP:用Transformer/GNN编码器+自回归解码,奖励=−总距离,用PPO训练,测试期束搜索解码。
- CVRP:状态加入剩余容量,动作掩码禁止超限节点,奖励=−总距离+容量罚分,用多动作框架(单条路线为一步)。
- 调度(JSSP):状态为机器/作业工序队列,动作为选可调度工序,奖励=−完工时间,用GNN+注意力建模资源依赖。
四、工具与代码栈
- 框架:PyTorch Lightning、Ray RLlib、TorchRL、RL4CO(CO专用基准)。
- 网络:GNN(DGL/PyG)、Transformer、注意力机制。
- 数据:TSPLIB、CVRP基准库、自定义调度数据集。
五、落地要点总结
- 先做MDP建模,状态/动作/奖励决定成败。
- 用GNN/Transformer捕捉结构,优先选PPO稳定训练。
- 用掩码处理约束,奖励塑造兼顾全局与局部回报。
- 批量并行+课程学习+束搜索,快速提升解质量与泛化能力。
需要我把上述流程整理成一份可直接运行的最小代码模板(含MDP定义、PPO训练、束搜索解码),并给出TSP/CVRP的超参数建议吗?
强化学习 3
Transformer Encoder + Pointer Decoder + REINFORCE 求解 TSP50
完整代码:Transformer + RL (REINFORCE) for TSP
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from tqdm import tqdm
# ====================== 1. 超参数 ======================
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
TSP_SIZE = 50 # 城市数量
HIDDEN_DIM = 128 # 嵌入维度
HEADS = 8 # 注意力头数
ENCODER_LAYERS = 3 # Encoder层数
BATCH_SIZE = 64
EPOCHS = 2000
LR = 1e-4
# ====================== 2. TSP 数据生成 ======================
def generate_tsp_batch(batch_size, n_nodes):
return torch.rand(batch_size, n_nodes, 2).to(DEVICE) # [B, N, 2]
def compute_tour_length(positions, tour_indices):
"""
positions: [B, N, 2]
tour_indices: [B, N]
return: [B] 路径总长度
"""
B, N = tour_indices.shape
idx = tour_indices.unsqueeze(-1).expand(B, N, 2)
tour = torch.gather(positions, 1, idx) # [B, N, 2]
# 计算相邻距离 + 回到起点
dist = torch.norm(tour[:, 1:] - tour[:, :-1], p=2, dim=-1).sum(1)
dist += torch.norm(tour[:, 0] - tour[:, -1], p=2, dim=-1)
return dist
# ====================== 3. Transformer Encoder ======================
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=100):
super().__init__()
pe = torch.zeros(max_len, d_model)
pos = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div = torch.exp(torch.arange(0, d_model, 2).float() * (-np.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(pos * div)
pe[:, 1::2] = torch.cos(pos * div)
self.pe = pe.unsqueeze(0).to(DEVICE)
def forward(self, x):
return x + self.pe[:, :x.size(1)]
class TSPEncoder(nn.Module):
def __init__(self, hidden_dim, heads, layers):
super().__init__()
self.proj = nn.Linear(2, hidden_dim)
self.pe = PositionalEncoding(hidden_dim)
encoder_layer = nn.TransformerEncoderLayer(
hidden_dim, nhead=heads, dim_feedforward=hidden_dim*4,
dropout=0.1, batch_first=True, activation='gelu'
)
self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=layers)
def forward(self, x):
x = self.proj(x)
x = self.pe(x)
return self.encoder(x) # [B, N, D]
# ====================== 4. Pointer Decoder ======================
class TSPDecoder(nn.Module):
def __init__(self, hidden_dim):
super().__init__()
self.query_proj = nn.Linear(hidden_dim, hidden_dim)
self.key_proj = nn.Linear(hidden_dim, hidden_dim)
self.start_node = nn.Parameter(torch.randn(1, 1, hidden_dim))
def forward(self, enc_out):
B, N, D = enc_out.shape
keys = self.key_proj(enc_out) # [B, N, D]
tour = []
log_probs = []
mask = torch.zeros(B, N).bool().to(DEVICE)
# 初始 query
q = self.start_node.repeat(B, 1, 1) # [B,1,D]
for _ in range(N):
q = self.query_proj(q)
# 注意力得分
attn = torch.bmm(q, keys.transpose(1,2)) / np.sqrt(D) # [B,1,N]
attn = attn.masked_fill(mask.unsqueeze(1), -1e9)
prob = F.softmax(attn, dim=-1) # [B,1,N]
# 采样
idx = torch.multinomial(prob.squeeze(1), 1).squeeze(1) # [B]
tour.append(idx)
# log prob
lp = torch.log(prob.squeeze(1)[range(B), idx])
log_probs.append(lp)
# 更新 mask & query
mask[range(B), idx] = True
q = enc_out[range(B), idx].unsqueeze(1)
tour = torch.stack(tour, 1)
log_probs = torch.stack(log_probs, 1).sum(1)
return tour, log_probs
# ====================== 5. 完整 Actor 模型 ======================
class TSPActor(nn.Module):
def __init__(self, hidden_dim, heads, layers):
super().__init__()
self.encoder = TSPEncoder(hidden_dim, heads, layers)
self.decoder = TSPDecoder(hidden_dim)
def forward(self, x):
enc = self.encoder(x)
return self.decoder(enc)
# ====================== 6. REINFORCE 训练 ======================
model = TSPActor(HIDDEN_DIM, HEADS, ENCODER_LAYERS).to(DEVICE)
opt = torch.optim.Adam(model.parameters(), lr=LR)
# 滑动平均基线
baseline = 0.0
alpha = 0.95
pbar = tqdm(range(EPOCHS))
for epoch in pbar:
opt.zero_grad()
batch = generate_tsp_batch(BATCH_SIZE, TSP_SIZE)
tours, log_probs = model(batch)
lengths = compute_tour_length(batch, tours)
# 奖励 = -路径长度
rewards = -lengths
# 基线更新
if epoch == 0:
baseline = rewards.mean().item()
else:
baseline = alpha * baseline + (1-alpha) * rewards.mean().item()
# REINFORCE 损失
loss = -( (rewards - baseline) * log_probs ).mean()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
opt.step()
if epoch % 50 == 0:
pbar.set_description(f"Len: {lengths.mean():.3f} | Loss: {loss.item():.3f}")
# ====================== 7. 推理测试 ======================
print("\n推理测试...")
with torch.no_grad():
test = generate_tsp_batch(1, TSP_SIZE)
tour, _ = model(test)
length = compute_tour_length(test, tour).item()
print(f"测试路径长度: {length:.4f}")
print("访问顺序:", tour[0].cpu().numpy())
代码说明(极简版)
- Encoder
- Transformer 编码城市坐标,用自注意力捕捉全局空间关系
- 加位置编码保证排列不变性
- Decoder(Pointer Network)
- 自回归逐个选下一个城市
- 用 mask 屏蔽已访问节点,保证合法路径
- 注意力直接“指向”下一个节点
- RL 训练(REINFORCE)
- 奖励:
reward = -tour_length(越短奖励越高) - 滑动平均基线减小梯度方差
- 策略梯度直接最大化期望奖励
- 奖励:
- 效果
- TSP50 训练 2000 轮后,路径质量显著优于贪心
- 速度远快于 Concorde 等精确求解器
- 是近似最优,不是严格最优(NP-hard)
你可以进一步改强的方向
- 换成 PPO 替代 REINFORCE,更稳定
- 加 Critic 网络(Actor-Critic)
- 加入 2-opt 局部搜索后处理
- 用稀疏注意力/图简化支持 TSP100、TSP200
需要我把它改成 PPO 版本 或者加上 Critic(AC) 吗?
相关工具和资料
求解器
- 线性规划求解器
- 整数线性规划求解器
- python 调用求解器
最新进展看相关论文,可以看一些较新的论文,然后里面会有综述章节。然后沿着时间线,也可以了解一些新的动态。