赞
踩
供需关系:客户节点(需求)<——————> 边缘节点(供给)
求解一种最优的供给策略,使得边缘节点的成本最低(注意成本是按95值进行计算的,也是这个问题最关键的点)。
需求表(demand.csv、客户节点)
mtime | CB | CA | CE | CX |
---|---|---|---|---|
00 | 9706 | 113409 | 5209 | 0 |
05 | 8127 | 11154 | 4262 | 300 |
10 | 7243 | 9865 | 3713 | 500 |
供给表(site_bandwidth.csv、边缘节点或者叫服务器节点)
site name | bandwidth | |
---|---|---|
S1 | 81920 | |
SB | 87040 | |
SD | 74586 |
时延约束表(qos.csv)
site name | CB | CA | CE | CX |
---|---|---|---|---|
S1 | 200 | 186 | 125 | 89 |
SB | 190 | 48 | 458 | 45 |
SD | 340 | 45 | 124 | 335 |
生成一个solution.txt文件,
边缘节点每时刻的带宽和:
time | S1 | SB | SD |
---|---|---|---|
0 | 21613 | 3358 | 3353 |
5 | 18302 | 1220 | 4321 |
10 | 5848 | 6500 | 8973 |
总成本 = 21613+6500 + 8973 = 37086
比赛初期,我写了一个非类的版本,完全用数组或者vector来存储表中的信息,后期又用类来实现了一个相同思路的版本,发现运行时间有了很大的提升。因此后边主要就采用了类版本,直接放弃了原始的函数编程。类或者结构体的形式应该是以牺牲内存空间来换取运行速度的,不过比赛的运行空间基本够用,不用考虑内存问题。
需求表类(客户端类、Client)
typedef pair<int, int> PII;
typedef struct C
{
string name; //客户节点的名字
int id = -1; //此客户节点对应一个唯一id下标
int need = 0; //客户总需求
int re_need = 0; // 客户剩余需求
int server_num = 0; //满足时延的服务器的节点数量;can_use_server.size()
vector<int> can_use_server; //我可以用在哪些服务器上,存放服务器id
vector<PII> server_info; //用了哪些服务器,每个服务器用多少,这个用于带宽的分配和迁移,以及最后的输出文件。
}Client;
服务器类(边缘节点类)
typedef struct S
{
string name; //此服务器的名字
int id = -1; //此服务器节点对应一个唯一id下标
int sup = 0; //服务器总带宽
int re_sup = 0; //服务器的可提供带宽
int client_num = 0; //服务器可对多少个客户提供服务
vector<int> can_sup_client; //这个服务器 可以供哪些客户
vector<float> client_degree; // 与可用供客户机的相关度 用客户需求/服务器总带宽
int order = -1; // 该服务器对象带宽占用量在t时刻中的排序
vector<PII> client_info; // 哪些客户用了这个服务器,用了多少
}Server;
时延信息,因为所有时刻这个信息是不会发生改变的,因此可以直接存数组就行。
下面的这几种思路都是拍脑袋就能想出来的,比较简单。贪心、随机、平均的单独一种思路来做效果明显不如,这几种策略混合着来比较好。
首先贪心,然后平均,最后加上优化的思路。这个思路练习赛效果很好,进入复赛,因为我的有优化思路太慢,代码运行时间就显得不够用了。贪心就是白嫖每个节点4%的时刻,也就是说,既然计算是按照95值的,那么95之上的时刻,服务器能提供多少带宽,就提供多少带宽。然后将剩余的需求平均分配到所有能存放的服务器上去。最后就是优化工作了,改变每个服务器95时刻的分配方案,直观上来看就是迁移,如果这一时刻,其他服务器的成本不是95值,那就向这个服务器迁移,多迭代几次,这个就要看中算法的时间复杂度了。
具体细节需要注意的有,如何白嫖4%的时刻?因为有所有时刻的信息,也就是开启了上帝视角,可以统计一下所有时刻,该服务器所能提供带宽的所有客户端(满足时延要求)的总需求,然后排序。从服务器带宽能力大的服务器开始遍历,处理完需求前4%大的时刻后,换下一个服务器,这时候需要重新统计总需求信息。平均,遍历demand.csv需求表,统计目前这个客户端能存放的服务器个数,求平均,因为这个平均值可能塞不下有些服务器,因此需要加判断,能放多少是多少,放完之后重新求平均值。
核心代码:
/* * 根据客户的剩余需求,来计算服务器目前的总需求信息 */ vector<vector<int>> getGlobal2() { vector< vector<int> > global_info2; //服务器关于时间的剩余全局需求信息表 for(int i = 0 ; i < sum_time; ++ i) { vector<int> v; for(int j = 0 ; j < N; ++ j) { int size_1 = allServers[i][j].client_num; int sum_need = 0; for(int k = 0 ; k < size_1; ++ k) { int client_id = allServers[i][j].can_sup_client[k]; sum_need += allClients[i][client_id].re_need; } if(sum_need > 0) v.push_back(sum_need); } global_info2.push_back(v); } return global_info2; } /* * 将所有能放在当前服务器上的流量节点,全部放入,直到塞满为止 */ void crowed_distribution(const int& time, const int& server_id) { //找到所有能放入此服务器的节点,按照客户满足服务器的数量,从小到大塞入 for(int i = 0 ; i < M; ++ i) { int client_id =pair_serverNums_clientId[i].second; if(allClients[time][client_id].re_need <= 0) continue; if(allServers[time][server_id].re_sup <= 0) break; bool flag = false; if(qos[server_id][client_id]) { //塞满分配 do_distribution(time, server_id, client_id); } } } void mean_distribution(const int& time) { //对该时刻节点的需求,平均分配到能存放的服务器上 for(int i = 0 ; i < M; ++ i) { int client_id = pair_serverNums_clientId[i].second; //如果当前的客户需求为零 直接跳下一个吧 if(allClients[time][client_id].re_need == 0) continue; int number_servers = allClients[time][client_id].server_num; int number_can_use_servers = 0 ; //实际能用的服务器个数 //统计能分配的服务器的个数 for(int j = 0; j < number_servers; ++ j) { int server_id = allClients[time][client_id].can_use_server[j]; if(allServers[time][server_id].re_sup == allServers[time][server_id].sup) number_can_use_servers = number_can_use_servers + 1; } int temp_mean = 0; if(number_can_use_servers != 0) temp_mean = allClients[time][client_id].re_need / number_can_use_servers + 1; if(temp_mean == 0) temp_mean = allClients[time][client_id].re_need; int tt = 0; for(int j = 0; j < number_servers; ++ j) { int server_id = allClients[time][client_id].can_use_server[j]; //如果当前的服务器剩余带宽为零 直接跳下一个吧 if(allServers[time][server_id].re_sup == 0) continue; if(allServers[time][server_id].re_sup != allServers[time][server_id].sup) continue; if(!qos[server_id][client_id]) continue; //执行分配任务 if(tt < number_can_use_servers - 1) { int put_temp = temp_mean; //先判断能分配多少,然后分配 if(allServers[time][server_id].re_sup < temp_mean) { put_temp = allServers[time][server_id].re_sup; //修改剩余的temp_mean temp_mean = allClients[time][client_id].re_need / (number_can_use_servers - tt - 1); } if(allClients[time][client_id].re_need < put_temp) put_temp = allClients[time][client_id].re_need; do_mean_distribution(time, server_id, client_id, put_temp); tt ++; if(allClients[time][client_id].re_need == 0) break; }else if(tt == number_can_use_servers - 1) { int put_temp = allClients[time][client_id].re_need; if(allServers[time][server_id].re_sup < put_temp) put_temp = allServers[time][server_id].re_sup; do_mean_distribution(time, server_id, client_id, put_temp); tt ++; break; } } } }
首先贪心、然后随机、最后优化。这个思路是这样产生的,因为这道题的本质是一个分配问题,在贪心之后想要达到的效果,就是让所有节点所有时刻分配的带宽都更加均衡,而分配均衡问题的一个常用思路就是随机。因此做了尝试,但是最终效果不太理想,猜测可能原因有两个,数据集太少以及线上测试数据集太奇奇怪怪,不适合这种方法。
随机具体来说,就是随机选择一个服务器,能放下多少带宽就放多少,随机一轮之后,顺序放剩余的带宽。
for(int i = M - 1; i > 0; -- i) { int client_id_temp = all_time_client_need_sort[time][i].second; //如果当前的客户需求为零 直接跳下一个吧 if(allClients[time][client_id_temp].re_need == 0) continue; int size_1 = allClients[time][client_id_temp].server_num; uniform_int_distribution<unsigned> u(0, size_1 - 1);//随机数分布类,范围为[0,9]的整数 int random_index = u(e); //这个客户能放到哪些服务器上 int server_id_temp = allClients[time][client_id_temp].can_use_server[random_index]; //如果当前的服务器剩余带宽为零 直接跳下一个吧 if(allServers[time][server_id_temp].re_sup == 0) continue; if(!qos[server_id_temp][client_id_temp]) continue; if(percent95_up[server_id_temp] <= 0) continue; //执行分配, 如果当前服务器完全满足当前客户带宽需求,直接跳下一个吧 do_distribution(time, server_id_temp, client_id_temp); } //3. 剩余的流量, 优先分给能放服务器少的客户节点 for(int i = 0 ; i < M; ++ i) { int client_id_temp = pair_serverNums_clientId[i].second; for(int j = N - 1; j > 0; -- j) { int server_id_temp = pair_ServerSup_ServerId[j].second; //如果当前的客户需求为零 直接跳下一个吧 if(allClients[time][client_id_temp].re_need == 0) break; //如果当前的服务器剩余带宽为零 直接跳下一个吧 if(allServers[time][server_id_temp].re_sup == 0) continue; if(!qos[server_id_temp][client_id_temp]) continue; //执行分配, 如果当前服务器完全满足当前客户带宽需求,直接跳下一个吧 if(do_distribution(time, server_id_temp, client_id_temp)) break; //否则找下一个服务器 } }
使用加权的思想。将服务器按照总需求标准差的顺序以及客户支持度顺序进行加权。总体思想是,优先给服务器所有时刻总需求差距小的安排分配任务,优先给服务器支持客户端数目少服务器优先分配任务。最后两个顺序乘0.5,给每个服务器求一个综合权重,并进行排序,优先给权重大的服务器安排分配任务。
服
务
器
总
权
重
=
服
务
器
标
准
差
顺
序
×
0.5
+
服
务
器
支
持
客
户
数
目
的
顺
序
×
0.5
服务器总权重 = 服务器标准差顺序 \times 0.5 + 服务器支持客户数目的顺序 \times 0.5
服务器总权重=服务器标准差顺序×0.5+服务器支持客户数目的顺序×0.5
上边的公式可以这样理解,服务器总权重,每个服务器会根据这个公式求一个权重,用这个权重给每个服务器进行排序,优先给这个服务器分配带宽。服务器标准差顺序,首先对每个服务器所有时刻求一个总需求,对这个服务器所有时刻的需求,求一个标准差,最后对每个服务器的所有时刻需求方差进行排序。这个可以这样来理解,对于每个服务器来说,所有时刻如果总需求更加均衡,那肯定优先满足这个服务器呀。这样就使得所有时刻的服务器使用量都接近95值,最后计算这个服务器的成本时就很划算。服务器支持客户数目的顺序,每个服务器因为时延原因,所能支持的客户端的数目是不一样的,一个直观的想法肯定是优先满足那些稀缺的服务器资源,满足之后,再去给支持客户数目多的服务器安排。**注意,**我这里是给每个服务器设置了一个使用限制,那就是分配的流量大小不能超过需求最少的时刻的流量大小。因为需求大也有可能最后放置到其它的服务器上,每分配完一个服务器就需要重新计算一下剩余服务器的带宽总需求了。
void distribution0() { int flag_server[max_n]; memset(flag_server, 0, sizeof flag_server); for(int i = 0; i < N; ++ i) //执行服务器的次数 { vector<PII> server_id_weight = getServerIdByWeight();// 获取一个权重排序 ,从小到大的排序 for(int j = N -1 ; j > 0; -- j) //选择服务器 { //判断是否要执行这个服务器的受限分配 int server_id = server_id_weight[j].second; if(flag_server[server_id] == 0) { //计算一下该服务器总需求的,10%的位置,作为受限位置; int cons_temp = getPercent10Need(server_id);//每个时刻总需求的最低值 //总需求是可能大于该服务器带宽的 if(cons_temp >= allServers[0][server_id].sup) cons_temp = allServers[0][server_id].sup * weight_plus; if(cons_temp == 0) continue; //要做回溯 int tt = 0 ; curClients = allClients; curServers = allServers; for(int k = 0; k < sum_time; ++ k) { if(allServers[k][server_id].re_sup <=0 ) continue; //判断一下能塞满的时间数 bool flag = do_distribution_cons(k, server_id, cons_temp); if(flag) tt += 1; } if(tt < N * 0.9) { allClients = curClients; allServers = curServers; } flag_server[server_id] = 1; break; } } } }
其实做到最后的这个思路应该是效果最好的,思想很简单,有策略的贪心,做到最后基本已经能够感觉出来,线上数据集的分布,大概是,服务器容量大的和小的,在所有时刻上的需求是很不均衡的。因此优先满足服务器容量处于中间梯队的服务器,直接使用贪心的策略,塞满这些服务器,满足这些之后,剩余的需求就分配到剩余的服务器上就行。虽然说有点面向数据集编程的意思,但还别说,效果是我想到的所有思路中最好的一个,可惜最后没剩多少提交次数了,要不然就卷进32强了。最后感慨一句,成渝和西北太卷了!
linux读取文件可能会多一个\r,另外写文件时,最后会多一行空行,这是在结果文件格式上遇到的问题。
/* *去除头尾的空格 */ std::string& trim(std::string &s) { if (s.empty()) { return s; } s.erase(0,s.find_first_not_of(" ")); s.erase(s.find_last_not_of(" ") + 1); return s; } /* * 去除字符末尾的\r */ string removeR(string& s) { //c_name = ""; char ch[4]; ch[0]='\0', ch[1]='\0',ch[2]='\0',ch[3]='\0'; int op = 0; for(auto c : s) { if(c != '\r') { ch[op++] = c; } else break; } string res(ch); return res; }
在本地编译运行是没有这样的问题的时候,但在提交时会报编译出错的问题,主要是在for循环的中间截止判定中使用了v.size()这个语句,而这中写法在线上的环境中是会报错误的。
vector<int> v;
//写法一,这种写法会出问题
for(int i = 0 ; i < v.size(); ++i)
cout<<"..."<<endl;
//写法二
int size_1 = v.size();
for(int i =0 ; i < size_1; ++i)
pass
这个比赛我本来打算单干呢,想着应该进复赛是没问题的,做到后期发现,还是有队友比较占优势一些,一个人每天就20次的提交次数,有队友的话,起码提交次数会增多,另外队友多也可以安排不同的分工,比如找想法的找想法、敲代码的敲代码,负责获取各种渠道信息的也可以在这上边多花精力。一个人要干完这些所有事情还是比较耗费精力的,最后我的建议是不建议单干,除非大佬实力特别强,那另说。
还有一点就是,不要着急码代码,一定要先分析清楚问题、先梳理清楚代码结构、先分析分析问题有哪些解决方法,先建好问题的模型后再有计划的敲代码,每一个思路、每一个代码版本一定要做好记录,后期会不断地迭代,不断的更改,有可能修改后的代码还不如原先版本的效果好。一定要做好版本保存工作。
最后,不要轻言放弃,坚持下去!不管最后有没有获奖,只要努力过,对你来说最后都会是一种美好的回忆!
没有经过总结的提交版本的代码,此版本代码写的比较乱。
后续再总结上传github。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。