这次博客的内容是关于我们算法课程的期末project:Capacited Facility Location Problem。我使用的是python语言求解。这道题乍一看像是一个线性规划问题,但最终我还是选择了贪心算法以及模拟退火算法。源代码即各种数据可见:github项目地址
首先,我们看看这个问题的约束条件以及目标函数。目标函数肯定是求总的opencost以及assigncost的最小值,而约束则是每个factory最终存放的所有customer的demand之和不能超过它自身capacity。
这样一来,我们首先可以定义一些全局变量了。
COST = 0 #目标函数
FACNUM = 0 #工厂数量
CUSNUM = 0 #顾客数量
CAPACITY = [] #理论最大值
LOAD = [] #实际装载量
OPENCOST = [] #每个fac的打开开销
STATUS = [] #工厂状态,打开为1,关闭为0
DEMAND = [] #每个顾客的需求
ASSIGNCOST = [] #每个工厂对于每个人存放其需求的开销
ASSIGN = [] #顾客将其需求存放在哪些工厂
然后,文件读取函数如下:
def readFile(filePath):
global COST, FACNUM, CUSNUM, CAPACITY, LOAD, OPENCOST, DEMAND, ASSIGNCOST, STATUS, ASSIGN
#清空
COST = 0
FACNUM = 0
CUSNUM = 0
CAPACITY = []
OPENCOST = []
DEMAND = []
ASSIGNCOST = []
fopen = open('Instances/' + filePath)
line = fopen.readline().split()
FACNUM = int(line[0])
CUSNUM = int(line[1])
STATUS = [0] * FACNUM
ASSIGN = [0] * CUSNUM
LOAD = [0] * FACNUM
for i in range(FACNUM):
line = fopen.readline().split()
CAPACITY.append(int(line[0]))
OPENCOST.append(int(line[1]))
for i in range(int(CUSNUM/10)):
line = fopen.readline().replace('.', '').split()
for j in range(10):
DEMAND.append(int(line[j]))
for i in range(FACNUM):
eachFac = []
for j in range(int(CUSNUM/10)):
line = fopen.readline().replace('.', '').split()
for k in range(10):
eachFac.append(int(line[k]))
ASSIGNCOST.append(eachFac)
#print(ASSIGNCOST)
fopen.close()
接下来,由于此问题是一个NP完全问题,无法在多项式时间内找到最优解。于是我决定先使用Greedy算法找到一个比较不错的局部最优解,然后用其结果,再进行模拟退火,争取收敛到全局最优解。
贪心算法的思路很简单,遍历每个顾客,看看他们的需求还能放进哪些未满的工厂。接着在所有候选工厂中选择费用最低的那个并放入。
核心代码如下:
for cus in range(CUSNUM):
availableFac = []
for fac in range(FACNUM):
if CAPACITY[fac] >= DEMAND[cus]:
availableFac.append(fac)
if len(availableFac) == 0:
return
index = availableFac[0]
minCost = ASSIGNCOST[index][cus]
for fac in availableFac:
tempCost = ASSIGNCOST[fac][cus]
if tempCost < minCost:
index = fac
minCost = tempCost
if STATUS[index] == 0:
STATUS[index] = 1
COST += minCost + OPENCOST[index]
else:
COST += minCost
CAPACITY[index] -= DEMAND[cus]
ASSIGN[cus] = index
注意,这个贪心的策略可能有不同,有些人说我们不应该光看这个顾客对于哪个工厂的费用最小,还要看那个工厂是否已经打开,如果没打开我们需要加工厂的打开费用。这里我想说的是,评估函数我做过三个实验:1.tempCost = ASSIGNCOST[fac][cus] 2. tempCost = ASSIGNCOST[fac][cus] + OPENCOST[fac][cus] 3.就像之前所说的,如果工厂没打开,则按照2式计算;工厂已经打开,则是按照1式进行计算。结果是,1式的最终结果最好。仔细思索了一下,应该是工厂少顾客多的缘故,顾客一多,我们就无需考略工厂的打开费用了。
贪心算法源代码:贪心算法
对于71个例子,结果和时间如下:
file | result | time |
---|---|---|
p1 | 9440 | 0.0 |
p2 | 8126 | 0.0 |
p3 | 10126 | 0.0 |
p4 | 12126 | 0.0009975433349609375 |
p5 | 9375 | 0.0 |
p6 | 8061 | 0.000997304916381836 |
p7 | 10061 | 0.0 |
p8 | 12061 | 0.0 |
p9 | 9040 | 0.0 |
p10 | 7726 | 0.0 |
p11 | 9726 | 0.0 |
p12 | 11726 | 0.0 |
p13 | 12032 | 0.0 |
p14 | 9180 | 0.0 |
p15 | 13180 | 0.0009620189666748047 |
p16 | 17180 | 0.000995635986328125 |
p17 | 12032 | 0.0 |
p18 | 9180 | 0.0 |
p19 | 13180 | 0.0 |
p20 | 17180 | 0.0009975433349609375 |
p21 | 12032 | 0.0009980201721191406 |
p22 | 9180 | 0.0 |
p23 | 13180 | 0.0 |
p24 | 17180 | 0.0 |
p25 | 19197 | 0.001996278762817383 |
p26 | 16131 | 0.000997304916381836 |
p27 | 21531 | 0.0009965896606445312 |
p28 | 26931 | 0.000997304916381836 |
p29 | 19305 | 0.0009975433349609375 |
p30 | 16239 | 0.000997304916381836 |
p31 | 21639 | 0.000997304916381836 |
p32 | 27039 | 0.0009975433349609375 |
p33 | 19055 | 0.000993490219116211 |
p34 | 15989 | 0.0009989738464355469 |
p35 | 21389 | 0.0009970664978027344 |
p36 | 26789 | 0.001995086669921875 |
p37 | 19055 | 0.000997304916381836 |
p38 | 15989 | 0.0009942054748535156 |
p39 | 21389 | 0.0009968280792236328 |
p40 | 26789 | 0.000997304916381836 |
p41 | 7226 | 0.0 |
p42 | 9957 | 0.0 |
p43 | 12448 | 0.0009970664978027344 |
p44 | 7585 | 0.0 |
p45 | 9848 | 0.0 |
p46 | 12639 | 0.0009970664978027344 |
p47 | 6634 | 0.0 |
p48 | 9044 | 0.0 |
p49 | 12420 | 0.0009984970092773438 |
p50 | 10062 | 0.0 |
p51 | 11351 | 0.0 |
p52 | 10364 | 0.0 |
p53 | 12470 | 0.000997304916381836 |
p54 | 10351 | 0.0 |
p55 | 11970 | 0.0009963512420654297 |
p56 | 23882 | 0.0009970664978027344 |
p57 | 32882 | 0.000997304916381836 |
p58 | 53882 | 0.0019948482513427734 |
p59 | 39121 | 0.0009980201721191406 |
p60 | 23882 | 0.000997304916381836 |
p61 | 32882 | 0.0009975433349609375 |
p62 | 53882 | 0.000997304916381836 |
p63 | 39121 | 0.001994609832763672 |
p64 | 23882 | 0.001994609832763672 |
p65 | 32882 | 0.000997304916381836 |
p66 | 53882 | 0.0009970664978027344 |
p68 | 23882 | 0.0009965896606445312 |
p69 | 32882 | 0.0009832382202148438 |
p70 | 53882 | 0.0019941329956054688 |
p71 | 39121 | 0.0019948482513427734 |
每个例子的具体结果:贪心算法测例具体结果
模拟0退火算法的步骤我就不详细说了,这个大家可以去自行百度。简而言之,对于贪心算法求出的结果,我用作了SA的初始解。邻域产生新解我使用了两种操作:1.随机选出一个工厂和顾客,把这个顾客的需求从原来的工厂放入随机选出的工厂。 2.随机选出两个顾客,把他们各自的需要放入对方的工厂。当然,这么操作可能会使某些工厂的承载超过capacity,那么就要重新选择顾客或者工厂了。
对于产生的新解,我们计算其新的cost。按照模拟退火算法,如果新解小于初始解,则更新它;否则,按照Metropolis准则判断是否接受这个解。
另外,退火的初温我选择了100,末温为1。退火速度为0.99。内层循环为100次。
模拟退火核心代码如下:
def SA():
global COST, LOAD, ASSIGN, STATUS
T0 = 100
T = T0
Tmin = 1
alpha = 0.99
innerIter = 100
while T > Tmin:
for i in range(innerIter):
if np.random.rand() > 0.5: #选一个customo去别的fac
randCus = np.random.randint(0, CUSNUM)
initFacOfCus = ASSIGN[randCus]
randFac = np.random.randint(0, FACNUM)
while randFac == initFacOfCus:
randFac = np.random.randint(0, FACNUM)
while LOAD[randFac] + DEMAND[randCus] > CAPACITY[randFac]:
randCus = np.random.randint(0, CUSNUM)
initFacOfCus = ASSIGN[randCus]
while randFac == initFacOfCus:
randFac = np.random.randint(0, FACNUM)
dValue = ASSIGNCOST[randFac][randCus] - ASSIGNCOST[initFacOfCus][randCus]
if dValue < 0:
COST += dValue
LOAD[randFac] += DEMAND[randCus]
LOAD[initFacOfCus] -= DEMAND[randCus]
if STATUS[randFac] == 0:
STATUS[randFac] = 1
COST += OPENCOST[randFac]
if LOAD[initFacOfCus] == 0:
STATUS[initFacOfCus] = 0
COST -= OPENCOST[initFacOfCus]
ASSIGN[randCus] = randFac
else:
if np.random.rand() < np.exp(-(dValue) / T): # 接受新解
COST += dValue
LOAD[randFac] += DEMAND[randCus]
LOAD[initFacOfCus] -= DEMAND[randCus]
if STATUS[randFac] == 0:
STATUS[randFac] = 1
COST += OPENCOST[randFac]
if LOAD[initFacOfCus] == 0:
STATUS[initFacOfCus] = 0
COST -= OPENCOST[initFacOfCus]
ASSIGN[randCus] = randFac
else: #选两个cus对调fac
randCus1 = 0
randCus2 = 0
while randCus1 == randCus2:
randCus1 = np.random.randint(0, CUSNUM)
randCus2 = np.random.randint(0, CUSNUM)
initFac1 = ASSIGN[randCus1]
initFac2 = ASSIGN[randCus2]
while (LOAD[initFac1] - DEMAND[randCus1] + DEMAND[randCus2] > CAPACITY[initFac1]) or (LOAD[initFac2] - DEMAND[randCus2] + DEMAND[randCus1] > CAPACITY[initFac2]) :
randCus1 = 0
randCus2 = 0
while randCus1 == randCus2:
randCus1 = np.random.randint(0, CUSNUM)
randCus2 = np.random.randint(0, CUSNUM)
initFac1 = ASSIGN[randCus1]
initFac2 = ASSIGN[randCus2]
dValue = ASSIGNCOST[initFac1][randCus2] + ASSIGNCOST[initFac2][randCus1] - ASSIGNCOST[initFac1][randCus1] - ASSIGNCOST[initFac2][randCus2]
if dValue < 0:
COST += dValue
LOAD[initFac1] += DEMAND[randCus2] - DEMAND[randCus1]
LOAD[initFac2] += DEMAND[randCus1] - DEMAND[randCus2]
ASSIGN[randCus1] = initFac2
ASSIGN[randCus2] = initFac1
else:
if np.random.rand() < np.exp(-(dValue) / T): # 接受新解
COST += dValue
LOAD[initFac1] += DEMAND[randCus2] - DEMAND[randCus1]
LOAD[initFac2] += DEMAND[randCus1] - DEMAND[randCus2]
ASSIGN[randCus1] = initFac2
ASSIGN[randCus2] = initFac1
T = T * alpha
#print(T, COST)
print("The Last Cost is:", COST)
SA算法源代码:SA算法
对于71个例子,结果和时间如下:
file | result | time |
---|---|---|
p1 | 9336 | 0.39295339584350586 |
p2 | 8009 | 0.40195631980895996 |
p3 | 10010 | 0.3580358028411865 |
p4 | 11942 | 0.39995670318603516 |
p5 | 9173 | 0.39095616340637207 |
p6 | 7891 | 0.3560817241668701 |
p7 | 9859 | 0.38598132133483887 |
p8 | 11861 | 0.3919527530670166 |
p9 | 9040 | 0.26628851890563965 |
p10 | 7727 | 0.27729058265686035 |
p11 | 9726 | 0.2722742557525635 |
p12 | 11727 | 0.26728224754333496 |
p13 | 12032 | 0.2682836055755615 |
p14 | 9180 | 0.2622981071472168 |
p15 | 13180 | 0.2642946243286133 |
p16 | 17180 | 0.26132726669311523 |
p17 | 12032 | 0.26126933097839355 |
p18 | 9180 | 0.2593350410461426 |
p19 | 13180 | 0.2573113441467285 |
p20 | 17180 | 0.2563450336456299 |
p21 | 12032 | 0.25829219818115234 |
p22 | 9180 | 0.260312557220459 |
p23 | 13180 | 0.2623291015625 |
p24 | 17180 | 0.26133203506469727 |
p25 | 18164 | 0.2712745666503906 |
p26 | 15141 | 0.27324533462524414 |
p27 | 19427 | 0.28224611282348633 |
p28 | 25475 | 0.27828550338745117 |
p29 | 19061 | 0.28325653076171875 |
p30 | 15713 | 0.2812771797180176 |
p31 | 20933 | 0.29720377922058105 |
p32 | 26148 | 0.28127551078796387 |
p33 | 19474 | 0.26928067207336426 |
p34 | 15433 | 0.26632142066955566 |
p35 | 21425 | 0.26628828048706055 |
p36 | 24729 | 0.2622997760772705 |
p37 | 17564 | 0.265291690826416 |
p38 | 15754 | 0.27227115631103516 |
p39 | 20944 | 0.2613034248352051 |
p40 | 26137 | 0.2603330612182617 |
p41 | 7100 | 0.31117701530456543 |
p42 | 9957 | 0.26332855224609375 |
p43 | 12458 | 0.25634098052978516 |
p44 | 7151 | 0.3241691589355469 |
p45 | 9859 | 0.2593069076538086 |
p46 | 12325 | 0.2543201446533203 |
p47 | 6318 | 0.4059157371520996 |
p48 | 9048 | 0.26628732681274414 |
p49 | 12873 | 0.26030492782592773 |
p50 | 9574 | 0.3400905132293701 |
p51 | 10900 | 0.2653172016143799 |
p52 | 10230 | 0.3670506477355957 |
p53 | 12795 | 0.3002490997314453 |
p54 | 9625 | 0.40192604064941406 |
p55 | 11893 | 0.2982041835784912 |
p56 | 23973 | 0.2593355178833008 |
p57 | 33049 | 0.26126980781555176 |
p58 | 54112 | 0.26030397415161133 |
p59 | 39409 | 0.26132631301879883 |
p60 | 24036 | 0.25232505798339844 |
p61 | 32994 | 0.26030397415161133 |
p62 | 53981 | 0.25834059715270996 |
p63 | 39205 | 0.2563159465789795 |
p64 | 24021 | 0.26030445098876953 |
p65 | 32977 | 0.2643263339996338 |
p66 | 53993 | 0.2563149929046631 |
p68 | 24030 | 0.2553138732910156 |
p69 | 33087 | 0.2583155632019043 |
p70 | 53954 | 0.2573122978210449 |
p71 | 39174 | 0.27825474739074707 |
每个例子的具体结果:SA算法测例具体结果
可以看到,模拟退火的例子相比于贪心算法的初始解,大部分的确有了小的进步;当然,还有些并不如贪心算法的初始解。可能这个题目的确比较适合用贪心算法把!