中山大学数据科学与计算机学院本科生实验报告
(2018年秋季学期)
课程名称 | 算法设计与分析 | 任课老师 | 张子臻 |
---|---|---|---|
年级 | 2016级 | 专业(方向) | 计算机应用 |
学号 | 16340030 | 姓名 | 陈斯敏 |
电话 | 15917173057 | 2540740154@qq.com | |
开始日期 | 2018.12.18 | 完成日期 | 2018.12.22 |
实验问题描述
Capacitated Facility Location Problem
Suppose there are n facilities and m customers. We wish
to choose:
(1) which of the n facilities to open ?
(2) the assignment of customers to facilities ?
The objective is to minimize the sum of the opening cost and the assignment cost.
note: The total demand assigned to a facility must not exceed its capacity.
题目要求给定相应的工厂和顾客,每个工厂有相应的容量(可容纳顾客需求)和相应的开启费用,而每个顾客有相应的需求(固定的)和任务费用(因工厂而改变),现在要对每个工厂进行分配顾客,要求所有顾客都要分配到工厂,然后工厂的容量不能超过它所分配到的顾客的需求之和,同时希望工厂开启费用和顾客任务费用总和最小,每一份数据的输入格式如下:
解决方案一、贪心算法
因为题目要求希望求出最优的算法,所以我们很容易可以想到使用贪心算法进行求解,但是贪心策略的选取很关键。对于该问题,贪心算法并不一定能求出最优解法,但是可以在较短的时间求得一个可满足解且该解的优劣程度不会太差。
接下来,讲一下实现该算法的流程和贪心策略:
一、贪心算法流程:
- 读取数据(采用C++的流读取方式)。
- 运用贪心策略选择分配一个未被分配顾客到相对应的工厂(该工厂需要可满足要求)。
- 重复第2步的操作,直至分配完所有的顾客。
- 评估当前费用,输出结果。
二、贪心策略
采用的贪心选取策略是选取性价比最高的操作,因为需要满足的需求是固定的,而产生的费用是不固定的,所以我们希望性价比最高当然是费用/需求最少,那么,我们每次分配顾客时,便会产生相应的费用,也会满足相应的需求,便可以遍历所有可进行的操作,选取其中费用需求比最小(性价比最高的)的操作,具体公式如下:
GreadyVal = assignment / demand
此处,我们除了考虑对应的客户任务费用,工厂的开启费用也需要考虑进去,这里需要进行判断,如果该工厂并未开启,则需要加上OpenCost,如果已经开启(分配过用户),那么我们便不需要加上这部分的费用,所以我们进行改进,总的运算公式如下:
GreadyVal = assignment / demand
if not isOpen:
GreadyVal += OpenCost / demand
三、贪心算法的优缺点
优点:
- 运行时间快,所有样例只需要进行两层循环遍历所有的操作,直至选取完所有的顾客节点即可,所有样例的运行时间都极短。
缺点:
- 不一定可解,因为这样的贪心策略,如果测例不够温柔的话,很容易分配到最后会剩余大量工厂碎片导致无法插入客户,不过此次实验的测试样例并不会出现该情况。
- 解的误差性有时候不一定可接受,这个需要跟后边的算法结果进行对比,很明显贪心算法不是最优算法,而有时其解与最优解的差距较大。
四、算法实现的代码
代码地址:https://github.com/chensm9/Algorithm-project/blob/master/Greedy.cpp
这里贴出主要的贪心算法实现函数:
// 贪心算法流程
int Greedyrun() {
// 需要遍历“顾客数量”次,以此达到分配完所有的顾客
for (int i = 0; i < Cnum; i++) {
int greedyVal = INT_MAX;
int c_id = -1, f_id = -1;
// 遍历所有的用户和工厂
for (int j = 0; j < Cnum; j++) {
// 该用户已经被分配
if (CList[j].belongTo != -1)
continue;
// 遍历所有的可容纳该用户的工厂
for (int k = 0; k < Fnum; k++) {
if (CList[j].demand <= FList[k].leftCapacity) {
int tempVal = assignmentMap[k][j] / CList[j].demand;
if (FList[k].leftCapacity == FList[k].Capacity)
tempVal += FList[k].OpenCost / CList[j].demand;
// 判断性价比是否优于当前最优操作
if (greedyVal > tempVal) {
greedyVal = tempVal;
c_id = j;
f_id = k;
}
}
}
}
// 这一轮最后被选取的操作
FList[f_id].leftCapacity -= CList[c_id].demand;
CList[c_id].belongTo = f_id;
}
int bestSolution = evaluate(FList, CList);
return bestSolution;
}
解决方案二、局部搜索法(爬山法)
局部搜索法的过程是对一个初始化的解反复进行相关领域操作,直到得到比该解更优的解,然后一直迭代下去,直到逼近最优的解,同样的,该算法也不一定能够得到最优的解,且其解与我们所选取的初始解和进行的邻域操作相关,但因为进行多次迭代能够得到比初始解更优的解,那么我们很容易想到可以将上面贪心算法得到的解作为初始解来进行迭代,那么最后得到的结果一定比贪心算法更优。
一、局部搜索算法流程:
- 输入数据(同样采用相同的流读取)
- 初始化一个最优解(此处可以采用贪心算法的解作为初始解)
- 对最优解进行邻域操作,产生一个新的解,如果该解优于当前最优解,则代替当前最优解。
- 重复第3步操作,直到N次迭代都未达到更优解,退出重复操作。
- 输出当前最优解。
二、相关邻域操作
这里为了效果较好,我采用了两种邻域操作:
- 随意寻找一个客户,改变其要分配去的工厂(需要满足容量和需求的关系),以此改变当前的费用总和。
- 随意交换两个客户的所属工厂(同样需要满足容量和需求的关系)。
还有N的取值,即未找到更优解的次数N,需要取多少次,此处我取的是1000。
三、局部搜索法的优缺点
优点:
- 相比贪心算法,能更趋近于最优解
- 收敛速度足够快,运算的时间也不会很长
缺点:
- 容易陷入局部最优,部分测例产生的解并不比贪心算法得到的解好多少
- 不够稳定,因为上面说到的陷入局部最优的原因,所以不同次运行可能陷入不同的局部最优,导致产生的结果相差较大
四、算法实现的代码
代码地址:https://github.com/chensm9/Algorithm-project/blob/master/HC.cpp
这里主要贴出局部搜索函数部分的代码:
// 局部搜索法
int HCrun() {
// initBestSolution();
// 利用贪心算法产生一个解
Greedyrun();
srand((int)time(0));
int flag = 0;
int bestSolution = evaluate(FList, CList);