Suppose there are n facilities and m customers.We wish to choose:
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.
简而言之就是存在n个工厂,每个工厂有各自的容量上限和开启花费,现在有m个顾客,当一个工厂的剩余容量大于等于某个顾客的需求时,这个工厂才能为这个顾客服务,不同的工厂服务同一个顾客的花费也不同。每个顾客都需要被服务到,怎样安排能使工厂的开启花费和顾客服务花费总和最小。
对于某一个合法的样例来说肯定是存在最优解的,但并不是那么容易求的,老师也不作这方面的要求,只要求用2种不同的方法求好解,因此我选择了最常见的贪心算法和这学期新学的模拟退火(SA)算法,下面我将一一介绍2种方法的实现。
我用的是较为简单的贪心思路,因为有n个工厂,每个顾客自然有n个对应cost,按照某种标准给n个工厂排序,形成第一志愿工厂、第二志愿工厂…起初我是按照顾客在每个工厂的服务花费customer.cost[i]
与工厂的开启花费facility[i].cost
之和为标准排序,后来经过实验发现,用两个花费的和不如单按照服务花费customer.cost[i]
为标准排序求得的结果好。我的猜想是工厂的开启花费往往比服务花费大得多,而且工厂只要开启了就不再需要额外的花费,如果给顾客的服务花费排序时算上工厂开启花费,会对排序结果有较大的影响,差解排在好解前面的概率大大增加。
经过排序后,每个顾客都有了各自的第一志愿工厂,再按照第一志愿工厂的服务花费对顾客进行排序,第一志愿工厂服务花费少的顾客优先分配。分配的原则是如果第一志愿工厂剩余容量足够,就把顾客分配到第一志愿工厂,否则把志愿往后按序推移。
这种贪心的思路总的来说很简单,就是通过两次排序决定顾客和各自志愿工厂优先处理顺序,然后根据排序结果分配。但问题也是显而易见的,就是没有考虑到顾客组合的影响,比如A、B两个顾客占用了工厂1,但其实A、C两个顾客占用工厂1能使花费更少,但却因为C的优先级在B后面,导致A、B分配到工厂1后没有剩余容量能容下C。因此这个贪心算法只能求得局部最优,往往求不到最优解,与最优解的差距和最优解的顾客组合复杂性有关。
测试样例 | 结果 | 时间 |
---|---|---|
p1 | 9472 | 0.000 |
p2 | 8158 | 0.000 |
p3 | 10158 | 0.000 |
p4 | 12158 | 0.000 |
p5 | 9661 | 0.000 |
p6 | 8347 | 0.000 |
p7 | 10347 | 0.000 |
p8 | 12347 | 0.000 |
p9 | 9040 | 0.000 |
p10 | 7726 | 0.001 |
p11 | 9726 | 0.000 |
p12 | 11726 | 0.000 |
p13 | 12032 | 0.000 |
p14 | 12032 | 0.000 |
p15 | 13180 | 0.000 |
p16 | 17180 | 0.000 |
p17 | 12032 | 0.000 |
p18 | 9180 | 0.000 |
p19 | 13180 | 0.000 |
p20 | 17180 | 0.000 |
p21 | 12032 | 0.000 |
p22 | 9180 | 0.001 |
p23 | 13180 | 0.000 |
p24 | 17180 | 0.000 |
p25 | 19189 | 0.001 |
p26 | 16123 | 0.001 |
p27 | 21523 | 0.001 |
p28 | 26923 | 0.001 |
p29 | 19145 | 0.001 |
p30 | 16079 | 0.002 |
p31 | 21479 | 0.001 |
p32 | 26879 | 0.001 |
p33 | 19055 | 0.001 |
p34 | 15989 | 0.001 |
p35 | 21389 | 0.001 |
p36 | 26789 | 0.001 |
p37 | 19055 | 0.001 |
p38 | 15989 | 0.001 |
p39 | 21389 | 0.001 |
p40 | 26789 | 0.001 |
p41 | 7218 | 0.000 |
p42 | 9957 | 0.000 |
p43 | 12448 | 0.001 |
p44 | 7567 | 0.001 |
p45 | 9848 | 0.000 |
p46 | 12639 | 0.001 |
p47 | 6777 | 0.001 |
p48 | 9044 | 0.001 |
p49 | 12420 | 0.000 |
p50 | 10184 | 0.001 |
p51 | 11388 | 0.000 |
p52 | 11966 | 0.000 |
p53 | 13167 | 0.000 |
p54 | 10993 | 0.000 |
p55 | 12013 | 0.000 |
p56 | 23882 | 0.002 |
p57 | 32882 | 0.001 |
p58 | 53882 | 0.002 |
p59 | 39121 | 0.001 |
p60 | 23882 | 0.001 |
p61 | 32882 | 0.001 |
p62 | 53882 | 0.001 |
p63 | 39121 | 0.001 |
p64 | 23882 | 0.001 |
p65 | 32882 | 0.001 |
p66 | 53882 | 0.001 |
p67 | 39671 | 0.002 |
p68 | 23882 | 0.001 |
p69 | 32882 | 0.002 |
p70 | 53882 | 0.001 |
p71 | 39121 | 0.001 |
#include
#include
#include
using namespace std;
typedef struct {
int capacity; //工厂容量
int cost; //工厂开启的花费
int occupy; //当前已经使用的容量
}facility;
typedef struct {
int demand; //容量需求
int *cost; //对每个工厂的花费
int *costID; //记录每个花费对应的是哪个工厂
}customer;
void greedy(string file){
//读取文件
ifstream infile("./Instances/p" + file);
ofstream outfile("./Greedy_output", ios::app);
int facilityNum, customerNum;
float temp;
infile >> facilityNum >> customerNum;
facility *facilities = new facility[facilityNum];
customer *customers = new customer[customerNum];
for (int i = 0; i < facilityNum; i++){
infile >> facilities[i].capacity >> facilities[i].cost;
facilities[i].occupy = 0;
}
for (int i = 0; i < customerNum; i++){
infile >> temp;
customers[i].demand = (int) temp;
customers[i].cost = new int[facilityNum];
customers[i].costID = new int[facilityNum];
}
for (int i = 0; i < facilityNum; i++)
for (int j = 0; j < customerNum; j++){
infile >> temp;
customers[j].cost[i] = (int) temp;
customers[j].costID[i] = i;
}
infile.close();
//开始计时
clock_t start = clock();
//将每个顾客的工厂cost按升序排序
for (int i = 0; i < customerNum; i++){
int tempp;
for (int j = 0; j < facilityNum - 1; j++)
for (int k = j + 1; k < facilityNum; k++)
if (customers[i].cost[j] > customers[i].cost[k]){
tempp = customers[i].cost[j];
customers[i].cost[j] = customers[i].cost[k];
customers[i].cost[k] = tempp;
tempp = customers[i].costID[j];
customers[i].costID[j] = customers[i].costID[k];
customers[i].costID[k] = tempp;
}
}
//将顾客依照第一志愿工厂的cost按升序排序
for (int i = 0; i < customerNum - 1; i++){
customer tempp;
for (int j = 0; j < customerNum; j++)
if (customers[i].cost[0] > customers[j].cost[0]){
tempp = customers[i];
customers[i] = customers[j];
customers[j] = tempp;
}
}
//给每个顾客分配工厂
int facilityCost = 0, customerCost = 0;
for (int i = 0; i < customerNum; i++){
int count = 0;
while (count < facilityNum){
int facilityID = customers[i].costID[count];
if (facilities[facilityID].capacity - facilities[facilityID].occupy > customers[i].demand){
if (facilities[facilityID].occupy == 0)
facilityCost += facilities[facilityID].cost;
customerCost += customers[i].cost[count];
facilities[facilityID].occupy += customers[i].demand;
break;
}
count++;
}
}
//结束计时
clock_t stop = clock();
//输出文件
double t = (double)stop - start;
t = t / CLOCKS_PER_SEC;
outfile << "Test file p" + file + ": " << facilityCost + customerCost << ' ' << t << endl;
outfile.close();
//释放内存空间
for (int i = 0; i < customerNum; i++){
delete []customers[i].cost;
delete []customers[i].costID;
}
delete []facilities;
delete []customers;
}
int main(){
//循环测试71个样例
for (int i = 1; i <= 71; i++){
greedy(to_string(i));
}
return 0;
}
模拟退火算法(以下简称SA)是一种搜索策略,它基于现在已有的解,在状态空间中的邻域寻找新解,如果新解比现有解好则接受,如果找到的是差解则概率接受。这也是算法被称为模拟退火的原因,它允许一定概率下接受差解,以此跳出局部最优的困境,随着温度的降低接受差解的条件也变得苛刻,差解不再被轻易接受,最后达到状态收敛,求得一个与最优解接近的解。所求解与最优解的相差程度与初温、邻域查找方法和降温手段等因素都有关联。以上是我对SA的一点个人认知,如果想要系统地学习SA,还是建议去查找专业一点的博客。
标准模拟退火算法的一般步骤:
我用SA解决这次问题的步骤如下所示:
temperature = 100.0
。200
次邻域搜索,搜索的方法为随机将一名顾客分配到一个随机选取的新工厂,如果是好解则接受,差解则概率接受。temperature = 0.98 * temperature
。temperature < 0.1
时结束整个退火过程,输出当前解。测试样例 | 结果 | 时间 |
---|---|---|
p1 | 9125 | 13.281 |
p2 | 8359 | 14.991 |
p3 | 10073 | 21.992 |
p4 | 12436 | 14.990 |
p5 | 9366 | 26.989 |
p6 | 7943 | 29.993 |
p7 | 10824 | 29.990 |
p8 | 12239 | 29.988 |
p9 | 8871 | 14.988 |
p10 | 8149 | 14.989 |
p11 | 9633 | 14.994 |
p12 | 11726 | 14.989 |
p13 | 9827 | 7.986 |
p14 | 7930 | 10.984 |
p15 | 9396 | 7.987 |
p16 | 13552 | 10.985 |
p17 | 9704 | 10.991 |
p18 | 7733 | 7.991 |
p19 | 9921 | 9.981 |
p20 | 12455 | 8.992 |
p21 | 9038 | 6.985 |
p22 | 7615 | 6.987 |
p23 | 9431 | 7.987 |
p24 | 13686 | 6.990 |
p25 | 14789 | 5.967 |
p26 | 12438 | 6.968 |
p27 | 14327 | 7.967 |
p28 | 16712 | 5.970 |
p29 | 15714 | 10.966 |
p30 | 12901 | 10.968 |
p31 | 15020 | 11.970 |
p32 | 18640 | 10.970 |
p33 | 13277 | 6.965 |
p34 | 12475 | 6.969 |
p35 | 14069 | 6.968 |
p36 | 16486 | 6.968 |
p37 | 12668 | 4.970 |
p38 | 12072 | 4.969 |
p39 | 14407 | 4.964 |
p40 | 18042 | 4.971 |
p41 | 7150 | 10.982 |
p42 | 8313 | 3.986 |
p43 | 8083 | 1.983 |
p44 | 7170 | 13.988 |
p45 | 9009 | 3.985 |
p46 | 7652 | 3.984 |
p47 | 6580 | 18.987 |
p48 | 6896 | 4.987 |
p49 | 6809 | 3.982 |
p50 | 9693 | 12.986 |
p51 | 9316 | 3.982 |
p52 | 10236 | 22.993 |
p53 | 10185 | 6.981 |
p54 | 9633 | 14.985 |
p55 | 9295 | 4.984 |
p56 | 24350 | 19.960 |
p57 | 30969 | 18.965 |
p58 | 48014 | 19.957 |
p59 | 36237 | 20.96 |
p60 | 23787 | 11.954 |
p61 | 30651 | 10.962 |
p62 | 46825 | 10.958 |
p63 | 34990 | 11.957 |
p64 | 23808 | 6.958 |
p65 | 30122 | 6.960 |
p66 | 46529 | 6.961 |
p67 | 32306 | 9.958 |
p68 | 23945 | 9.958 |
p69 | 29754 | 10.956 |
p70 | 46275 | 12.960 |
p71 | 33103 | 10.959 |
起初SA求得的结果并不理想,我的解决办法是把内循环的次数从20提高到200,在一个温度下寻找更多可能的状态空间,代价就是代码的运行时间变得超级长。通过查询资料得知,提高SA效果的方法其实非常多,列举其中几个:
我没有采用上面提及到的优化方法一来是因为第一次使用SA求解问题,不太敢尝试复杂的高手操作,一不小心可能就操作失误翻车了,二来也是因为时间不允许了,临近期末还有其他许许多多的事情要做,对SA的深入研究还是留到日后比较空闲的时候吧,实在是对不起。
通过与贪心算法的测试结果对比,我们能发现SA的结果普遍地要比贪心的好,自然地,运行时间也长很多,每个样例的测试时间大致在7-18s。即使是同一个样例,多次测试的运行时间波动也很大,主要是与状态的更新频率有关,尽管运行时间不同,但最终结果都是比较接近最优解的。
#include
#include
#include
#include
#include
#include
using namespace std;
typedef struct {
int capacity; //工厂容量
int cost; //工厂的开启花费
int occupy; //当前已经使用的容量
}facility;
typedef struct {
int demand; //容量需求
int *cost; //对每个工厂的花费
int facilityID; //顾客目前在哪个工厂接受服务
}customer;
bool accept(int difference, float temperature){
return exp((-difference) / temperature) >= (double)(rand() % 1000 / 1000.0);
}
void SA(string file){
//读取文件
ifstream infile("./Instances/p" + file);
ofstream outfile("./SA_output", ios::app);
int facilityNum, customerNum;
float temp;
infile >> facilityNum >> customerNum;
facility *facilities = new facility[facilityNum];
customer *customers = new customer[customerNum];
for (int i = 0; i < facilityNum; i++){
infile >> facilities[i].capacity >> facilities[i].cost;
facilities[i].occupy = 0;
}
for (int i = 0; i < customerNum; i++){
infile >> temp;
customers[i].demand = (int) temp;
customers[i].cost = new int[facilityNum];
customers[i].facilityID = -1;
}
for (int i = 0; i < facilityNum; i++)
for (int j = 0; j < customerNum; j++){
infile >> temp;
customers[j].cost[i] = (int) temp;
}
infile.close();
//开始计时
clock_t start = clock();
//随机生成一个初态
int facilityCost = 0, customerCost = 0;
//为每个顾客随机分配一个工厂
for (int i = 0; i < customerNum; i++){
while (true){
srand((unsigned)time(NULL));
int randnum = rand() % facilityNum;
//工厂有足够空间提供
if (facilities[randnum].capacity - facilities[randnum].occupy >= customers[i].demand){
//如果工厂没开工,计算开工费用
if (facilities[randnum].occupy == 0)
facilityCost += facilities[randnum].cost;
facilities[randnum].occupy += customers[i].demand;
customerCost += customers[i].cost[randnum];
customers[i].facilityID = randnum;
break;
}
}
}
//模拟退火
float temperature = 100.0;
while (temperature > 0.1){
int count = 0;
//每个温度下进行200次状态空间搜索
while (count < 200){
count++;
//随机产生新状态
int randcustomer = rand() % customerNum;
int randfacility = 0;
while (true){
randfacility = rand() % facilityNum;
if (randfacility != customers[randcustomer].facilityID &&
facilities[randfacility].capacity - facilities[randfacility].occupy >= customers[randcustomer].demand)
break;
}
//计算新状态的值
int newFacilityCost = facilityCost;
if (facilities[customers[randcustomer].facilityID].occupy == customers[randcustomer].demand)
newFacilityCost -= facilities[customers[randcustomer].facilityID].cost;
if (facilities[randfacility].occupy == 0)
newFacilityCost += facilities[randfacility].cost;
int newCustomerCost = customerCost
- customers[randcustomer].cost[customers[randcustomer].facilityID]
+ customers[randcustomer].cost[randfacility];
//判断是否接受新状态,如果接受就更新状态
if (accept((newFacilityCost+ newCustomerCost) - (facilityCost + customerCost), temperature)){
facilities[customers[randcustomer].facilityID].occupy -= customers[randcustomer].demand;
facilities[randfacility].occupy += customers[randcustomer].demand;
customers[randcustomer].facilityID = randfacility;
facilityCost = newFacilityCost;
customerCost = newCustomerCost;
}
}
//降温
temperature *= 0.98;
}
//结束计时
clock_t stop = clock();
//输出文件
double t = (double)stop - start;
t = t / CLOCKS_PER_SEC;
outfile << "Test file p" + file + ": " << facilityCost + customerCost << ' ' << t << endl;
outfile.close();
//释放内存空间
for (int i = 0; i < customerNum; i++){
delete []customers[i].cost;
}
delete []facilities;
delete []customers;
}
int main(){
for (int i = 1; i <= 71; i++){
SA(to_string(i));
}
return 0;
}
最后必须要吐槽一下在博客上手动输入71个样例的运行结果是多么的心累,而且还要做2次。年纪大了,老眼昏花,偶尔还会间歇性手残,一不小心就会打错。单单是上面那两个表格就花了我一个多小时的时间,为了节省时间,我就没有按照要求里的把开启了哪些广场,每个顾客最终选择的服务服务工厂这些信息输出,希望TA能够谅解。