算法设计与分析(期末项目)

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 tofacilities

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,还是建议去查找专业一点的博客。

标准模拟退火算法的一般步骤:

  • 给定初温t=t0,随机产生初始状态s=s0,令k=0
  • Repeat:
    • Repeat:
      • 产生新状态sj=Generate(s)
      • if min{1,exp[-(C(sj)- C(s))/tk] ≥random[0.1] s=sj
      • Until 抽样稳定准则满足
    • 退温tk+1=update(tk),并令k=k+1
  • Until算法终止准则满足
  • 输出算法搜索结果

我用SA解决这次问题的步骤如下所示:

  1. 给每个顾客随机分配一个工厂,生成一个总花费很高的初解,并设定初温为temperature = 100.0
  2. 在每个温度下进行200次邻域搜索,搜索的方法为随机将一名顾客分配到一个随机选取的新工厂,如果是好解则接受,差解则概率接受。
  3. 200次邻域搜索结束后进行降温,降温函数为temperature = 0.98 * temperature
  4. 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能够谅解。

你可能感兴趣的:(学校作业)