选址问题是运筹学中经典的问题之一。选址问题在生产生活、物流、甚至军事中都有着非常广泛的应用,如工厂、仓库、急救中心、消防站、垃圾处理中心、物流中心、导弹仓库的选址等。
在前面三个基本选址问题的基础上考虑其它因素就形成了扩展选址问题。我们以带固定费用和容量限制的选址问题为例并用爬山算法和模拟退火算法进行求解。
给定若干个带容量限制的服务站,并给每个服务站定义一个固定费用,只要该有任何需求点分配给该服务站则需要消耗该固定费用(相当于开启该服务站的费用)。另外每个需求点分配给服务站时也有一定的费用。总费用=固定费用+分配费用。
现在要求给出一个分配方案,将若干个服务站的其中一部分或全部分配给相应所有的需求点,确保所有需求点都被分配到,并且让总费用最小。
算法用python实现,完整代码在github上。
首先需要选择合适的方式来表示当前解,方便后续算法的实现。这里我们定义了服务站和需求点两个类。
class Customer:
'需求点'
def __init__(self, cid, demand):
self.id = cid
self.demand = demand
self.fid = -1
self.assignment_cost = -1
def __str__(self):
return 'Customer(id:%d, demand:%d, fid:%d, cost:%d)' % (self.id, self.demand, self.fid, self.assignment_cost)
class Ficility:
'服务站'
def __init__(self, fid, capacity, opening_cost, assignment_costs):
self.id = fid
self.capacity = capacity
self.opening_cost = opening_cost
self.assignment_costs = assignment_costs
self.assignment = []
self.cur_capacity = capacity
self.total_cost = 0
# 计算将某一需求点分配到该服务站所新增的代价
def get_cost(self, customer):
if self.cur_capacity < customer.demand:
return float("inf")
if len(self.assignment) == 0:
return self.opening_cost + self.assignment_costs[customer.id]
return self.assignment_costs[customer.id]
# 将某一需求点分配到该服务站并更新服务站和需求点的状态,返回所需的cost
def assign(self, customer):
cost = self.get_cost(customer)
if cost == float('inf'):
return 0
self.total_cost += cost
self.cur_capacity -= customer.demand
self.assignment.append(customer)
customer.fid = self.id
customer.assignment_cost = self.assignment_costs[customer.id]
return cost
# 将某一需求点从该服务站的分配列表中删除并更新服务站和需求点的状态,返回减少的cost
def deassign(self, customer):
if customer.fid != self.id:
return 0
cost = 0
for i in range(len(self.assignment)):
if self.assignment[i].id == customer.id:
cost += self.assignment[i].assignment_cost
self.cur_capacity += self.assignment[i].demand
del self.assignment[i]
if len(self.assignment) == 0:
cost += self.opening_cost
customer.fid = -1
customer.assignment_cost = -1
break
self.total_cost -= cost
return cost
def __str__(self):
tostr = 'Ficility(id-%d, cap-%d, o_cost-%d, c-cap-%d, cost-%d)' % (self.id, self.capacity, self.opening_cost, self.cur_capacity, self.total_cost)
for customer in self.assignment:
tostr = tostr + '\n' + str(customer)
return tostr
当前解可由Ficility对象构成的数组以及Customer对象构成的数组和total_cost来表示。
由于我们难以获得最优解,为了衡量算法的输出结果好坏,我们需要一个比较好的结果来作为参考。以下简单的用一个函数来估算最优解的下限。我们将总费用分成两部分,也就是:总费用=固定费用(服务站开启的费用)+分配费用(将用户分配到服务站的费用)。
这里的基本思想是做一些比较理想的假设,分别求出固定费用和分配费用在理想情况下的最小值,从而估算最小总费用的下限。对于理想分配费用的计算,每个用户都有P个服务站可选,我们不考虑服务站是否有足够容量满足用户需求,总是将每个用户分配给费用最低的服务站,计算此时所有用户的分配费用总和即可估算出分配费用的理想值。
对于理想固定费用的计算,我们是基于总的需求量及满足单位需求量所需的最小开启费用来算,即:理想固定费用 = 总需求量 * 满足单位需求量所需的理想开启费用。总需求量即所有用户需求量的总和,而“满足单位需求量所需的理想开启费用”(记为ideal_rate)可以简单理解为分配方案的整体性价比,即:理想情况下总的开启费用/所满足的总需求量。对于ideal_rate这一项的估算,下面的代码中包含了两种方式,一种是找出所有服务站中,开启费用/容量的值最小的服务器,并将这一比值作为ideal_rate的值,相当于将服务站中最高的性价比作为方案整体的性价比;另一种方式是用平均值来计算ideal_rate,也就是:服务站总开启费用/服务站总容量。第二种方式算出来的理想固定费用有可能比实际的最优理想开启费用要高,但相比第一种方式来说往往更接近实际的理想值,用第一种方式算容易受到测试数据中个别“超高性价比”服务站(开启费用/容量很高的服务站)的影响,而使得估算结果与实际有较大偏差。总的来说这两种计算方式都还有较大的改进空间,可根据实际需要进一步改进。
# 估算当前问题所能达到的最小代价
def eval_min_cost():
global ficility_count
global customer_count
global customers
global demands
global assignment_costs_matrix
global opening_costs
global capacities
total_cost = 0
total_demand = sum(demands)
total_capacity = sum(capacities)
total_opening_cost = sum(opening_costs)
# 估算分配费用的理想值
for i in range(customer_count):
total_cost += min(assignment_costs_matrix[:, i])
# min(rate)即单位需求量所需的最小固定费用
rate = [opening_costs[i] / capacities[i] for i in range(ficility_count)]
# 单位需求量所需的平均固定费用
mean_rate = total_opening_cost / total_capacity
# 估算固定费用的理想值
## 方式1:固定费用理想值 = 总需求量 * 满足单位需求量所需的最小开启费用
#total_cost += int(min(rate) * total_demand)
## 方式2: 固定费用理想值 = 总需求量 * 满足单位需求量所需的平均开启费用
total_cost += int(mean_rate * total_demand)
return total_cost
邻域操作可以用来从当前解产生新解,这一过程往往是随机的,邻域操作在当前解进行一些扰动,若扰动比较小,则新解的代价变化幅度一般比较小,比较容易收敛到局部最优。若扰动比较大,则新解的代价变化幅度可能波动比较大,对于跳出局部最优有时有所帮助。在设计算法时,扰动比较小和扰动比较大的邻域操作可以相结合。这次选择的是以下两个邻域操作。
# 随机交换两个需求点,并返回cost的变化(大于0表示cost增加小于0表示减少),结果保存在传入的参数中
# 扰动程度较小
def get_neighbor_method_1(cur_ficilities, cur_customers, cur_total_cost, max_retries=100):
global customer_count
cid1, cid2 = gen_random_index_pair(0, customer_count - 1)
# 产生新的随机数直至需求点cid1和cid2能够互换位置而不会超出服务站容量
retries = 0
while (cur_ficilities[cur_customers[cid1].fid].cur_capacity < cur_customers[cid2].demand - cur_customers[cid1].demand or cur_ficilities[cur_customers[cid2].fid].cur_capacity < cur_customers[cid1].demand - cur_customers[cid2].demand):
cid1, cid2 = gen_random_index_pair(0, customer_count - 1)
retries += 1
# 防止死循环
if retries > max_retries:
break
if retries > max_retries:
return 0
fid1 = cur_customers[cid1].fid
fid2 = cur_customers[cid2].fid
cost1 = cur_ficilities[fid1].deassign(cur_customers[cid1])
cost2 = cur_ficilities[fid2].deassign(cur_customers[cid2])
cost3 = cur_ficilities[fid2].assign(cur_customers[cid1])
cost4 = cur_ficilities[fid1].assign(cur_customers[cid2])
return (cost3 + cost4) - (cost1 + cost2)
# 随机选取两个需求点,将其中一个需求点分配给另一个需求点所在服务站
# 扰动程度比较小
def get_neighbor_method_2(cur_ficilities, cur_customers, cur_total_cost, max_retries=100):
global customer_count
cid1, cid2 = gen_random_index_pair(0, customer_count - 1)
# 将需求点cid2分配到需求点cid1所在的服务站
retries = 0
while cur_ficilities[cur_customers[cid1].fid].cur_capacity < cur_customers[cid2].demand:
cid1, cid2 = gen_random_index_pair(0, customer_count - 1)
retries += 1
# 防止死循环
if retries > max_retries:
break
if retries > max_retries:
return 0
fid1 = cur_customers[cid1].fid
fid2 = cur_customers[cid2].fid
cost1 = cur_ficilities[fid2].deassign(cur_customers[cid2])
cost2 = cur_ficilities[fid1].assign(cur_customers[cid2])
return cost2 - cost1
# 爬山法求解
def solve_problem_by_climb(problem_index, max_search_times = 100, round_times = 100):
global ficilities
global customers
st = time.time()
# 获取初始较优解
cur_ficilities, cur_customers, cur_cost = greedy(ficilities, customers)
# 记录算法过程的辅助变量
epoch = 0
min_cost = eval_min_cost()
while epoch < max_search_times:
best_local_ficilities = None
best_local_customers = None
best_local_cost = float('inf')
# 找出当前解的邻域中round_times个随机解的最优解
for i in range(round_times):
new_ficilities, new_customers, cost_diff = get_neighbor(cur_ficilities, cur_customers, cur_cost)
if cur_cost + cost_diff < best_local_cost:
best_local_ficilities = new_ficilities
best_local_customers = new_customers
best_local_cost = cur_cost + cost_diff
# 发现更好的解则更新当前解
if best_local_cost < cur_cost:
cur_cost = best_local_cost
cur_ficilities = best_local_ficilities
cur_customers = best_local_customers
epoch += 1
print('当前邻域搜索次数,当前代价,误差:NO.%5d %d %.2f%%' % (epoch, cur_cost, 100 * (cur_cost - min_cost) / min_cost))
et = time.time()
t = et -st
return cur_ficilities, cur_customers, cur_cost, t
# 模拟退火算法
def SA(ficilities, customers, init_cost, initial_temp, min_temp, rate, round_times, min_cost):
cur_ficilities = copy.deepcopy(ficilities)
cur_customers = copy.deepcopy(customers)
cur_total_cost = init_cost
cur_temp = initial_temp
best_ficilities = None
best_customers = None
best_cost = float('inf')
# 辅助变量,用于跟踪算法的效果以及绘图
epoch = 0
# 记录估算精度随温度的变化
temps = []
precisions = []
# 记录整个过程代价的变化
costs_record = []
while cur_temp > min_temp:
# 内循环
for i in range(round_times):
new_ficilities, new_customers, cost_diff = get_neighbor(cur_ficilities, cur_customers, cur_total_cost)
costs_record.append(cur_total_cost + cost_diff)
# 记录当前找到的最优解
if cur_total_cost + cost_diff < best_cost:
best_ficilities = new_ficilities
best_customers = new_customers
best_cost = cur_total_cost + cost_diff
# 若符合条件则接受新解
if cost_diff < 0 or random.random() < math.exp(-1 * (abs(cost_diff) / cur_temp)):
cur_ficilities = new_ficilities
cur_customers = new_customers
cur_total_cost += cost_diff
# 降温
cur_temp = cur_temp * rate
epoch += 1
print('当前降温次数,温度,总代价,误差率:NO.%4d %.2f %d %.2f%%' % (epoch, cur_temp, cur_total_cost, 100 * (cur_total_cost - min_cost) / min_cost))
temps.append(cur_temp)
precisions.append(100 * (best_cost - min_cost) / min_cost)
states = [temps, precisions, costs_record]
return best_ficilities, best_customers, best_cost, states
# 模拟退火方法求解
def solve_problem_by_SA(problem_index, initial_temp = 100, min_temp = 1, rate = 0.95, round_times = 100):
global ficilities
global customers
st = time.time()
new_ficilities, new_customers, new_cost = greedy(ficilities, customers)
min_cost = eval_min_cost()
new_ficilities, new_customers, new_cost, states = SA(new_ficilities, new_customers, new_cost, initial_temp, min_temp, rate, round_times, min_cost)
et = time.time()
t = et - st
#gen_picture_sa(states)
return new_ficilities, new_customers, new_cost, t
运行代码后,输出了两份csv文件,一份是用爬山算法求解得到的结果,另一份是用模拟退火算法求解得到的结果。每一个问题实例的结果有三行:
第一行为最终的总费用和运行时间,第二行为服务站的最终状态,0表示不开启该服务站,1表示开启。第三行表示各个服务站所服务的需求点的列表。