【附代码】【6种方法】旅行商问题(TSP)整数规划 VS 启发式——2023.9 持续更新ing


作者:小猪快跑

基础数学&计算数学,从事优化领域5年+,主要研究方向:MIP求解器、整数规划、随机规划、智能优化算法

本文以综述为主,将从常见的多种整数规划建模,如DFJ模型、MTZ模型等,使用Gurobi、Cplex、SCIP、Or-Tools、Cbc等常见求解器,并和常见的启发式LKH等给出性能分析报告,并浅谈其优缺点。

如有错误,欢迎指正。如有更好的算法,也欢迎交流!!!——@小猪快跑

相关文献

问题概述

TSP,即旅行商问题,又称TSP问题(Traveling Salesman Problem),是数学领域中著名问题之一。

假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求所选的路径路程为所有路径中的最小值。

例如,下图显示 一个只有四个位置的 TSP,标记为 A、B、C 和 D。 任意两个位置之间的距离由边旁边的数字给出 加入他们。
在这里插入图片描述

通过计算所有可能路线的距离,您可以看到 最短路线是 ACDBA,其总距离为 。35 + 30 + 15 + 10 = 90

位置越多,问题就越严重。上面的示例中只有六条路线。但是,如果有 10 个位置(不计算起点),路线数量为 362880。对于 20 个位置,该数量将跳转到 2432902008176640000。 对于较大的问题,往往很难在给定的时间得到最优解,我们一般倾向于得到近乎最佳的解决方案。

测试电脑配置

博主三千元电脑的渣渣配置:

CPU model: AMD Ryzen 7 7840HS w/ Radeon 780M Graphics, instruction set [SSE2|AVX|AVX2|AVX512]
Thread count: 8 physical cores, 16 logical processors, using up to 16 threads

测试结果——更新于2023.9

使用著名的TSPLIB 95测试集(有些case因为电脑内存受限报错没有计入)

怀疑官方给出的某些case的bounds存在一些问题(也或许是数值误差?)。比如说:tsp225,在各种方法中我们都得到了比官方给出bound更优的解,但为了保持和官方一致性,我们暂时不对官方数据进行修正,特此说明。

表格中没有时间的说明内存溢出,电脑配置有限无法进行测试。

Name#citiesTypeBoundsDFJ_LazyDFJ_LAZY GapDFJ_Lazy Run Time(s)LKHLKH GapLKH Run Time(s)LKH(10 times)LKH(10 times) GapLKH(10 times) Run Time(s)MTZMTZ GapMTZ Run Time(s)ConcordeConcorde GapConcorde Run Time(s)OrTools RoutingOrTools Routing GapOrTools Routing Run Time(s)OrTools SatOrTools Sat GapOrTools Sat Run Time(s)
burma1414GEO332333230.00%0.00433230.00%033230.00%033230.00%0.13133230.00%033230.00%0.00333230.00%0.019
ulysses1616GEO685968590.00%0.00468590.00%068590.00%068590.00%0.32468590.00%0.0168590.00%0.00368590.00%0.217
gr1717MATRIX208520850.00%0.00820850.00%020850.00%020850.00%0.43220850.00%020850.00%0.00320850.00%0.054
gr2121MATRIX270727070.00%0.00827070.00%027070.00%027070.00%0.00827070.00%027070.00%0.00727070.00%0.068
ulysses2222GEO701370130.00%0.01270130.00%070130.00%070130.00%4.09770130.00%0.0370130.00%0.00670130.00%0.152
gr2424MATRIX127212720.00%0.01712720.00%012720.00%012720.00%0.05712720.00%013143.30%0.00912720.00%0.180
fri2626MATRIX9379370.00%0.0259370.00%09370.00%09370.00%0.2179370.00%09531.71%0.0059370.00%0.169
bayg2929GEO161016100.00%0.04616100.00%016100.00%016100.00%0.28016100.00%0.0117488.57%0.01116100.00%0.214
bays2929GEO202020200.00%0.03620200.00%020200.00%020200.00%0.30320200.00%020200.00%0.01020200.00%0.154
dantzig4242MATRIX6996990.00%0.0396990.00%06990.00%06990.00%1.9316990.00%0.017385.58%0.0266990.00%0.339
swiss4242MATRIX127312730.00%0.09012730.00%012730.00%012730.00%0.39212730.00%0.0113687.46%0.01512730.00%0.359
att4848ATT10628106280.00%0.109106280.00%0106280.00%0106280.00%20.482106280.00%0.03108361.96%0.039106280.00%0.612
gr4848MATRIX504650460.00%0.24250460.00%050460.00%0.0150460.00%3.96050460.00%0.0350971.01%0.02450460.00%1.308
hk4848MATRIX11461114610.00%0.104114610.00%0114610.00%0114610.00%0.766114610.00%0.01118473.37%0.027114610.00%0.451
eil5151EUC_2D4264260.00%0.1424260.00%04260.00%0.014260.00%0.6264260.00%0.024393.05%0.0304260.00%1.114
berlin5252EUC_2D754275420.00%0.03275420.00%075420.00%075420.00%0.33175420.00%0.0179445.33%0.03075420.00%0.247
brazil5858MATRIX25395253950.00%0.122253950.00%0253950.00%0.01253950.00%101.686253950.00%0.03259372.13%0.032253950.00%0.574
st7070EUC_2D6756750.00%0.2676750.00%06750.00%0.016750.00%23.0766750.00%0.046831.19%0.0996750.00%4.584
eil7676EUC_2D5385380.00%0.2315380.00%05380.00%0.015380.00%1.3385380.00%0.025481.86%0.0735380.00%1.129
pr7676EUC_2D1081591081590.00%3.3901081590.00%01081590.00%0.041081590.00%38.6971081590.00%0.121109482.58%0.0721081590.00%117.067
gr9696GEO55209552090.00%1.540552090.00%0552090.00%0.06552090.00%156.626552090.00%0.12569253.11%0.152552090.00%13.319
rat9999EUC_2D121112110.00%0.64812110.00%012110.00%0.0112110.00%3.07512110.00%0.0612846.03%0.10112110.00%5.752
kroA100100EUC_2D21282212820.00%1.586212820.00%0212820.00%0.03212820.00%79.691212820.00%0.05219603.19%0.138212820.00%12.701
kroB100100EUC_2D22141221410.00%2.607221410.00%0221410.00%0.05221410.00%600221410.00%0.1229453.63%0.138221410.00%16.237
kroC100100EUC_2D20749207490.00%0.757207490.00%0207490.00%0.01207490.00%58.760207490.00%0.05216994.58%0.110207490.00%7.455
kroD100100EUC_2D21294212940.00%0.991212940.00%0212940.00%0.03214500.73%600212940.00%0.07224395.38%0.122212940.00%7.964
kroE100100EUC_2D22068220680.00%1.695220680.00%0.01220680.00%0.1220680.00%600220680.00%0.11225512.19%0.182220680.00%15.506
rd100100EUC_2D791079100.00%0.52479100.00%079100.00%0.0179100.00%60079100.00%0.0482213.93%0.14079100.00%3.946
eil101101EUC_2D6296290.00%0.4316290.00%06290.00%0.026290.00%6006290.00%0.046503.34%0.1846290.00%7.013
lin105105EUC_2D14379143790.00%0.348143790.00%0143790.00%0.01143790.00%600143790.00%0.03153636.84%0.280143790.00%3.139
pr107107EUC_2D44303443030.00%0.452443030.00%0443030.00%0.03444740.39%600443030.00%0.07445730.61%0.167443030.00%3.989
gr120120MATRIX694269420.00%1.58269420.00%069420.00%0.0369420.00%60069420.00%0.0871162.51%0.38269420.00%14.897
pr124124EUC_2D59030590300.00%2.601590300.00%0590300.00%0.04590760.08%600590300.00%0.84604132.34%0.142590300.00%25.276
bier127127EUC_2D1182821182820.00%3.6161182820.00%01182820.00%0.041182820.00%6001182820.00%0.11217292.91%0.3201182820.00%15.329
ch130130EUC_2D611061100.00%3.28161100.00%061100.00%0.0462171.75%60061100.00%0.1263293.58%0.25361100.00%15.927
pr136136EUC_2D96772967720.00%4.480967720.00%0.01967720.00%0.1967720.00%600967720.00%0.21028136.24%0.344967720.00%52.818
gr137137GEO69853698530.00%2.404698530.00%0698530.00%0.04698530.00%600698530.00%0.16720283.11%0.317698530.00%27.201
pr144144EUC_2D58537585370.00%2.101585370.00%0.01585370.00%0.07587630.39%600585370.00%0.17592861.28%0.482585370.00%23.272
ch150150EUC_2D652865280.00%6.96465320.06%0.0465280.00%0.4265280.00%60065280.00%0.2567333.14%0.45465280.00%43.855
kroA150150EUC_2D26524265240.00%5.665265240.00%0265240.00%0.05265250.00%600265240.00%0.24275033.69%0.270265240.00%74.588
kroB150150EUC_2D26130261300.00%5.698261310.00%0.06261300.00%0.6261880.22%600261300.00%0.38266712.07%0.359261300.00%75.159
pr152152EUC_2D73682736820.00%12.436736820.00%0.02736820.00%0.19752812.17%600736820.00%0.48758322.92%0.570736820.00%42.787
u159159EUC_2D42080420800.00%2.733420800.00%0420800.00%0.02420800.00%600420800.00%0.06434033.14%0.815420800.00%13.925
si175175MATRIX21407214070.00%10.557214070.00%0.01214070.00%0.14214220.07%600214070.00%1.56215130.50%0.652214070.00%126.282
brg180180MATRIX195019500.00%1.19619500.00%019500.00%0.0419500.00%60019500.00%0.2819600.51%0.47119500.00%7.529
rat195195EUC_2D232323230.00%39.38423230.00%0.0223230.00%0.1823230.00%60023230.00%1.2423752.24%0.81223230.00%275.161
d198198EUC_2D15780157800.00%16.663157800.00%0.06157800.00%0.57159741.23%600157800.00%0.68160051.43%0.741157800.00%83.168
kroA200200EUC_2D29368293680.00%14.420293680.00%0.01293680.00%0.16293820.05%600293680.00%0.27298741.72%0.544293680.00%326.296
kroB200200EUC_2D29437294370.00%22.398294370.00%0294370.00%0.05298001.23%600294370.00%0.17311105.68%0.840294370.00%143.404
gr202202GEO40160401600.00%10.337401600.00%0401600.00%0.04401600.00%600401600.00%0.3422185.12%0.832401600.00%35.310
ts225225EUC_2D1266436001266430.00%0.011266430.00%0.111275870.75%6001266430.00%2.111277630.88%0.7661272290.46%664.010
tsp225225EUC_2D39193916-0.08%92.8043916-0.08%0.043916-0.08%0.4239791.53%6003916-0.08%0.8641175.05%1.04239300.28%627.207
pr226226EUC_2D80369803690.00%14.744803690.00%0.01803690.00%0.08813081.17%600803690.00%0.25831133.41%0.857803690.00%252.200
gr229229GEO1346021346020.00%13.9561346120.01%0.11346020.00%11346020.00%6001346020.00%1.841400144.02%0.7911351010.37%631.349
gil262262EUC_2D237823780.00%41.07623780.00%0.0323780.00%0.3123980.84%60023780.00%0.5725175.85%1.88923800.08%628.027
pr264264EUC_2D49135491350.00%49.310491350.00%0.02491350.00%0.285528912.52%600491350.00%0.15514954.80%2.470491350.00%236.860
a280280EUC_2D257925790.00%19.93025790.00%025790.00%0.0825790.00%60025790.00%0.3127426.32%1.54425790.00%299.732
pr299299EUC_2D48191481910.00%193.045481910.00%0.07481910.00%0.71484720.58%600481910.00%1.01506175.03%1.819487691.20%662.902
lin318318EUC_2D42029420290.00%39.310420750.11%0.17420290.00%1.79452737.72%600420290.00%1.18435503.62%3.436422850.61%652.776
rd400400EUC_2D15281152810.00%281.219152810.00%0.04152810.00%0.481773316.05%600152810.00%7.24158203.53%5.347154310.98%644.397
fl417417EUC_2D11861600118710.09%1.33118610.00%13.371439621.37%600118610.00%3.8121642.55%3.113118780.14%678.489
gr431431GEO1714146001715220.06%0.791714140.00%8.0222165529.31%6001714140.00%101773153.44%5.7221723990.57%675.936
pr439439EUC_2D1072176001072210.00%0.121072170.00%1.312899220.31%6001072170.00%9.951171719.28%4.8691091061.76%654.776
pcb442442EUC_2D50778600507780.00%0.11507780.00%1.245745813.16%600507780.00%11.97523903.17%5.990539416.23%644.089
d493493EUC_2D35002600350030.00%0.63350020.00%6.385921169.16%600350020.00%14.03367374.96%8.003359682.76%644.176
att532532ATT27686600276860.00%0.13276860.00%1.415259689.97%600276860.00%11.99287853.97%8.977280351.26%644.428
ali535535GEO2023106002023390.01%1.972023390.01%19.8123519316.25%6002023390.01%1.682181717.84%10.9262052381.45%635.610
si535535MATRIX48450600484530.01%1.66484500.00%16.685389711.24%600484500.00%3.41488430.81%9.206488930.91%642.263
pa561561MATRIX276360027630.00%0.2427630.00%2.56426454.33%60027630.00%15.7429245.83%13.06929637.24%673.489
u574574EUC_2D36905600369080.01%0.18369050.00%1.995267042.72%600369050.00%3.16387174.91%12.583377182.20%656.241
rat575575EUC_2D677360067730.00%0.1267730.00%1.3803418.62%60067730.00%15.9371465.51%11.31572667.28%658.255
p654654EUC_2D34643600346430.00%0.75346430.00%7.75393055.67%600346430.00%1.49349840.98%15.3364349425.55%668.280
d657657EUC_2D48912600489130.00%0.86489130.00%8.77115563136.27%600489130.00%13.84514995.29%18.6375417510.76%654.260
gr666666GEO2943586002944520.03%1.712943580.00%17.29698914137.44%6002943580.00%4.33069094.26%16.51635884021.91%659.841
u724724EUC_2D41910600419100.00%0.29419100.00%3.1890860116.80%600419100.00%10.07439684.91%22.4854856415.88%659.095
rat783783EUC_2D880660088060.00%0.0188060.00%0.4160088060.00%2.4192344.86%34.6221013515.09%687.173
dsj10001000CEIL_2D18659688600186623540.01%13.06186601880.00%131.13600186601880.00%37.56201737638.11%70.1752729042546.25%733.970
pr10021002EUC_2D2590456002590450.00%0.232590450.00%2.926002590450.00%2.752718934.96%60.72033206628.19%755.929
si10321032MATRIX92650600926900.04%0.98926500.00%10.22600926500.00%1.95927310.09%54.85814482256.31%781.457
u10601060EUC_2D2240946002241130.01%11.812240940.00%118.686002240940.00%70.482362975.45%62.83425433013.49%774.805
vm10841084EUC_2D2392976002393350.02%3.112392970.00%31.726002392970.00%64.732514905.10%67.09940956051611.52%761.969
pcb11731173EUC_2D56892600568920.00%0.55568920.00%6.17600568920.00%32.66607646.81%98.028178312213.42%819.574
d12911291EUC_2D50801600508360.07%9.35508010.00%94.32600508010.00%11622.49539856.27%124.25217343843314.07%853.088
rl13041304EUC_2D2529486002530370.04%1.332529480.00%14.146002529480.00%19.1227872610.19%119.56450953801914.40%866.635
rl13231323EUC_2D2701996002702240.01%4.692701990.00%47.836002701990.00%682.142843555.24%98.40131639161070.96%887.869
nrw13791379EUC_2D56638600566390.00%2.06566380.00%21.48600566380.00%26.45592314.58%125.2777167271165.45%972.965
fl14001400EUC_2D20127600201650.19%62.61201640.18%626.99600201270.00%582.94210314.49%168.51316902738298.04%921.967
u14321432EUC_2D1529706001529700.00%0.271529700.00%3.716001529700.00%100.291607675.10%173.34124214958.30%957.349
d16551655EUC_2D62128600621280.00%0.82621280.00%9.49600621280.00%15.94653465.18%240.322206258231.99%1128.190
vm17481748EUC_2D3365566003365930.01%5.723365560.00%58.726003365560.00%283.173562015.84%215.322113259583265.25%1281.710

测试数据集

使用著名的TSPLIB 95测试集,这是官方给出的结果:

Name#citiesTypeBounds
burma1414GEO3323
ulysses1616GEO6859
gr1717MATRIX2085
gr2121MATRIX2707
ulysses2222GEO7013
gr2424MATRIX1272
fri2626MATRIX937
bayg2929GEO1610
bays2929GEO2020
dantzig4242MATRIX699
swiss4242MATRIX1273
att4848ATT10628
gr4848MATRIX5046
hk4848MATRIX11461
eil5151EUC_2D426
berlin5252EUC_2D7542
brazil5858MATRIX25395
st7070EUC_2D675
eil7676EUC_2D538
pr7676EUC_2D108159
gr9696GEO55209
rat9999EUC_2D1211
kroA100100EUC_2D21282
kroB100100EUC_2D22141
kroC100100EUC_2D20749
kroD100100EUC_2D21294
kroE100100EUC_2D22068
rd100100EUC_2D7910
eil101101EUC_2D629
lin105105EUC_2D14379
pr107107EUC_2D44303
gr120120MATRIX6942
pr124124EUC_2D59030
bier127127EUC_2D118282
ch130130EUC_2D6110
pr136136EUC_2D96772
gr137137GEO69853
pr144144EUC_2D58537
ch150150EUC_2D6528
kroA150150EUC_2D26524
kroB150150EUC_2D26130
pr152152EUC_2D73682
u159159EUC_2D42080
si175175MATRIX21407
brg180180MATRIX1950
rat195195EUC_2D2323
d198198EUC_2D15780
kroA200200EUC_2D29368
kroB200200EUC_2D29437
gr202202GEO40160
ts225225EUC_2D126643
tsp225225EUC_2D3919
pr226226EUC_2D80369
gr229229GEO134602
gil262262EUC_2D2378
pr264264EUC_2D49135
a280280EUC_2D2579
pr299299EUC_2D48191
lin318318EUC_2D42029
linhp318318EUC_2D41345
rd400400EUC_2D15281
fl417417EUC_2D11861
gr431431GEO171414
pr439439EUC_2D107217
pcb442442EUC_2D50778
d493493EUC_2D35002
att532532ATT27686
ali535535GEO202310
si535535MATRIX48450
pa561561MATRIX2763
u574574EUC_2D36905
rat575575EUC_2D6773
p654654EUC_2D34643
d657657EUC_2D48912
gr666666GEO294358
u724724EUC_2D41910
rat783783EUC_2D8806
dsj10001000CEIL_2D18659688
pr10021002EUC_2D259045
si10321032MATRIX92650
u10601060EUC_2D224094
vm10841084EUC_2D239297
pcb11731173EUC_2D56892
d12911291EUC_2D50801
rl13041304EUC_2D252948
rl13231323EUC_2D270199
nrw13791379EUC_2D56638
fl14001400EUC_2D20127
u14321432EUC_2D152970
fl15771577EUC_2D[22204,22249]
d16551655EUC_2D62128
vm17481748EUC_2D336556
u18171817EUC_2D57201
rl18891889EUC_2D316536
d21032103EUC_2D[79952,80450]
u21522152EUC_2D64253
u23192319EUC_2D234256
pr23922392EUC_2D378032
pcb30383038EUC_2D137694
fl37953795EUC_2D[28723,28772]
fnl44614461EUC_2D182566
rl59155915EUC_2D[565040,565530]
rl59345934EUC_2D[554070,556045]
pla73977397CEIL_2D23260728
rl1184911849EUC_2D[920847,923368]
usa1350913509EUC_2D[19947008,19982889]
brd1405114051EUC_2D[468942,469445]
d1511215112EUC_2D[1564590,1573152]
d1851218512EUC_2D[644650,645488]
pla3381033810CEIL_2D[65913275,66116530]
pla8590085900CEIL_2D[141904862,142487006]

测试代码

DFJ_Lazy

import glob
import networkx as nx
import numpy as np
import gurobipy as gp
import pandas as pd
from gurobipy import GRB, quicksum
import tsplib95


class DfjLazy:
    def __init__(self, cost: np.ndarray, problem=None):
        self.problem = problem
        self.cost = cost
        self.dimension = len(cost)
        self.x = {}
        self.m = None

    @classmethod
    def read_tsplib95(cls, path):
        problem = tsplib95.load(path)
        cost = np.zeros([problem.dimension, problem.dimension], dtype=int)
        for i, start_node in enumerate(problem.get_nodes()):
            for j, end_node in enumerate(problem.get_nodes()):
                cost[i, j] = problem.get_weight(start_node, end_node)
        return cls(cost, problem)

    def create_solver(self, params: dict = None):
        self.m = gp.Model("DFJ_Lazy")
        self.m.setParam('lazyConstraints', 1)
        for param_name, new_val in params.items():
            self.m.setParam(param_name, new_val)

    def add_vars(self):
        for i in range(self.dimension):
            for j in range(self.dimension):
                if i == j:
                    continue
                self.x[i, j] = self.m.addVar(vtype=GRB.BINARY, name="x(%s,%s)" % (i, j))

    def set_obj(self):
        self.m.setObjective(quicksum(self.cost[i, j] * self.x[i, j] for (i, j) in self.x), GRB.MINIMIZE)

    def add_conss(self):
        for i in range(self.dimension):
            self.m.addConstr(quicksum(self.x[i, j] for j in range(self.dimension) if j != i) == 1, "Out(%s)" % i)
            self.m.addConstr(quicksum(self.x[j, i] for j in range(self.dimension) if j != i) == 1, "In(%s)" % i)

    def solve(self):
        def subtour_elimination(model, where):
            if where == GRB.Callback.MIPSOL:
                x_sol = model.cbGetSolution(self.x)

                graph = nx.Graph()
                for k, sol in x_sol.items():
                    if sol > 0.5:
                        graph.add_edge(k[0], k[1])
                for c in nx.connected_components(graph):
                    if len(c) < self.dimension:
                        model.cbLazy(quicksum(self.x[i, j] for i in c for j in c if i != j) <= len(c) - 1)

        self.m.optimize(subtour_elimination)
        return self.get_sol()

    def get_status(self):
        match self.m.status:
            case GRB.status.OPTIMAL:
                return 'OPTIMAL'
            case GRB.status.TIME_LIMIT:
                return 'TIME_LIMIT'

    def get_sol(self):
        status = self.get_status()
        gap = self.m.MIPGap
        lb = self.m.ObjBoundC
        ub = self.m.ObjVal
        runtime = self.m.Runtime

        return {'gap': gap, 'status': status, 'lb': lb, 'ub': ub, 'runtime': runtime}

    def make_model(self, params: dict = None):
        self.create_solver(params)
        self.add_vars()
        self.add_conss()
        self.set_obj()


if __name__ == '__main__':
    res_file = 'result.csv'
    df = pd.DataFrame(columns=['Method', 'Name', '#cities', 'LB', 'UB', 'Run Time'])
    df.to_csv(res_file, index=False)
    for path in glob.glob('D:/Data/ALL_tsp/*.tsp'):
        print(path)
        m = DfjLazy.read_tsplib95(path)
        p = m.problem
        m.make_model({'OutputFlag': 1, 'TimeLimit': 600, 'MIPGap': 0})
        sol = m.solve()
        with open(res_file, 'a') as f:
            f.write(f"DFJ_Lazy,{p.name},{p.dimension},{sol['lb']},{sol['ub']},{sol['runtime']}\n")

MTZ

import glob
import networkx as nx
import numpy as np
import gurobipy as gp
import pandas as pd
from gurobipy import GRB, quicksum
import tsplib95


class MtzStrong:
    def __init__(self, cost: np.ndarray, problem=None):
        self.problem = problem
        self.cost = cost
        self.dimension = len(cost)
        self.n = self.dimension
        self.mu = None
        self.x = None
        self.m = None

    @classmethod
    def read_tsplib95(cls, path):
        problem = tsplib95.load(path)
        cost = np.zeros([problem.dimension, problem.dimension], dtype=int)
        for i, start_node in enumerate(problem.get_nodes()):
            for j, end_node in enumerate(problem.get_nodes()):
                cost[i, j] = problem.get_weight(start_node, end_node)
        return cls(cost, problem)

    def create_solver(self, params: dict = None):
        self.m = gp.Model("MTZ - Strong")
        for param_name, new_val in params.items():
            self.m.setParam(param_name, new_val)

    def add_vars(self):
        self.x = {}
        self.mu = {}

        for i in range(self.dimension):
            self.mu[i] = self.m.addVar(lb=0, ub=self.n, vtype=GRB.CONTINUOUS, name="mu(%s)" % i)
            for j in range(self.dimension):
                if i == j:
                    continue
                self.x[i, j] = self.m.addVar(vtype=GRB.BINARY, name="x(%s,%s)" % (i, j))

        self.m.addConstr(self.mu[0] == 0, "mu[0]")

    def set_obj(self):
        self.m.setObjective(quicksum(self.cost[i, j] * self.x[i, j] for (i, j) in self.x), GRB.MINIMIZE)

    def add_in_out_cons(self):
        for i in range(self.dimension):
            self.m.addConstr(quicksum(self.x[i, j] for j in range(self.dimension) if j != i) == 1, "Out(%s)" % i)
            self.m.addConstr(quicksum(self.x[j, i] for j in range(self.dimension) if j != i) == 1, "In(%s)" % i)

    def add_lifted_mtz_cons(self):
        for i in range(self.dimension):
            for j in range(1, self.dimension):
                if i == j:
                    continue
                self.m.addConstr(self.mu[i] - self.mu[j] + (self.n - 1) * self.x[i, j] + (self.n - 3) * self.x[
                    j, i] <= self.n - 2, "LiftedMTZ(%s,%s)" % (i, j))
        for i in range(1, self.dimension):
            self.m.addConstr(-self.x[0, i] - self.mu[i] + (self.n - 3) * self.x[i, 0] <= -2, name="LiftedLB(%s)" % i)
            self.m.addConstr(-self.x[i, 0] + self.mu[i] + (self.n - 3) * self.x[0, i] <= self.n - 2,
                             name="LiftedUB(%s)" % i)

    def add_conss(self):
        self.add_in_out_cons()
        self.add_lifted_mtz_cons()

    def solve(self):
        self.m.optimize()
        return self.get_sol()

    def get_status(self):
        match self.m.status:
            case GRB.status.OPTIMAL:
                return 'OPTIMAL'
            case GRB.status.TIME_LIMIT:
                return 'TIME_LIMIT'

    def get_sol(self):
        status = self.get_status()
        gap = self.m.MIPGap
        lb = self.m.ObjBoundC
        ub = self.m.ObjVal
        runtime = self.m.Runtime

        return {'gap': gap, 'status': status, 'lb': lb, 'ub': ub, 'runtime': runtime}

    def make_model(self, params: dict = None):
        self.create_solver(params)
        self.add_vars()
        self.add_conss()
        self.set_obj()


if __name__ == '__main__':
    res_file = 'result_mtz_strong.csv'
    df = pd.DataFrame(columns=['Method', 'Name', '#cities', 'LB', 'UB', 'Run Time'])
    df.to_csv(res_file, index=False)
    for path in glob.glob('D:/Data/ALL_tsp/*.tsp'):
        print(path)
        m = MtzStrong.read_tsplib95(path)
        p = m.problem
        m.make_model({'OutputFlag': 1, 'TimeLimit': 600, 'MIPGap': 0})
        sol = m.solve()
        with open(res_file, 'a') as f:
            f.write(f"MTZ_STRONG,{p.name},{p.dimension},{sol['lb']},{sol['ub']},{sol['runtime']}\n")

LKH

Taillard & Helsgaun, 2019 : LKH3

首先看下目录结构:

LKH-3.0.9
├── ALL_tsp
│   ├── INSTANCES
│   │   └── Tsplib95
│   │       ├── a280.tsp
│   │       ├── ali535.tsp
│   │       └── vm1748.tsp
│   ├── LOG
│   ├── run_TSP
│   ├── test.txt
│   └── 旅行商问题(TSP)整数规划 VS 启发式.csv
├── LKH
├── Makefile
├── README.txt
└── SRC

run_TSP

#!/bin/bash
# Usage: ./run_TSP class name runs [ optimum ]

if [ -z "$3" ]; then
    echo "./run_TSP class name runs [ optimum ]"
    exit
fi

lkh="../LKH"
class=$1
name=$2
runs=$3
optimum=$4
par=TMP/$name.pid$$.par

mkdir -p TOURS
mkdir -p TOURS/$class
mkdir -p TMP
# mkdir -p SOLUTIONS
# mkdir -p SOLUTIONS/$class

echo "PROBLEM_FILE = INSTANCES/$class/$name.tsp" >> $par

echo "RUNS = $runs" >> $par
# echo "TOUR_FILE = TOURS/$class/$name.$.tour" >> $par
# echo "SINTEF_SOLUTION_FILE = SOLUTIONS/$class/$name.$.sol" >> $par
echo "SEED = 0" >> $par

if [ -n "$optimum" ]; then
    echo "OPTIMUM = $optimum" >> $par
fi

$lkh $par

/bin/rm -f $par

最后我是采用了python调用脚本的方式,个人感觉写起来方便一点,当然也可以用bash直接批量调用,仅供参考:

import os
import glob
import pandas as pd

os.chdir('/home/user/ClionProjects/LKH-3.0.9/ALL_tsp/INSTANCES/Tsplib95')
os.chdir('/home/user/ClionProjects/LKH-3.0.9/ALL_tsp')
os.getcwd()

df = pd.read_csv('旅行商问题(TSP)整数规划 VS 启发式.csv')
for i, row in df.iterrows():
    with os.popen(f'./run_TSP Tsplib95 {row.Name} 10 {row.Bounds}', "r") as p:
        r = p.read()
    with open(f'LOG/{row.Name}.txt', 'w') as f:
        f.write(r)

res = []
for path in glob.glob('LOG/*'):
    with open(path, 'r') as f:
        t = f.readlines()
    name = path[len('LOG/'):-len('.txt')]
    avg_time = float(t[-3].split(',')[1][len(' Time.avg = '):-len(' sec.')])
    total_time = float(t[-2][len('Time.total = '):-len(' sec.\n')])
    avg_cost = float(t[-6].split(',')[1][len(' Cost.avg = '):])
    min_cost = float(t[-6].split(',')[0][len('Cost.min = '):])
    res.append([name, avg_cost, avg_time, min_cost, total_time])
result = pd.DataFrame(res, columns=['name', 'avg_cost', 'avg_time', 'min_cost', 'total_time'])
result.to_csv('result.csv', index=False)

Concorde

目录结构如下,数据主要放在data下:

pyconcorde
├── COPYING
├── MANIFEST.in
├── README.md
├── build
├── concorde
│   ├── tests
│   │   ├── data
│   │   │   ├── a280.tsp
│   │   │   ├── ali535.tsp
│   │   │   └── vm1748.tsp
│   │   └── test_solution.py
│   ├── tsp.py
│   └── util.py
└── tools

最后我是采用了python调用脚本的方式,个人感觉写起来方便一点,仅供参考:

import glob
import os
import pandas as pd

if __name__ == '__main__':
    df = pd.read_csv('旅行商问题(TSP)整数规划 VS 启发式.csv')
    for i, row in df.iterrows():
        test_py = f'''from concorde.tsp import TSPSolver
from concorde.tests.data_utils import get_dataset_path

fname = get_dataset_path("{row.Name}")
solver = TSPSolver.from_tspfile(fname)
solution = solver.solve()'''
        with open(f'test.py', 'w') as f:
            f.write(test_py)
        with os.popen('python test.py', "r") as p:
            r = p.read()
        with open(f'LOG/{row.Name}.txt', 'w') as f:
            f.write(r)

    res = []
    for path in glob.glob('LOG/*'):
        with open(path, 'r') as f:
            t = f.read().split('\n')
        name = path[len('LOG/'):-len('.txt')]
        time = float(t[-2][len('Total Time to solve TSP: '):])
        cost = float(t[-3][len('Optimal tour: '):])
        res.append([name, cost, time])
    result = pd.DataFrame(res, columns=['name', 'cost', 'time'])
    result.to_csv('result.csv', index=False)

OrTools Routing

from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp

import os
import glob
import time
import numpy as np
import pandas as pd
import tsplib95


class OrToolsRouting:
    def __init__(self, cost: np.ndarray, problem=None):
        self.problem = problem
        self.cost = cost

    @classmethod
    def read_tsplib95(cls, path):
        problem = tsplib95.load(path)
        cost = np.zeros([problem.dimension, problem.dimension], dtype=int)
        for i, start_node in enumerate(problem.get_nodes()):
            for j, end_node in enumerate(problem.get_nodes()):
                cost[i, j] = problem.get_weight(start_node, end_node)
        return cls(cost, problem)

    @staticmethod
    def print_solution(manager, routing, solution):
        """Prints solution on console."""
        print(f"Objective: {solution.ObjectiveValue()} miles")
        index = routing.Start(0)
        plan_output = "Route for vehicle 0:\n"
        route_distance = 0
        while not routing.IsEnd(index):
            plan_output += f" {manager.IndexToNode(index)} ->"
            previous_index = index
            index = solution.Value(routing.NextVar(index))
            route_distance += routing.GetArcCostForVehicle(previous_index, index, 0)
        plan_output += f" {manager.IndexToNode(index)}\n"
        print(plan_output)
        plan_output += f"Route distance: {route_distance}miles\n"

    def run(self):
        # Create the routing index manager.
        manager = pywrapcp.RoutingIndexManager(len(self.cost), 1, 0)

        # Create Routing Model.
        routing = pywrapcp.RoutingModel(manager)

        def distance_callback(from_index, to_index):
            """Returns the distance between the two nodes."""
            # Convert from routing variable Index to distance matrix NodeIndex.
            from_node = manager.IndexToNode(from_index)
            to_node = manager.IndexToNode(to_index)
            return self.cost[from_node][to_node]

        transit_callback_index = routing.RegisterTransitCallback(distance_callback)

        # Define cost of each arc.
        routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)

        # Setting first solution heuristic.
        search_parameters = pywrapcp.DefaultRoutingSearchParameters()
        search_parameters.first_solution_strategy = (routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)
        search_parameters.time_limit.seconds = 600

        # Solve the problem.
        s = time.time()
        solution = routing.SolveWithParameters(search_parameters)
        run_time = time.time() - s

        # Print solution on console.
        if solution:
            self.print_solution(manager, routing, solution)
            return solution.ObjectiveValue(), run_time


if __name__ == '__main__':
    res_file = 'result_ortools_routing.csv'
    for path in glob.glob('D:/Data/ALL_tsp/*.tsp'):
        print(path)
        if tsplib95.load(path).dimension > 1748:
            continue
        m = OrToolsRouting.read_tsplib95(path)
        try:
            obj, run_time = m.run()
        except:
            continue
        res = {'obj': obj, 'run_tim': run_time, 'name': m.problem.name, 'dimension': m.problem.dimension}
        if os.path.exists(res_file):
            pd.DataFrame([res]).to_csv(res_file, index=False, header=False, mode='a+')
        else:
            pd.DataFrame([res]).to_csv(res_file, index=False)

OrTools Sat

import glob
import multiprocessing
import os
import numpy as np
import pandas as pd
import tsplib95
from ortools.sat.python import cp_model


class OrToolsSat:
    def __init__(self, cost: np.ndarray, problem=None):
        self.solver = None
        self.problem = problem
        self.cost = cost
        self.dimension = len(cost)
        self.x = None
        self.m = None

    @classmethod
    def read_tsplib95(cls, path):
        problem = tsplib95.load(path)
        cost = np.zeros([problem.dimension, problem.dimension], dtype=int)
        for i, start_node in enumerate(problem.get_nodes()):
            for j, end_node in enumerate(problem.get_nodes()):
                cost[i, j] = problem.get_weight(start_node, end_node)
        return cls(cost, problem)

    def create_solver(self):
        self.m = cp_model.CpModel()

    def add_vars(self):
        self.x = {}
        for i in range(self.dimension):
            for j in range(self.dimension):
                if i == j:
                    continue
                self.x[i, j] = self.m.NewBoolVar('%i follows %i' % (j, i))

    def add_circuit_cons(self):
        self.m.AddCircuit([[i, j, x] for (i, j), x in self.x.items()])

    def add_conss(self):
        self.add_circuit_cons()

    def set_obj(self):
        self.m.Minimize(sum(self.cost[i, j] * self.x[i, j] for (i, j) in self.x))

    def solve(self):
        # Solve and print out the solution.
        solver = cp_model.CpSolver()
        solver.parameters.log_search_progress = False
        # To benefit from the linearization of the circuit constraint.
        solver.parameters.linearization_level = 2
        # Sets a time limit of 10 seconds.
        solver.parameters.max_time_in_seconds = 600
        # The number of parallel workers (i.e. threads) to use during search
        solver.parameters.num_search_workers = multiprocessing.cpu_count()

        solver.Solve(self.m)

        self.solver = solver

    def get_status(self):
        res = {}
        for s in self.solver.ResponseStats().split('\n')[1:-1]:
            k, v = s.split(':')
            res[k] = v[1:]
        return res

    def get_sol(self):
        current_node = 0
        str_route = '%i' % current_node
        route_is_finished = False
        route_distance = 0
        while not route_is_finished:
            for i in range(self.dimension):
                if i == current_node:
                    continue
                if self.solver.BooleanValue(self.x[current_node, i]):
                    str_route += ' -> %i' % i
                    route_distance += self.cost[current_node][i]
                    current_node = i
                    if current_node == 0:
                        route_is_finished = True
                    break

        print('Route:', str_route)
        print('Travelled distance:', route_distance)

    def run(self):
        self.create_solver()
        self.add_vars()
        self.add_conss()
        self.set_obj()
        self.solve()


if __name__ == '__main__':
    res_file = 'result_ortools_sat.csv'
    for path in glob.glob('D:/Data/ALL_tsp/*.tsp'):
        print(path)
        if tsplib95.load(path).dimension > 1748:
            continue
        m = OrToolsSat.read_tsplib95(path)
        try:
            m.run()
        except:
            continue
        status = m.get_status()
        status['Name'] = m.problem.name
        status['Dimension'] = m.problem.dimension
        if os.path.exists(res_file):
            pd.DataFrame([status]).to_csv(res_file, index=False, header=False, mode='a+')
        else:
            pd.DataFrame([status]).to_csv(res_file, index=False)

算法原理(待补充。。。敬请期待!!!)

min ⁡ ∑ ( i , j ) ∈ A c i j x i j s.t. ∑ j ∈ V , ( i , j ) ∈ A x i j = 1 , ∀ i ∈ V ∑ i ∈ V , ( i , j ) ∈ A x i j = 1 , ∀ j ∈ V ∑ j ∉ S , i ∈ S , ( i , j ) ∈ A x i j ≥ 1 , ∀ S ⊂ V , 2 ≤ ∣ S ∣ ≤ n − 1 x i j ∈ { 0 , 1 } , ∀ ( i , j ) ∈ A \begin{align} \min \quad &\sum_{(i, j)\in A} c_{ij}x_{ij}\\ \text{s.t.} \quad & \sum_{j \in V, (i,j)\in A} x_{ij} = 1, \quad \forall i \in V \\ & \sum_{i \in V, (i,j)\in A} x_{ij} = 1, \quad \forall j \in V \\ & \sum_{j\notin S, i\in S, (i,j)\in A} x_{ij} \ge 1, \quad \forall S \subset V, 2\le |S| \le n-1 \\ & x_{ij} \in \{0, 1\}, \quad \forall (i, j) \in A \end{align} mins.t.(i,j)AcijxijjV,(i,j)Axij=1,iViV,(i,j)Axij=1,jVj/S,iS,(i,j)Axij1,SV,2Sn1xij{0,1},(i,j)A

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值