Capacitated Facility Location Problem
问题描述
- Suppose there are n facilities and m customers. We wish to choose:
- which of the n facilities to open
- the assignment of customers to facilities
- The objective is to minimize the sum of the opening cost and the assignment cost.
- The total demand assigned to a facility must not exceed its capacity.
算法设计
贪心算法
对于每个顾客,寻找满足其需求的代价最小的设施。若设施未开启,则代价为开启该设施费用和为其分配需求的费用之和。由于为前面的顾客分配后会影响后面的顾客的分配,因此贪心算法与顾客分配顺序有关。可以多次使用贪心算法求解,每次先打乱顾客分配次序,取最优解。
伪代码如下:
for i in 0 to times
shuffle customers order
for c in customers
choose the facility f with the least cost
update solution
update optimal_solution
局部搜索
首先利用贪心算法求得一个初始解,然后在这个解的基础上求邻域解。若该邻域解的总代价小于原解,则讲解替换为该邻域解,然后在新解的基础上继续求邻域解;若该邻域解的总代价大于原解,则将该解舍去。重复搜索邻域解较大次数,最终可得到一个局部最优解。
寻找邻域解有以下几种方法:
- 随机将1名顾客分配给另一个设施。
- 随机交换2名顾客分配的设施。
- 随机选2名顾客,将这两名顾客间的所有顾客分配的设施反转,例如顾客编号为[0, 1, 2, 3, 4, 5]:
- 若选择顾客1和4:将顾客1,4分配的设施交换;将顾客2,3分配的设施交换。
- 若选择顾客4和1:将顾客4,1分配的设施交换;将顾客5,0分配的设施交换。
- 在交换过程中,若出现设施剩余容量不能满足交换时,跳过交换这两名顾客,继续交换其余顾客。
伪代码如下:
for i in 0 to times
get a neighborhood solution s
if s.cost < current_solution.cost
current_solution = s
模拟退火法
由于局部搜索法的解只向较好解的方向移动,因此很容易陷入局部最优中。而模拟退火法在局部搜索法的基础上增加了接受差解的情况:若邻域解比当前解差,以一定的概率接受该差解。温度越高,接受差解的概率越高,随着温度的降低,接受差解的概率趋于0。在模拟退火法的初始阶段,温度较高,解能够自由的在解空间移动;随着温度的降低,解向劣解移动的概率逐渐降低,因此最终求得的解是一个局部最优解。模拟退火法与局部搜索法相比,两者最终都是求得一个局部最优解;但模拟退火法初期能够允许接受差解,因此能够跳过某些局部最优解,最终得到一个较好的局部最优解。
伪代码如下:
T = T0
current_solution = s0
while T > Tmin
for i in 0 to times
get a neighborhood solution s
if newCost < cost or random() < exp(-(newCost - cost) / T)
current_solution = s
T *= rate /*rate < 1*/
程序代码(C++)
具体代码可见github: https://github.com/Huang-Junjie/CFLP
数据结构:
struct solution {
int cost;
vector<bool> isOpen;
vector<int> assign;
vector<int> capacityLeft;
};
struct problem {
int facilityNum;
int customerNum;
vector<int> capacity;
vector<int> openingCost;
vector<int> demand;
//assignmentCost[i][j] is the cost of allocating all the demand of customer i to facility j.
vector<vector<int>> assignmentCost;
};
贪心法
void greedy() {
vector<int> result;
vector<float> runtime;
for (int i = 1; i < 72; i++) {
cout << "---------------------------" << endl;
cout << "P" << i << ":" << endl;
srand((unsigned int)(time(NULL)));
long startTime = clock();
problem pro;
solution sol;
readInstance("Instances/p" + to_string(i), pro);
getInitalSolution(pro, sol);
//初始化一个未开始分配的解
solution emptySol;
emptySol.isOpen.resize(pro.facilityNum);
emptySol.assign.resize(pro.customerNum);
emptySol.capacityLeft.resize(pro.facilityNum);
emptySol.cost = 0;
for (bool i : emptySol.isOpen) i = false;
for (int i = 0; i < pro.facilityNum; i++) emptySol.capacityLeft[i] = pro.capacity[i];
//初始化分配顺序
vector<int> order(pro.customerNum);
for (int i = 0; i < order.size(); i++) order[i] = i;
int index;
int temp;
pair<int, int> leastAssign;
solution newSol;
//打乱顺序使用贪心法,取最优值
for (int j = 0; j < 1000; j++) {
//打乱分配顺序
for (int i = 0; i < order.size() - 1; i++) {
index = rand() % (order.size() - i) + i;
temp = order[i];
order[i] = order[index];
order[index] = temp;
}
//使用贪心法进行分配
newSol = emptySol;
for (int i = 0; i < pro.customerNum; i++) {
leastAssign = findLeastCostFacility(pro, newSol, order[i]);
newSol.isOpen[leastAssign.first] = true;
newSol.assign[order[i]] = leastAssign.first;
newSol.capacityLeft[leastAssign.first] -= pro.demand[order[i]];
newSol.cost += leastAssign.second;
}
if (newSol.cost < sol.cost) sol = newSol;
cout << "p" << i << ": times: " << j << endl;
printSolution(sol);
}
result.push_back(sol.cost);
runtime.push_back((clock() - startTime) / CLK_TCK);
writeSolution("Results/Greedy/p" + to_string(i) + ".txt", sol);
}
wirteResultTable("Results/GreedyTable.csv", result, runtime);
}
Result table: https://github.com/Huang-Junjie/CFLP/blob/master/Results/GreedyTable.csv
Detailed solution: https://github.com/Huang-Junjie/CFLP/tree/master/Results/Greedy
局部搜索法
void localSearch() {
vector<int> result;
vector<float> runtime;
for (int i = 1; i < 72; i++) {
cout << "---------------------------" << endl;
cout << "P" << i << ":" << endl;
srand((unsigned int)(time(NULL)));
long startTime = clock();
problem pro;
solution sol;
readInstance("Instances/p" + to_string(i), pro);
getInitalSolution(pro, sol);
solution newSol(sol);
for (int j = 0; j < 100000; j++) {
//寻找邻域解
getNewSolution(pro, newSol);
//若新解cost小,则直接接受
if (newSol.cost < sol.cost) {
sol = newSol;
}
else newSol = sol;
if (j % 1000 == 0) {
cout << "p" << i << ": times: " << j << endl;
printSolution(sol);
}
}
result.push_back(sol.cost);
runtime.push_back((clock() - startTime) / CLK_TCK);
writeSolution("Results/LS/p" + to_string(i) + ".txt", sol);
}
wirteResultTable("Results/LSTable.csv", result, runtime);
}
Result table: https://github.com/Huang-Junjie/CFLP/blob/master/Results/LSTable.csv
Detailed solution: https://github.com/Huang-Junjie/CFLP/tree/master/Results/LS
模拟退火法
int main()
{
vector<int> result;
vector<float> runtime;
double Tmax = 100;
double Tmin = 0.1;
int times = 1500;
double rate = 0.99;
for (int i = 1; i < 72; i++) {
cout << "---------------------------" << endl;
cout << "P" << i << ":" << endl;
srand((unsigned int)(time(NULL)));
long startTime = clock();
problem pro;
solution sol;
readInstance("Instances/p" + to_string(i), pro);
getInitalSolution(pro, sol);
solution newSol(sol);
double T = Tmax;
while (T > Tmin) {
for (int i = 0; i < times; i++) {
//寻找邻域解
getNewSolution(pro, newSol);
//若新解cost小,则直接接受
//若新解cost大,则以exp(-(newcost - cost)/ T)概率接受
if(newSol.cost < sol.cost ||
rand() / (RAND_MAX + 1.0) < exp((sol.cost - newSol.cost) / T)) {
sol = newSol;
}
else newSol = sol;
/*cerr << i << endl;*/
}
T *= rate;
cout << "T: " << T << endl;
printSolution(sol);
}
result.push_back(sol.cost);
runtime.push_back((clock() - startTime) / CLK_TCK);
writeSolution("Results/SA/p" + to_string(i) + ".txt", sol);
}
wirteResultTable("Results/SATable.csv", result, runtime);
}
Result table: https://github.com/Huang-Junjie/CFLP/blob/master/Results/SATable.csv
Detailed solution: https://github.com/Huang-Junjie/CFLP/tree/master/Results/SA