一、 问题陈述
问题如上图,下面为中文描述:
有容量限制的设施地址问题:假设有n个设施和m个顾客,我们可以作以下操作:
①开启设施 ②分配顾客到某设施
上述两个操作都有各自的成本,我们希望总成本最低,且分配到某设施的总需求不能超过其容量。
二、建立模型
为了方便问题的解决,我们首先建立模型,更具体地说,我们为设施、顾客创建一个具有相应属性的类。
我们以一个实例来更好地了解如何构建一个类:
由上图可知,设施有容量、开启费用、是否开启、服务某个顾客的费用四个属性;顾客有需求、被哪个设施服务两个属性。为了区分每个设施和顾客,我们用ID区分他们,由此建立Facility,Customer两个类:
public class Facility { int facilityId; int capacity; int cost; boolean open; // 从customerId -> cost的映射 Map<Integer, Integer> assignmentCost; public int getFacilityId() { return facilityId; } public void setFacilityId(int facilityId) { this.facilityId = facilityId; } public int getCapacity() { return capacity; } public void setCapacity(int capacity) { this.capacity = capacity; } public int getCost() { return cost; } public void setCost(int cost) { this.cost = cost; } public Map<Integer, Integer> getAssignmentCost() { return assignmentCost; } public void setAssignmentCost(Map<Integer, Integer> assignmentCost) { this.assignmentCost = assignmentCost; } public boolean isOpen() { return open; } public void setOpen(boolean open) { this.open = open; } public Facility(Facility faci) { super(); this.facilityId = faci.facilityId; this.capacity = faci.capacity; this.cost = faci.cost; this.open = faci.open; this.assignmentCost = faci.assignmentCost; } public Facility() {} }
1 public class Customer { 2 int customerId; 3 int demand; 4 int assignedTo; // 去哪个设施 5 public int getCustomerId() { 6 return customerId; 7 } 8 public void setCustomerId(int customerId) { 9 this.customerId = customerId; 10 } 11 public int getDemand() { 12 return demand; 13 } 14 public void setDemand(int demand) { 15 this.demand = demand; 16 } 17 public int getAssignedTo() { 18 return assignedTo; 19 } 20 public void setAssignedTo(int assignedTo) { 21 this.assignedTo = assignedTo; 22 } 23 public Customer(Customer cust) { 24 super(); 25 this.customerId = cust.customerId; 26 this.demand = cust.demand; 27 this.assignedTo = cust.assignedTo; 28 } 29 public Customer() {} 30 }
三、 读取文件及展示
在解决问题前,我们需要得到数据,以方便测试。一个样例的数据格式和第二部分的第一张图一样,输出结果的格式如下图:
我们为样例也构造一个类,格式如下:
1 public class Instance { 2 int result; 3 int time; 4 String id; 5 List<Boolean> openList; 6 List<Integer> assignmentList; 7 public int getResult() { 8 return result; 9 } 10 public void setResult(int result) { 11 this.result = result; 12 } 13 public int getTime() { 14 return time; 15 } 16 public void setTime(int time) { 17 this.time = time; 18 } 19 public List<Boolean> getOpenList() { 20 return openList; 21 } 22 public void setOpenList(List<Boolean> openList) { 23 this.openList = openList; 24 } 25 public List<Integer> getAssignmentList() { 26 return assignmentList; 27 } 28 public void setAssignmentList(List<Integer> assignmentList) { 29 this.assignmentList = assignmentList; 30 } 31 public String getId() { 32 return id; 33 } 34 public void setId(String id) { 35 this.id = id; 36 } 37 38 }
我们用List保存每个顾客、每个设施、每个实例,以及记录他们的数量:
建立好数据结构后,我们编写读取文件和初始化每个对象的代码:
1 //读取文件内容,默认文件内容格式正确,不做检查 2 public void ReadFileAndInit(String path) { 3 File file = new File(path); 4 //System.out.println(path); 5 BufferedReader bReader = null; 6 try { 7 // 字符串相关 8 String str; 9 List<Integer> intList = null; 10 bReader = new BufferedReader(new FileReader(file)); 11 12 // 读取设施和顾客数量 13 str = bReader.readLine(); 14 intList = getNumberFromLine(str); 15 facilityNum = intList.get(0).intValue(); 16 customerNum = intList.get(1).intValue(); 17 18 // 读取设施容量和开销 19 for (int i = 0; i < facilityNum; i++) { 20 str = bReader.readLine(); 21 intList = getNumberFromLine(str); 22 Facility faci = new Facility(); 23 faci.setCapacity(intList.get(0).intValue()); 24 faci.setCost(intList.get(1).intValue()); 25 faci.setOpen(false); 26 faci.setFacilityId(i); 27 faci.setAssignmentCost(new HashMap<Integer, Integer>()); 28 facilityList.add(faci); 29 } 30 // 读取顾客的需求 31 for (int i = 0; i < customerNum; ) { 32 str = bReader.readLine(); 33 intList = getNumberFromLine(str); 34 for (Integer tmp : intList) { 35 Customer cust = new Customer(); 36 cust.setAssignedTo(-1); 37 cust.setCustomerId(i); 38 cust.setDemand(tmp); 39 customerList.add(cust); 40 i++; 41 } 42 } 43 // 读取每个顾客到设施的开销 44 for (int i = 0; i < facilityNum; i++) { 45 for (int j = 0; j < customerNum; ) { 46 str = bReader.readLine(); 47 intList = getNumberFromLine(str); 48 Facility faci = facilityList.get(i); 49 for (Integer tmp : intList) { 50 faci.getAssignmentCost().put(new Integer(j), tmp); 51 j++; 52 } 53 } 54 } 55 } catch(Exception e) { 56 e.printStackTrace(); 57 } finally { 58 if (bReader != null) { 59 try { 60 bReader.close(); 61 } catch(Exception ex) { 62 ex.printStackTrace(); 63 } 64 } 65 } 66 }
再编写用于展示的代码:
1 public void GenerateTable() { 2 if (instanceList == null) { 3 return; 4 } 5 System.out.println("\t"+"result"+ " " + "Time(s)"); 6 7 for (Instance ins : instanceList) { 8 System.out.print(ins.getId() + " "); 9 System.out.print(ins.getResult()); 10 System.out.print(" "); 11 // 转化为毫秒 12 System.out.print((double)ins.getTime()/1000); 13 System.out.print("\n"); 14 } 15 } 16 public void DisplayInstance() { 17 if (instanceList == null) { 18 return; 19 } 20 for (Instance ins : instanceList) { 21 System.out.println(ins.getResult()); 22 for (Boolean bool : ins.getOpenList()) { 23 System.out.print(bool ? 1 : 0); 24 System.out.print(" "); 25 } 26 System.out.println(""); 27 for (Integer tmp : ins.getAssignmentList()) { 28 System.out.print(tmp.intValue()); 29 System.out.print(" "); 30 } 31 System.out.println(""); 32 } 33 }
编写用于生成实例的代码:
1 public Instance GenerateInstance(String id) { 2 Instance ins = new Instance(); 3 long t1 = System.currentTimeMillis(); 4 //int result = Greedy(); 5 int result = SimulateAnneal(); 6 long t2 = System.currentTimeMillis(); 7 List<Boolean> openList = new ArrayList<Boolean>(); 8 List<Integer> assignmentList = new ArrayList<Integer>(); 9 for (Facility faci : facilityList) { 10 openList.add(faci.isOpen()); 11 } 12 for (Customer cust : customerList) { 13 assignmentList.add(cust.getAssignedTo()); 14 } 15 ins.setId(id); 16 ins.setResult(result); 17 ins.setTime((int)(t2-t1)); 18 ins.setOpenList(openList); 19 ins.setAssignmentList(assignmentList); 20 21 return ins; 22 }
四、问题思路及算法
1. 贪心算法
比较简单的解决办法是贪心算法,虽然不能够得到最优解,但它的思路最直接、最简单,实现起来简单,且时间复杂度不算高,下面说下贪心算法在该问题下的运用。
N个用户,编号为1-N,首先编号1选择服务费用最低的且容量足够的设施,编号2一样,只不过在1选择之后选择,以此类推,这并没有考虑到设施的开启费用,这是因为顾客的数量一般比设施多,所以如果设施开启的费用相对服务顾客的费用比较低的话,设施开启的费用是个次要矛盾,因为服务费用占的比例会大很多,当然,如果这个前提不成立的话,贪心算法的效果会差很多。
根据上面所说,我们编写代码:
1 public int Greedy() { 2 int result = 0; 3 for (Customer cust : customerList) { 4 int demand = cust.getDemand(); 5 int cost = Integer.MAX_VALUE; 6 int faciId = -1; 7 for (Facility faci : facilityList) { 8 Map<Integer, Integer> assignmentMap = faci.getAssignmentCost(); 9 int assignmentCost = assignmentMap.get(cust.getCustomerId()); 10 if (assignmentCost < cost && faci.getCapacity() >= demand) { 11 cost = assignmentCost; 12 faciId = faci.getFacilityId(); 13 } 14 } 15 cust.setAssignedTo(faciId); 16 if (faciId >= 0) { 17 Facility faci = facilityList.get(faciId); 18 result += cost; 19 if (!faci.isOpen()) { 20 faci.setOpen(true); 21 result += faci.getCost(); 22 } 23 faci.setCapacity(faci.getCapacity()-demand); 24 } 25 26 } 27 return result; 28 }
具体效果在最后一同展示。
2. 模拟退火
模拟退火算法来源于固体退火原理,是一种基于概率的算法,将固体加温至充分高,再让其徐徐冷却,加温时,固体内部粒子随温升变为无序状,内能增大,而徐徐冷却时粒子渐趋有序,在每个温度都达到平衡态,最后在常温时达到基态,内能减为最小。
根据热力学规律并结合计算机对离散数据的处理, 我们定义: 如果当前温度为T, 当前状态与新状态之间的能量差为ΔE , 则发生状态转移的概率为:
伪代码如下图(来自一篇博客):
http://www.cnblogs.com/heaad/archive/2010/12/20/1911614.html#!comments
1 /* 2 * J(y):在状态y时的评价函数值 3 * Y(i):表示当前状态 4 * Y(i+1):表示新的状态 5 * r: 用于控制降温的快慢 6 * T: 系统的温度,系统初始应该要处于一个高温的状态 7 * T_min :温度的下限,若温度T达到T_min,则停止搜索 8 */ 9 while( T > T_min ) 10 { 11 dE = J( Y(i+1) ) - J( Y(i) ) ; 12 13 if ( dE >=0 ) //表达移动后得到更优解,则总是接受移动 14 Y(i+1) = Y(i) ; //接受从Y(i)到Y(i+1)的移动 15 else 16 { 17 // 函数exp( dE/T )的取值范围是(0,1) ,dE/T越大,则exp( dE/T )也 18 if ( exp( dE/T ) > random( 0 , 1 ) ) 19 Y(i+1) = Y(i) ; //接受从Y(i)到Y(i+1)的移动 20 } 21 T = r * T ; //降温退火 ,0<r<1 。r越大,降温越慢;r越小,降温越快 22 /* 23 * 若r过大,则搜索到全局最优解的可能会较高,但搜索的过程也就较长。若r过小,则搜索的过程会很快,但最终可能会达到一个局部最优值 24 */ 25 i ++ ; 26 }
在该问题下,如果想得到新的状态Y(i+1),还不是十分清晰。换句话说,我们需要考虑如何得到邻近解,我采用的策略有两个:一是将两个顾客位置调换,即挑两个顾客出来,让一个顾客去另一个顾客的设施,另一个顾客去该顾客的设施。二是让一个顾客去另一个设施。顾客都是随机挑选的,两个策略在某个时刻时仅会执行一个。另外如果执行策略时,发现某些不合法的行为,就不会执行,直接放弃,例如某个设施容量不足。因为策略和顾客都是随机挑选的,且执行策略的次数会很大,所以放弃执行某次策略并不会影响整体效果。
综上,我们执行模拟退火的步骤如下:
①为了方便,状态初始化为贪心算法里的结果,设定初始温度,终止温度,温度下降率。
②开始循环,在某个温度时(内循环),根据上述两种策略得到临近解,然后将得到的临近解和当前解进行比较,采取状态转移的步骤,由公式得到概率,决定是否向较差的情况转移。内循环结束后,将当前解与最优解比较,更新最优解。开始降温。
③当温度降至终止温度时,结束循环。得到该算法下最有解。
代码如下:
1 public int SimulateAnneal() { 2 double temper = 100000; //初始温度 3 double minTemper = 0.001; //终止温度 4 double coolRate = 0.99; 5 double count = 1000; 6 // 初始状态,为了方便选用贪婪算法的解 7 int bestVal = Greedy(); 8 int curVal = bestVal; 9 int nextVal = bestVal; 10 List<Facility> facilityListBestCopy = new ArrayList<Facility>(); 11 List<Customer> customerListBestCopy = new ArrayList<Customer>(); 12 for (Facility faci : facilityList) { 13 facilityListBestCopy.add(new Facility(faci)); 14 } 15 for (Customer cust : customerList) { 16 customerListBestCopy.add(new Customer(cust)); 17 } 18 while (temper > minTemper) { 19 for (int i = 0; i < count; i++) { 20 //拷贝,用于还原 21 List<Facility> facilityListCopy = new ArrayList<Facility>(); 22 List<Customer> customerListCopy = new ArrayList<Customer>(); 23 for (Facility faci : facilityList) { 24 facilityListCopy.add(new Facility(faci)); 25 } 26 for (Customer cust : customerList) { 27 customerListCopy.add(new Customer(cust)); 28 } 29 nextVal = GetNextResult(curVal); 30 double delta = nextVal - curVal; 31 if (delta < 0) { 32 curVal = nextVal; 33 } else { 34 if (Math.exp(-delta/temper) > Math.random()) { 35 curVal = nextVal; 36 } else { 37 facilityList = facilityListCopy; 38 customerList = customerListCopy; 39 } 40 } 41 } 42 43 if (curVal < bestVal) { 44 bestVal = curVal; 45 facilityListBestCopy = facilityList; 46 customerListBestCopy = customerList; 47 } 48 temper *= coolRate; 49 } 50 facilityList = facilityListBestCopy; 51 customerList = customerListBestCopy; 52 return bestVal; 53 }
五、运算结果
设施开启状态和顾客去了哪个设施的结果可以在https://github.com/thougr/CFLP/tree/master/src/docs查看。
下面展示每个实例的运算时间和问题的结果(时间精度为毫秒):
result(SA) | Time(s) | result(Greedy) | Time(s) | |
p1 | 8958 | 2.738 | 9440 | 0.001 |
p2 | 8010 | 2.187 | 8126 | 0 |
p3 | 9389 | 1.974 | 10126 | 0.001 |
p4 | 10714 | 1.978 | 12126 | 0 |
p5 | 9142 | 1.966 | 9375 | 0 |
p6 | 7809 | 1.985 | 8061 | 0.007 |
p7 | 9577 | 1.971 | 10061 | 0.001 |
p8 | 11173 | 1.931 | 12061 | 0 |
p9 | 8742 | 2.074 | 9040 | 0.001 |
p10 | 7617 | 2.045 | 7726 | 0.002 |
p11 | 9077 | 2.508 | 9726 | 0.002 |
p12 | 10132 | 2.066 | 11726 | 0 |
p13 | 8492 | 2.418 | 12032 | 0 |
p14 | 7526 | 2.391 | 9180 | 0.002 |
p15 | 8937 | 2.512 | 13180 | 0 |
p16 | 10764 | 2.458 | 17180 | 0.001 |
p17 | 8378 | 2.335 | 12032 | 0.002 |
p18 | 7152 | 2.351 | 9180 | 0.002 |
p19 | 9042 | 2.406 | 13180 | 0 |
p20 | 11071 | 2.417 | 17180 | 0 |
p21 | 8667 | 2.427 | 12032 | 0 |
p22 | 7194 | 2.402 | 9180 | 0.001 |
p23 | 8746 | 2.434 | 13180 | 0 |
p24 | 11483 | 2.394 | 17180 | 0 |
p25 | 13191 | 5.039 | 19197 | 0.002 |
p26 | 11022 | 4.95 | 16131 | 0.002 |
p27 | 13037 | 4.919 | 21531 | 0.002 |
p28 | 16410 | 4.925 | 26931 | 0.002 |
p29 | 13289 | 4.96 | 19305 | 0.001 |
p30 | 12171 | 4.893 | 16239 | 0.001 |
p31 | 14228 | 4.937 | 21639 | 0.001 |
p32 | 15903 | 5.005 | 27039 | 0.001 |
p33 | 12220 | 4.973 | 19055 | 0.002 |
p34 | 11004 | 5.006 | 15989 | 0.001 |
p35 | 13637 | 4.926 | 21389 | 0 |
p36 | 15004 | 4.929 | 26789 | 0 |
p37 | 11935 | 4.946 | 19055 | 0 |
p38 | 10984 | 4.933 | 15989 | 0.001 |
p39 | 12984 | 4.944 | 21389 | 0.001 |
p40 | 14984 | 4.951 | 26789 | 0 |
p41 | 7103 | 2.932 | 7226 | 0 |
p42 | 6678 | 3.201 | 9957 | 0 |
p43 | 6758 | 3.038 | 12448 | 0 |
p44 | 7128 | 2.848 | 7585 | 0 |
p45 | 7478 | 3.102 | 9848 | 0 |
p46 | 6160 | 3.044 | 12639 | 0 |
p47 | 6257 | 2.865 | 6634 | 0 |
p48 | 6642 | 3.069 | 9044 | 0 |
p49 | 5658 | 3.048 | 12420 | 0 |
p50 | 9239 | 3.12 | 10062 | 0 |
p51 | 7920 | 3.451 | 11351 | 0.001 |
p52 | 9247 | 3.042 | 10364 | 0 |
p53 | 9319 | 3.43 | 12470 | 0 |
p54 | 9034 | 3.028 | 10351 | 0 |
p55 | 7938 | 3.451 | 11970 | 0 |
p56 | 22710 | 6.109 | 23882 | 0.001 |
p57 | 29464 | 6.079 | 32882 | 0.001 |
p58 | 43765 | 6.105 | 53882 | 0 |
p59 | 32854 | 6.113 | 39121 | 0.001 |
p60 | 23086 | 6.144 | 23882 | 0.001 |
p61 | 30093 | 6.193 | 32882 | 0.002 |
p62 | 41891 | 6.261 | 53882 | 0.001 |
p63 | 31788 | 6.32 | 39121 | 0.001 |
p64 | 22443 | 6.136 | 23882 | 0.003 |
p65 | 29279 | 6.15 | 32882 | 0.001 |
p66 | 44219 | 6.124 | 53882 | 0.001 |
p67 | 32471 | 7.23 | 39671 | 0 |
p68 | 23024 | 6.149 | 23882 | 0.001 |
p69 | 30318 | 6.145 | 32882 | 0.017 |
p70 | 43835 | 6.152 | 53882 | 0 |
p71 | 32071 | 6.128 | 39121 | 0 |
|
|
|
|
|
由上面的运算结果可以看出,贪心算法运算的很快,但相对来说结果没有那么好,模拟退火算法运算时间上升了很多,但结果优化了很多。
完整代码可以在 https://github.com/thougr/CFLP 看到。