赛题分析
今年被中兴坑了一波,受美国制裁,比赛终止,现在官网都打不开了,没看过赛题的先了解下赛迪杰斯特拉赛题及测试数据。
简单来理解就是做n次选择题,看谁的分数更高。从测试赛题来看,网格中在网格中有1000个链路需要填入,每个链路都有三个选择,而我们的目标是要使填完后网格中的最大链路利用率最低。看起来简单,但是要在一分钟内尽可能地逼近最优解(用工具测试的样例的最优解是37.08左右,这需要运行几分钟的时间),还是很有挑战的。
初始解法
一开始,我沿用的是华为软挑的思路,采用首次适应法+模拟退火算法,适应函数为当前网格最大链路利用率,在一次退火中,依次遍历在三个链路中选择适应值最小的那个放入,选取n次,选取完毕。退火的评估函数为选取完毕后的网格最大利用率,然后随机交换两个链路的顺序,重新选取,最后退火结束后可以得到一个局部最优解。
public static double assign(Requests requests,Map<String,Node> map){
double T = 10;
double minT = 1;
double r = 0.999;
double k = 50000;
//保存每个链路请求选取的备选方案
chosePathNumbers = new int[requests.getTotalRequestNum()];
// double finalMaxRateOfAllRequests = fail;
double tmpMaxRateOfAllRequsts=0;
Requests newRequests = new Requests(requests.getTotalRequestNum(), requests.getEachRequestPathNum());
// clone(requests,newRequests);
//打乱请求数据
Collections.shuffle(requests.getRequestsList());
while(T>minT){
//备份
ArrayList<Request> requestsList = requests.getRequestsList();
//交换两个请求
int[][] arr = exchange(requestsList);
int reqIdx = 0;
for(Request req:requestsList){
double minRateOfOneRequest=1;
int oneRequestChosePathNumber = 0;
//在多条备用请求路径中选取一条最优的路径
//评价函数是路径中使用的链路的最大带宽负载,最小的那个
for (int i=0;i<requests.getEachRequestPathNum();i++) {
double rate = computeMaxLinkLoadOfThePath(req.requestBandWidth,req.paths.get(i),map);
// System.out.print("rate:"+rate);
if(rate < minRateOfOneRequest){
minRateOfOneRequest = rate;
oneRequestChosePathNumber = i;
}
}
// System.out.println("minRateOfOneRequest:"+minRateOfOneRequest);
//说明所有备选链路带宽都不够,分配失败,结束放置,打乱requests顺序
if(minRateOfOneRequest>=1){
tmpMaxRateOfAllRequsts = fail;
System.out.println("放置失败");
break;
}else{//可装分配
// System.out.println("requestBandWidth:"+req.requestBandWidth);
placePath(req.requestBandWidth,req.paths.get(oneRequestChosePathNumber),map);
}
chosePathNumbers[reqIdx++] = oneRequestChosePathNumber;
}
//分配失败,降温,直接下次循环
if(tmpMaxRateOfAllRequsts==fail){
// System.out.println("NA");
// cancelExchange(requestsList,arr);
// requests.setRequestsList(requestsList);
T = r*T;
continue;
}
tmpMaxRateOfAllRequsts = computeMaxLinkLoadInTopo(map);
// System.out.println("当前全网最大链路利用率:"+tmpMaxRateOfAllRequsts);
//如果当前带宽利用率更低,保存更小的最大带宽利用率和选择的链路下标
//评价函数是当前节点中的最大链路的带宽负载,使其变的更小
if(tmpMaxRateOfAllRequsts<=finalMaxRateOfAllRequests){
finalMaxRateOfAllRequests = tmpMaxRateOfAllRequsts;
// finalChosePathNumbers = tmpChosePathNumbers.clone();
}else{
if( Math.exp(k*(finalMaxRateOfAllRequests - tmpMaxRateOfAllRequsts)/T)> Math.random() ){
finalMaxRateOfAllRequests = tmpMaxRateOfAllRequsts;
// finalChosePathNumbers = tmpChosePathNumbers.clone();
}else{//取消交换
cancelExchange(requestsList,arr);
}
}
T = r*T;
}
return finalMaxRateOfAllRequests;
}
但是这有一个缺陷,所以每选择一个链路分配,都要重新计算链路中的最大链路带宽利用率,这就需要持续改动拓扑图的map,放入一个链路就要改动map中对应的所有链路带宽。分配n个就需要计算n次。最后遍历网格map计算最大链路带宽且每轮分配的总链路和上次轮的链路相比改动是不可控制的,不利于模拟退火找寻局部最优解。且整个过程相当于是两层循环。运行时间长,减少了随机次数,难以得到很好的解。此方案网格最大链路带宽利用率可以取得38.3左右。
优化方法
新的方法,在旧方法的基础上改进。首先从第一条链路开始选取,每次选取的链路使得当前的网格最大链路带宽利用率最小,一直选取到最后一个链路,放置完毕。样例中,这样做,利用率为40左右。然后在此基础上,使用模拟退火,每次退火改变一个链路的选择,换成三个链路中的另外两个。这样每次该改变是可控的,一次退火,不需要一个一个的重新添加链路,只需要计算一次网格利用率,每轮计算总的网格带宽利用率。只需要减去去除的链路带宽,增加新选择的链路带宽,再遍历一次网格,减少了计算量,同样的时间,可以有更多的退火次数。调整模拟退火参数,此方案网格最大链路带宽利用率可以取得37.19左右。1分钟内非常逼近最优解了,我比较满意。
public static double assign(Requests requests,int[][] totalBW,int[][] usedBW,int[][] link){
double T = 10;
double minT = 1;
double r = 0.999998;
int num = requests.getEachRequestPathNum();
ArrayList<Integer> list = new ArrayList<>(num);
for(int i=0;i<num;i++){
list.add(i);
}
//保存每个链路请求选取的备选方案
double newMaxRateOfAllRequsts=0;
chosePathNumbers = new int[requests.getTotalRequestNum()];
maxRateOfAllRequests = init(chosePathNumbers,num,requests, totalBW, usedBW,link);//随机初始化
while(T>minT){
int[] change = new int[3];
changeNumbers(change,chosePathNumbers, list);
updateMap(change,requests,usedBW);
newMaxRateOfAllRequsts = computeMaxLinkLoadInTopo(usedBW,totalBW,link);
//如果当前带宽利用率更低,保存更小的最大带宽利用率和选择的链路下标
//评价函数是当前节点中的最大链路的带宽负载,使其变的更小
if(newMaxRateOfAllRequsts<=maxRateOfAllRequests){
maxRateOfAllRequests = newMaxRateOfAllRequsts;
}else{
if( Math.exp(k*(maxRateOfAllRequests - newMaxRateOfAllRequsts)/T)> Math.random() ){
maxRateOfAllRequests = newMaxRateOfAllRequsts;
}else{
revertPath(change, requests,chosePathNumbers, usedBW);
}
}
T = r*T;
}
return maxRateOfAllRequests;
}
总结与反思
通过这次的解题,我对模拟退火算法的理解更深刻了,之前存在了结果不稳定,那是因为参数没有设置好,模拟退火退化为近似随机,设置好参数,基本上结果还是很稳定的。同时还学习了遗传算法,编写了代码实现,但是效果很差。
感兴趣的可以看看我的迪杰斯特拉代码。