华为2021软件精英挑战赛复赛赛后方案分享


  

引言:

我是来自成渝赛区UESTC的选手,成渝赛区初赛排名13名,复赛最终排名12,再一次成功拿到手环。成渝赛区总报名人数全国第二,电子科技大学单校报名人数全国第一,太卷了,太卷了。鄙人十分不幸,生在成渝赛区的电子科技大学,据说成渝赛区的排名若是放在其他赛区可以有飞一般的提升,哇,我哭得好大声。初赛正式赛排名 复赛正式赛排名这里做一个复赛的赛后分享吧,对于前排大佬来说没啥用处,仅供后排抱团取暖,菜鸡互学。gitee仓库开源地址

赛题介绍

  云上资源的规划和调度是云计算场景中非常重要的一个优化问题。好的优化算法能够为云运营商节约上亿的运营成本,并为客户提供更稳定、更流畅的云端体验。本次比赛主要是根据用户每天的添加、删除虚拟机请求序列,为云运营商设计总体花费成本最低的每天的迁移虚拟机、部署虚拟机、购买服务器方案,解决用户的请求。初赛时知晓用户所有天的请求序列;而复赛练习赛时为了贴合实际情况,赛题修改为只知晓后续某个固定时间窗口的请求序列,不知道所有天数的请求序列。(这个地方对于我们的代码来说,影响不大,因为我们都是一天天的处理当天请求,没有考虑用到后续天数的请求信息。这也是我们菜的原因,但我觉等大部分队伍可能都没有用到这个信息)。最终复赛正式赛时,现场临时增加需求。本来每天的迁移次数是有上限的,但是复赛正式赛时,增加了一个规则,就是允许你选择一天进行虚拟机的大迁移,也就是这一天的迁移次数没有上限要求,但只允许选择一天进行这样的大迁移。
  主办方提供了不同的服务器和虚拟机。服务器具有不同的型号、规格(CPU核心数,内存大小)、硬件成本、每日耗能成本。每台服务器上都有两个节点,A节点和B节点,A、B节点的拥有的资源(CPU核心数,内存大小)各占总资源的一半。比如以型号为 NV603 的服务器为例,其 A、B 两个节点分别包含 46C 和 162G 的资源,且主办方保证所给的数据中服务器的CPU 核数和内存大小均为偶数。硬件成本即为服务器一次性的购买成本;若服务器上当天部署了虚拟机,则当天具有能耗成本;若其上没有虚拟机,则当天不耗能。虚拟机具有不同的型号、规格(CPU核心数,内存大小)、部署方式。若虚拟机是单节点部署,则其可部署在服务器的任一A、B节点上;若其为双节点部署,则其需的一半资源部署在A节点上,另一半资源部署在同一服务器的B节点上。当然所部署的虚拟机不能超过服务器上的资源容量。

服务器类型
虚拟机类型

资源规划和调度

  • 容量约束:服务器可以用来容纳用户的虚拟机,但是服务器上的任意一个节点(A和 B)上的资源负载(CPU 和内存)均不能超过其容量上限。
  • 请求类型:用户的请求共分为两种类型:创建请求和删除请求。创建请求表示用户向公有云平台提交的一个创建虚拟机的请求;删除请求表示用户提交的删除一台之前创建的虚拟机的请求。
  • 请求序列:由一系列请求构成的序列。题目会给出接下来若干天中每一天用户的请求序列,根据每天的请求序列,你需要进行相应的资源规划和调度。
  • 数据中心扩容:在得知了一天的请求序列后,你可以在实际进行调度前进行一次数据中心扩容。即购买一些新的服务器来容纳后续用户请求的虚拟机,同时你需要付出所购买服务器相应的硬件成本。你需要指定购买哪些类型的服务器以及购买的数量。初始时你没有任何服务器。
  • 虚拟机迁移:在完成扩容后,在处理每一天的新请求之前,你还可以对当前存量虚拟机进行一次迁移,即把虚拟机从一台服务器迁移至另一台服务器。对于单节点部署的虚拟机,将其从一台服务器的 A 节点迁移至 B 节点(或反之)也是允许的。迁移的目的服务器和节点必须有足够的资源容纳所迁移的虚拟机。迁移的虚拟机总量不超过当前存量虚拟机数量的千分之五。即假设当前有 n 台存量虚拟机,每天你可以迁移的虚拟机总量不得超过 5n/10000 向下取整。初赛时这个上限是 5n/10000,复赛时这个上限提高到了3n/100,也就是说每日可迁移的虚拟机数量提高了。
  • 部署虚拟机:在完成扩容和迁移之后,你需要按顺序处理当天所有的新请求。对于每一个创建虚拟机的新请求,你要为虚拟机指定一台服务器进行部署。若虚拟机是单节点部署的,你还需要指明部署在服务器的 A 节点还是 B 节点。处理请求的过程中,任意一台服务器上每个节点容纳的虚拟机资源总和都不能超出节点本身的资源容量(指 CPU 和内存两个维度)。
  • 未知请求序列:在初赛中,我们面对的是提前知晓了未来所有用户请求序列的场景,这在实际中是很难作到的。现实场景中,我们往往只能预测后续较短的一段时间内用户可能的请求序列。所以在复赛中,你需要面对这种存在未知的场景,进行云上的资源规划和调度。

输入示例:

输入示例

  • 上述示例中,第 1 行表示共有两种类型的服务器可供采购,第 2 和第 3 行分别描述这两种类型服务器的详细信息。
  • 第 4 行表示共有两种类型的虚拟机提供给用户购买,第 5 和第 6 行分别描述这两种类型的虚拟机的详细信息。
  • 第 7 行表示共有三天的用户请求序列。
  • 第 8 行表示第一天的用户请求序列中共有两条请求,第 9 和第 10 行分别描述这两条请求。
  • 第 11-13 行描述第二天的用户请求。
  • 第 14-17 行描述第三天的用户请求。

输出示例:

输出示例

  • 上述示例中,第 1 行表示在第一天购买两种类型的服务器,第 2 和第 3 行分别表示购买的服务器型号和数量。
  • 第 4 行表示迁移 0 台虚拟机。若有迁移,则格式为**(虚拟机 ID, 目的服务器 ID)(虚拟机 ID, 目的服务器 ID, 目的服务器节点)**。例如(3, 1)表示将 ID 为 3 的虚拟机从当前所在服务器迁移至 ID 为 1 的服务器,该虚拟机必须是双节点部署的;(4, 1, A)表示将 ID 为 4 的虚拟机从当前所在服务器迁移至 ID 为 1 的服务器的 A 节点,该虚拟机必须是单节点部署的。
  • 第 5 和第 6 行表示第一天创建的两个虚拟机分别部署到服务器 0 的 A 节点和服务器 0 的 B 节点。
  • 第 7-12 行描述第二天的决策信息

总体处理流程

变量数据结构定义(c++实现)

//服务器结构
struct Server{
	string type;
	int cpuCore, memSize;//这个值保持不变
	int cpuA, cpuB, memA, memB, serverCost, powerCost;
	int rest;
	//空参构造器
	Server(){}
	//带参构造器
	Server(string type, int cpuCore, int memSize, int sCost, int pCost):type(type),cpuCore(cpuCore),memSize(memSize),serverCost(sCost),powerCost(pCost){
		this->cpuA = cpuCore / 2;
		this->cpuB = this->cpuA;
		this->memA = memSize / 2;
		this->memB = this->memA;
	}
};

//虚拟机结构
struct VirtualMachine{
	string type;
	int cpuCore, memSize, isDual;
	//空参构造器
	VirtualMachine(){}
	//带参构造器
	VirtualMachine(string type, int cpuCore, int memSize, int isDual):type(type),cpuCore(cpuCore),memSize(memSize),isDual(isDual){}
};

//请求结构
struct Request{
	string type;//add or del
	string vmType, vmId;
	int vmCpu, vmMem, isDual;
	Request(){}//空参构造器
	Request(string type, string vmType, string vmId, int vmCpu, int vmMem, int isDual):type(type),vmType(vmType),vmId(vmId),vmCpu(vmCpu),vmMem(vmMem),isDual(isDual){}
	//当其为一条删除请求时,只有其vmId有效,其他的数据别访问,会出错。
	Request(string type, string vmId):type(type),vmId(vmId){}
};

/******* 全局变量 ***********************/
int reqDays=0, windowDays=0;
unordered_map<string, Server> svInfos;              //原始的服务器信息
unordered_map<string, VirtualMachine> vmInfos;      //原始的虚拟机信息
vector<Server> svCostServers;                       //按硬件价格排序服务器(小 --> 大)
vector<Server> pwCostServers;                       //按每日能耗排序服务器(小 --> 大)
vector<vector<Request>> daysReqs;                   //所有的请求

vector<Server> svResources;                         //购买的服务器
#define DeployInfo vector<int>                      //虚拟机的部署信息。{服务器编号, 占用cpu, 占用mem, maybe 部署节点}。长度为3->双节点部署;当长度为4->单节点部署(0->A, 1->B)
unordered_map<string, DeployInfo> vmDeployInfos;    //记录虚拟机运行在哪台服务器上(key=虚拟机id(不是类型), value={服务器编号,占用cpu,占用mem,部署节点})
vector<vector<string>> svRunVms;                    //记录已购的每台服务器上的各自运行的虚拟机Id。长度和已购服务器的数量对应
vector<int> svRunVmsNumber;                         //记录已购的每台服务器上的各自运行的虚拟机数量(看似显得多余,其实是为了方便后续的处理)。长度和已购服务器的数量对应
vector<vector<string>> purchase_info;//每天的购买信息
vector<vector<string>> migrate_info;//每天的迁移信息
vector<vector<string>> deploy_info;//每天的部署信息

  先读取所有的数据,构建所有的服务器和虚拟机信息(svInfosvmInfos),并将服务器按照硬件价格和每日耗能排序(svCostServerpwCostServer)。然后依次读入所有天的请求,存入 daysReqs 中(复赛中不能读入所有天的数据,只能读到指定时间窗口的后续数据),后续可以通过 dayReqs[day] 从中取出指定天的请求序列数据。数据处理完成后,便开始依次处理每天的请求序列(包括添加虚拟机和删除虚拟机)。我们队伍的思路是,每天开始处理前,先将当天的请求序列排个序,按照虚拟机所需的资源总量(cpu + mem)从大到小排序,而且把单双节点请求分开,虚拟机核内比(cpu/mem)太小或太大的都单独考虑,而且还有一个很重的就是这个排序是分段排序,以删除请求分段。所以在每一段内的顺序是:单节点请求在前面,然后是双节点请求,最后是核内比很奇葩排在最后。分段排序是为了保证删除请求所在序列中的位置固定不变,避免后面处理时出现错误,因为某个已部署的虚拟机删除后,其腾出来的资源就可以部署其他的虚拟机了。如果你问我为什么要把单节点、双节点、奇葩节点单独弄出来排序?而且单节点还得放前面,奇葩节点放后面?那我只能说,这是我们调参调出来的结果,确实这样做对于我们的策略来说最后的成本最低。代码中还有一些地方也是用了一些 magic number,不要惊讶,都是一步步调参调出来的。
  排序完成后,便可以正式开始处理当天的请求了。顺序遍历排序后的虚拟机请求,若其为删除请求,那就找到其之前部署到的服务器,删除其部署信息,并修改服务器的剩余资源即可。若其为添加请求,则遍历目前所购买的服务器,判断该虚拟机是否能部署到该服务器上,后面会说如何判断。若能部署,则直接部署到该服务器上即可,即修改对应服务器上的剩余资源量,记录该虚拟机的一些部署信息。若已购买的服务器上都不能部署该虚拟机请求,则把该虚拟机请求记录下来,后续统一为所有不能部署的虚拟机购买服务器进行单独的部署,也就是这部分记录下来的暂时无法部署的虚拟机会全部部署到当日新购买的服务器上。置于这个购买的策略后面会讲到,其实也不难,我们选择的是一个贪心购买的策略。因为试了很多策略,发现这个策略对于我们来说最好。当然购买时还有一个很神奇的骚操作,后面再讲吧。
  以上讲了我们的部署和购买的过程,其实还有个很重要的迁移过程。赛题要求是,先购买,在迁移,在部署。我们队伍将迁移操作放到了最前面,即不管三七二十一,每天一上来先迁移,尽量腾空服务器出来,这样可以减少耗能成本,然后再部署当日请求,购买服务器部署剩余请求。但是我们必须得考虑输出信息的顺序,因为这点也很重要,按要求,先输出购买信息,再输出迁移信息,最后输出部署信息。只要保证这个输出的顺序是正确的就行。有人可能会问,我先迁移后,后面部署时会把本来腾空的服务器又占用了,岂不是还是会有耗能成本。这点确实是这样,但是,如果是先部署再迁移,那么根据题意要求,迁移时不能考虑新部署的虚拟机,因为赛题要求迁移时考虑的虚拟机是当前存量的虚拟机,那么这样考虑的东西变多了,而且也不好处理输出,我们想的是怎么简单怎么来。所以就把迁移直接放到了每一天的开始,这样的话,当前存量的虚拟机就是当前已部署的虚拟机,没有什么歧义;而且我们发现所提供的练习数据中,后面的很多天中的请求几乎都是删除请求,也就是说,在后面几天里,迁移之后,本来腾空的虚拟机当天也基本不怎么会有虚拟机再部署上来了,达到降低能耗成本的目的。这也是我们选择先迁移的整个心路历程。至于如何进行迁移,哪台虚拟机该迁移到哪台服务器上,请听后续的分解。这里只是给出了整个流程:排序,迁移,部署,购买部署。

部署策略

  当一个添加虚拟机的请求到来时,如何判断用哪个已购买的服务器去部署这个虚拟机呢。这里我们队伍做了很多尝试,比如先将已购的服务器排个序,按照服务器上的剩余资源量(A、B节点剩余cpu和mem的总量和)从小到大排序,然后找到第一个能装下该虚拟机的服务器,就将该虚拟机部署到该服务器上。这样做,确实也是最简单的最容易想到的方法,但是效果不咋地。我们还尝试了一些其他的方法,这里就不一一说明了,直接说一说我们最终的方案。不用对已买服务器进行排序,而是直接挨个遍历所以已买服务器,找到该虚拟机能部署到的(即满足资源要求)且部署之后该服务器剩余资源最小,且部署后该服务器A、B节点不失衡的服务器进行部署,如果找不到这样服务器,则把这个虚拟机请求记录下来,后续统一进行购买部署处理。一开始我们其实并没有想这么多,只想到了部署之后若该服务器剩余资源最小,那么我们就部署到该服务器上。但后来我们考虑了平衡部署,也就是说如果这个虚拟机部署到这个服务器上后,这个服务器上剩余的CPU资源和mem资源比例失衡了,那我们就不要部署到这样的服务器上。这个失衡的比例我们设置成了 ratio<0.13 或 ratio>7.5 都算作是失衡了,反正这个参数我们调了很久才调出来一个比较满意的值(其中ratio = 剩余CPU/剩余MEM)。这个过程讲出来看似很简单,其实实现起来考虑的东西还是比较多的,因为要考虑虚拟机的单双节点不同部署方式的差异和服务器A、B资源不同剩余量的情况。所以就是一两句话也说不清楚,但我们确实是考虑了平衡部署后,效果有了很大的提升。具体步骤还得仔细参考我们的代码中 findBestServer() 这个函数。这个比赛主要不是比时间,而是看最后谁提供的方案成本更低,所以可以疯狂遍历,并且我们发现,这个遍历也花费不了所少时间。比赛要求在90内得出结果,我们的时间花费排名应该算是比较靠前的,相比于其他队伍。所以时间并不是阻碍我们上分的主要因素,哎,还是方案太菜了,进不了决赛。具体细节请参考我们代码中的 handleOneDayRequests()函数

购买策略

  前面说到,我们会统一考虑为部署不了的虚拟机购买服务器。我们用一个临时变量专门记录当天新买的服务器,然后做好编号映射(为了满足输出要求,据说前期很多队伍掉到了这个坑里,没有做编号映射。比如你买的时候是按ABCABC这些类型买的,那么你默认他们的编号依次是123456,但是赛题要求的输出是AABBCC,编号依次是123456,这个时候你若是没有做好编号映射,判题系统就会给你判错)后更新到全局的已购服务器列表中。我们依次遍历收集到的前期无法部署的虚拟机,看当天新买的服务器中是否有服务器能部署上这台虚拟机,这个判断标准类似于上面部署策略中使用的 findBestServer() 函数,但是在平衡部署的判断上略有不同,所以我们单独又写了一个 findBestServer() 函数来在这里做判断。如果在当日新买的服务器中能找到能部署的服务器,那也就可以直接部署了;如果找不到能部署的服务器,那就以这个虚拟机请求为准去考虑如何购买服务器来。前面我们将服务器种类按照硬件价格和每日耗能排过序(svCostServerpwCostServer)。我们在购买时,设定了一个算是超参数的东西,一个天数 T = 333。当前处理的请求天数低于这个参数值时,我们按照每日耗能排序去购买,当前处理的请求天数高于这个参数值时,我们按照硬件价格排序去购买。为什么,大家可以想一想,其实也很简单。你先买的服务器毕竟要一直用到最后一天,得让他们的耗能低才好;后面买的服务器耗电可能就不是主要考虑因素了,怎么买便宜就怎么来。置于为什么我们将这个值设置成了 333,那也是调参调出来的结果,所以这玩意就很神奇。当我们购买时,其实也并不是说只要这个服务器能放下这个虚拟机就购买,我们还是得为后续的虚拟机稍微考虑一下,所以我们购买时规定这台服务器要能装下 1.32倍 的该虚拟机资源才购买;如果所有的都不能放下 1.32倍,那就降低一点设为 1.2倍;如果还是不能放下,那就回归本质到 1倍,主办方已说明每台虚拟机至少有一台服务器能装下。初赛时我们就是靠着这个倍数购买上的分,在最后千钧一发之际,从三十几名飙到了十几名,成功进入复赛。这个想法也是我们队员提出来的,感觉还挺神奇的,估计很多队伍都没敢这么去尝试,都想着最低能装下该虚拟机就行了,但确实考虑倍数购买效果提升很大,至少对于我们的这份代码来说,提升很大。直接决定了我们能不能进复赛。具体细节请参考我们代码中的 buyServersForReqs()函数

迁移策略

  迁移这块也是很复杂的,我们前面也讲到了我们队伍将迁移拿到了最前面进行处理。每日的虚拟机迁移次数是有上限的,且复赛和初赛中这个上限还不一样,复赛中提高了这个上限值,允许迁移的次数变多了。首先我们将当前已购买的服务器进行了排序,按照其上剩余的资源量从大到小进行排序,第二序为耗电成本,整体迁移方向是剩余资源量大的服务器上的虚拟机往剩余资源量小的服务器上迁移,尽量多的空出服务器来,节约耗电成本。如果单考虑这一点,效果不是很好,肯定是进不了复赛的。因为这个迁移的策略对最终的结果影响很大,大于能节约 2 亿左右的成本,直接影响你的排名。我们最后的优化,主要就是放在了迁移这块。按照我们一开始的想法,剩余资源量大的服务器上的虚拟机往剩余资源量小的服务器上迁移,我们实现了一版出来,每次依次从前面的服务器上依次取出其上部署的虚拟机,往后面能迁移到的服务器且迁移后剩余资源量最少的服务器上迁移,直到到达当日最大迁移次数或不能再迁移。这个方法不仅速度慢,迁移效果还不是很好,达不到最大迁移次数,严重影响效果,而且还经常超时。我们在这个地方卡了很久,掉了很多头发,最终一步步优化,才走出了超时这个坑。最终我们的迁移策略整体方向没有变,但是中间进行了很多优化,使得效果提升。先是一样的,将当前已购买的服务器按剩余资源量从大到小排序,然后我们将今日即将要删除的虚拟机统计了一下(因为我们知道当日的请求),每次依次从前面的服务器上依次取出其上部署的虚拟机。如果该虚拟机即将在当天被删除,那么就不考虑当台虚拟机的迁移;如果该虚拟机是才迁移到这台服务器上来的,也不考虑该虚拟机的迁移;如果该虚拟机无法找到合适的迁移目标服务器,也不迁移;其他情况下该虚拟机能正常迁移,动态更新服务器上的剩余资源量。最重要的一点来了,如果该台服务器上目前遇到了一台不能迁移的虚拟机,那么这台服务器当天的耗能成本肯定是不能被节省下来了,因为它至少有这么一台不能迁移的虚拟机运行着。那么一旦出现这种情况,我们就记录下该台服务器,并且该台服务器上余下的虚拟机也不考虑去迁移了,直接考虑下一台服务器上虚拟机的迁移。后续我们考虑其他的虚拟机的迁移服务器目标时,将这些记录下来的服务器也当做目标来考虑,因为这些服务器反正是开机状态,不能省电了,那我们就得把它利用起来。还有一点重要的就是,如果这台服务器上的装载的资源很满,那其上的虚拟机也就不考虑迁移了,我们用一个衡量标准(资源剩余量 / 资源总容量 < 0.018)来判断装载率。我们一点点实现了上面的所有想法,最后发现确实效果不错,时间大大缩短了,效果大大增加了,不过这个过程确实很复杂,还有很多细节需要处理,实现过程中又掉了很多头发。具体细节请参考我们代码中的 migrate()函数

赛后感受

  • 个人还是比较喜欢该类比赛,能为企业解决实际问题
  • 细节决定成败,代码编写还是要规范,命名也最好注意一下,简明扼要
  • 有队友还是好得多,不用孤军奋战
  • 一个好赛区也很重要,成渝赛区简直不要太卷,但是莫得办法
  • 思想碰撞,交流沟通也很重要
  • 快速搭建出框架,后续不断优化上分

开源代码

仅供参考,确实有点复杂,若没有参加比赛理解题意,很难看懂。一下是复赛练习赛时我们队伍的最终代码,也可访问我的 开源仓库,查看所有版本的代码。

#include <iostream>
#include <string>
#include <unordered_map>
#include <queue>
#include <set>
#include <vector>
#include <ctime>
#include <algorithm>
#include <assert.h>
#include <math.h>

using namespace std;

//服务器结构
struct Server{
	string type;
	int cpuCore, memSize;//这个值保持不变
	int cpuA, cpuB, memA, memB, serverCost, powerCost;
	int rest;
	//空参构造器
	Server(){}
	//带参构造器
	Server(string type, int cpuCore, int memSize, int sCost, int pCost):type(type),cpuCore(cpuCore),memSize(memSize),serverCost(sCost),powerCost(pCost){
		this->cpuA = cpuCore / 2;
		this->cpuB = this->cpuA;
		this->memA = memSize / 2;
		this->memB = this->memA;
	}
};

//虚拟机结构
struct VirtualMachine{
	string type;
	int cpuCore, memSize, isDual;
	//空参构造器
	VirtualMachine(){}
	//带参构造器
	VirtualMachine(string type, int cpuCore, int memSize, int isDual):type(type),cpuCore(cpuCore),memSize(memSize),isDual(isDual){}
};

//请求结构
struct Request{
	string type;//add or del
	string vmType, vmId;
	int vmCpu, vmMem, isDual;
	Request(){}//空参构造器
	Request(string type, string vmType, string vmId, int vmCpu, int vmMem, int isDual):type(type),vmType(vmType),vmId(vmId),vmCpu(vmCpu),vmMem(vmMem),isDual(isDual){}
	//当其为一条删除请求时,只有其vmId有效,其他的数据别访问,会出错。
	Request(string type, string vmId):type(type),vmId(vmId){}
};

/******* 全局变量 ***********************/
int reqDays=0, windowDays=0;
unordered_map<string, Server> svInfos;              //原始的服务器信息
unordered_map<string, VirtualMachine> vmInfos;      //原始的虚拟机信息
vector<Server> svCostServers;                       //按硬件价格排序服务器(小 --> 大)
vector<Server> pwCostServers;                       //按每日能耗排序服务器(小 --> 大)
vector<vector<Request>> daysReqs;                   //所有的请求

vector<Server> svResources;                         //购买的服务器
#define DeployInfo vector<int>                      //虚拟机的部署信息。{服务器编号, 占用cpu, 占用mem, maybe 部署节点}。长度为3->双节点部署;当长度为4->单节点部署(0-A,1->B)
unordered_map<string, DeployInfo> vmDeployInfos;    //记录虚拟机运行在哪台服务器上(key=虚拟机id(不是类型), value={服务器编号,占用cpu,占用mem,部署节点})
vector<vector<string>> svRunVms;                    //记录已购的每台服务器上的各自运行的虚拟机Id。长度和已购服务器的数量对应
vector<int> svRunVmsNumber;                         //记录已购的每台服务器上的各自运行的虚拟机数量(看似显得多余,其实是为了方便后续的处理)。长度和已购服务器的数量对应
vector<vector<string>> purchase_info;//每天的购买信息
vector<vector<string>> migrate_info;//每天的迁移信息
vector<vector<string>> deploy_info;//每天的部署信息
//提交时可能用不到的变量
# define INPUT_REDIRECTION_1 "training-data/training-1.txt"  //输入重定向
# define INPUT_REDIRECTION_2 "training-data/training-2.txt"  //输入重定向
# define OUTPUT_REDIRECTION "result.txt"                     //输出重定向
long long SERVERCOST=0, POWERCOST=0, TOTALCOST=0;            //各种成本
int MIGRATE_NUMBER=0, TOTAL_MIGRATE=0;                       //迁移数量
long long SC=0, PC=0, TC=0;                                  //两份数据的成本
/************************************************/

/****************** 读数据构建各种类型的 服务器 和 虚拟机 ********************/
void readServer(string &serverType, string &cpuCore, string &memSize, string &serverCost, string &powerCost){
	string _serverType = "";
    for(int i =1; i<serverType.size()-1; i++) _serverType += serverType[i];
    int _cpuCore=0, _memSize=0, _serverCost=0, _powerCost=0;
    for(int i=0; i<cpuCore.size()-1; i++) _cpuCore = 10*_cpuCore + cpuCore[i] - '0';
    for(int i=0; i<memSize.size()-1; i++) _memSize = 10*_memSize + memSize[i] - '0';
    for(int i=0; i<serverCost.size()-1; i++) _serverCost = 10*_serverCost + serverCost[i] - '0';
    for(int i=0; i<powerCost.size()-1; i++) _powerCost = 10*_powerCost + powerCost[i] - '0';
	svInfos[_serverType] = Server{_serverType, _cpuCore, _memSize, _serverCost, _powerCost};
}
void readVm(string &vmType, string &cpuCore, string &memSize, string &isDual){
    string _vmType = "";
    for(int i=1; i<vmType.size()-1; i++) _vmType += vmType[i];
    int _cpuCore=0, _memSize=0, _isDual=0;
    for(int i=0; i<cpuCore.size()-1; i++) _cpuCore = _cpuCore*10 + cpuCore[i] - '0';
    for(int i=0; i<memSize.size()-1; i++) _memSize = _memSize*10 + memSize[i] - '0';
    if(isDual[0] == '1') _isDual = 1;
    vmInfos[_vmType] = VirtualMachine{_vmType, _cpuCore, _memSize, _isDual};
}
void readServerAndVmInfos(){
	int svNum;//服务器数量
	string type, cpuCore, memSize, serverCost, powerCost, isDual;
	scanf("%d", &svNum);
	for (int i=0; i<svNum; i++){
		cin>>type>>cpuCore>>memSize>>serverCost>>powerCost;
		readServer(type, cpuCore, memSize, serverCost, powerCost);
	}
	int vmNum;//虚拟机数量
    scanf("%d",&vmNum);
	for (int i =0; i<vmNum; i++){
        cin>>type>>cpuCore>>memSize>>isDual;
        readVm(type, cpuCore, memSize, isDual);
    }
    //复制一份排序。
    for(auto const &pair:svInfos){
		svCostServers.emplace_back(pair.second);
		pwCostServers.emplace_back(pair.second);
	}
	//硬件价格排序
	sort(svCostServers.begin(), svCostServers.end(), [](Server const &s1, Server const s2){
		if(s1.serverCost == s2.serverCost) return s1.powerCost < s2.powerCost;
		return s1.serverCost < s2.serverCost;
	});
	//每日能耗排序
	sort(pwCostServers.begin(), pwCostServers.end(), [](Server const &s1, Server const s2){
		if(s1.powerCost== s2.powerCost) return s1.serverCost < s2.serverCost;
		return s1.powerCost < s2.powerCost;
	});
}
/*****************************************************************************/

/************** 读数据构建每日请求 ****************/
void readAddRequest(int day, string &op, string & reqVmType, string &reqVmId){
	string _op, _reqVmType, _reqVmId;
    _op = op.substr(1, op.size()-2);
    _reqVmType = reqVmType.substr(0, reqVmType.size()-1);
    _reqVmId = reqVmId.substr(0, reqVmId.size() -1);
    VirtualMachine vm = vmInfos[_reqVmType];
    daysReqs[day].emplace_back(Request{_op, _reqVmType, _reqVmId, vm.cpuCore, vm.memSize, vm.isDual});
}
void readDelRequest(int day, string &op, string &reqVmId){
	string _op, _reqVmId;
    _op = op.substr(1, op.size()-2);
    _reqVmId = reqVmId.substr(0, reqVmId.size()-1);
    daysReqs[day].emplace_back(Request{_op, _reqVmId});
}
void readDayRequests(int day, int dayReqNumber){
	string op, reqVmType, reqVmId;
	daysReqs.emplace_back(vector<Request>{});
	for (int i=0; i<dayReqNumber; i++){
		cin>>op;
		if(op[1] == 'a'){
			cin>>reqVmType>>reqVmId;
			readAddRequest(day, op, reqVmType, reqVmId);;
		}else{
			cin>>reqVmId;
			readDelRequest(day, op, reqVmId);
		}
	}
}
/**************************************************/

/*************** 为剩余的虚拟机请求购买服务器并部署 *******/
//寻找合适的服务器购买
int buyBestServer(vector<Server> const &servers, Request const &req){
	int needCpu = req.isDual ? req.vmCpu/2 : req.vmCpu;
	int needMem = req.isDual ? req.vmMem/2 : req.vmMem;
	//单双节点都是统一的判断方式,很神奇,是吧
	for(int i=0; i<servers.size(); i++){
		Server const &sv=servers[i];
		if(sv.cpuA>1.32*needCpu && sv.memA>1.32*needMem) return i;
	}
	for(int i=0; i<servers.size(); i++){
		Server const &sv=servers[i];
		if(sv.cpuA>1.2*needCpu && sv.memA>1.2*needMem) return i;
	}
	for(int i=0; i<servers.size(); i++){
		Server const &sv=servers[i];
		if(sv.cpuA>=needCpu && sv.memA>=needMem) return i;
	}
	return -1;//这个地方永远也执行不了
}
int findBestServer(vector<Server> const &servers, Request const &req, int start=0){
	int index = -1;
	int minRest = ~(1<<(sizeof(int)*8 - 1));
	int needCpu = req.isDual ? req.vmCpu/2 : req.vmCpu;
	int	needMem = req.isDual ? req.vmMem/2 : req.vmMem;
	int rest = 0;
	if(req.isDual){//双节点
		for(int i=start; i<servers.size(); i++){
			Server const &sv = servers[i];
			if(sv.cpuA>=needCpu && sv.cpuB>=needCpu && sv.memA>=needMem && sv.memB>=needMem){
				//float ratio1 = (float)(sv.cpuA-needCpu)/(sv.memA-needMem);
				//if(ratio1<0.13 || ratio1>7.5) continue;//保证A节点都不失衡
				float ratio2 = (float)(sv.cpuB-needCpu)/(sv.memB-needMem);
				if(ratio2<0.13 || ratio2>7.5) continue;//保证B节点都不失衡
				rest = sv.cpuA+sv.cpuB+sv.memA+sv.memB - req.vmCpu - req.vmMem;//放入后的剩余资源量 cpu + mem
				if(rest < minRest){
					index = i;
					minRest=rest;
				}
			}
		}
	}else{//单节点
		for(int i=start; i<servers.size(); i++){
			Server const &sv = servers[i];
			if((sv.cpuA>=needCpu && sv.memA>=needMem) || (sv.cpuB>=needCpu && sv.memB>=needMem)){
				//float ratio1 = (float)(sv.cpuA-needCpu)/(sv.memA-needMem);
				//if(ratio1<0.13 || ratio1>7.5) continue;//保证A节点都不失衡
				float ratio2 = (float)(sv.cpuB-needCpu)/(sv.memB-needMem);
				if(ratio2<0.13 || ratio2>7.5) continue;//保证B节点都不失衡
				rest = sv.cpuA+sv.cpuB+sv.memA+sv.memB - req.vmCpu - req.vmMem;//放入后的剩余资源量 cpu + mem
				if(rest < minRest){
					index = i;
					minRest=rest;
				}
			}
		}
	}
	return index;
}
//为一波请求买服务器,并同时部署。这波请求可能含有del
void buyServersForReqs(int day, vector<Request> const &reqs){
	int old_sv_number = svResources.size();//新买的服务器从这里开始编号
	unordered_map<string, DeployInfo> temp_dep_infos;//临时的部署信息,最后把这个信息更新到全局部署信息中。这里主要是为了编号映射,太麻烦了

	for(Request const &req:reqs){
		string vmId = req.vmId;
		if(req.type=="add"){//添加请求
			int isDual = req.isDual;
			int needCpu = isDual ? req.vmCpu/2 : req.vmCpu;
			int needMem = isDual ? req.vmMem/2 : req.vmMem;
			int svId = findBestServer(svResources, req, old_sv_number);
			if (svId < 0){//需购买
				int index;
				Server sv;
				if(day <= 333){//333
					index = buyBestServer(pwCostServers, req);//按照每日能耗最低,买一台最合适的服务器
					sv = pwCostServers[index];//购买
				}else{
					index = buyBestServer(svCostServers, req);//按照硬件成本最低,买一台最合适的服务器
					sv = svCostServers[index];//购买
				}
				SERVERCOST += sv.serverCost;
				svId = svResources.size();//这台新购买的服务器编号
				if(isDual){//双节点
					sv.cpuA -= needCpu;
					sv.cpuB -= needCpu;
					sv.memA -= needMem;
					sv.memB -= needMem;
					temp_dep_infos[vmId]=DeployInfo{svId, needCpu, needMem};
				}else{//单节点,默认放在新买服务器的B节点上
					sv.cpuB -= needCpu;
					sv.memB -= needMem;
					temp_dep_infos[vmId]=DeployInfo{svId, needCpu, needMem, 1};
				}
				svResources.emplace_back(sv);
				svRunVmsNumber.emplace_back(1);
				svRunVms.emplace_back(vector<string>{vmId});
			}else{//无需购买即可部署
				Server &sv = svResources[svId];//涉及到修改资源量了
				if(isDual){//双节点部署
					sv.cpuA -= needCpu;
					sv.cpuB -= needCpu;
					sv.memA -= needMem;
					sv.memB -= needMem;
					temp_dep_infos[vmId] = DeployInfo{svId, needCpu, needMem};
					svRunVmsNumber[svId]++;
					svRunVms[svId].emplace_back(vmId);
				}else{//单节点部署
					if(sv.cpuA <= sv.cpuB){//A节点剩得少
						if(sv.cpuA>=needCpu && sv.memA>=needMem){//并且A节点能放下
							sv.cpuA -= needCpu;
							sv.memA -= needMem;
							temp_dep_infos[vmId] = DeployInfo{svId, needCpu, needMem, 0};
							svRunVmsNumber[svId]++;
							svRunVms[svId].emplace_back(vmId);
							continue;
						}
						if(sv.cpuB>=needCpu && sv.memB>=needMem){//A节点剩得少,但是A节点放不下
							sv.cpuB -= needCpu;
							sv.memB -= needMem;
							temp_dep_infos[vmId] = DeployInfo{svId, needCpu, needMem, 1};
							svRunVmsNumber[svId]++;
							svRunVms[svId].emplace_back(vmId);
							continue;
						}
					}else{//B节点剩得少
						if(sv.cpuB>=needCpu && sv.memB>=needMem){//并且B节点能放下
							sv.cpuB -= needCpu;
							sv.memB -= needMem;
							temp_dep_infos[vmId] = DeployInfo{svId, needCpu, needMem, 1};
							svRunVmsNumber[svId]++;
							svRunVms[svId].emplace_back(vmId);
							continue;
						}
						if(sv.cpuA>=needCpu && sv.memA>=needMem){//B节点剩得少,但是B节点放不下
							sv.cpuA -= needCpu;
							sv.memA -= needMem;
							temp_dep_infos[vmId] = DeployInfo{svId, needCpu, needMem, 0};
							svRunVmsNumber[svId]++;
							svRunVms[svId].emplace_back(vmId);
							continue;
						}
					}
				}
			}
		}else{//删除请求
			DeployInfo vmDeInfo = temp_dep_infos[vmId];
			int svId=vmDeInfo[0], occupiedCpu=vmDeInfo[1], occupiedMem=vmDeInfo[2];//获取部署时的关键信息
			Server &sv=svResources[svId];//涉及到修改资源量了
			if(vmDeInfo.size() == 3){//之前是双节点部署的
				sv.cpuA += occupiedCpu;
				sv.cpuB += occupiedCpu;
				sv.memA += occupiedMem;
				sv.memB += occupiedMem;
			}else{//之前是单节点部署的
				if(vmDeInfo[3] == 0){//之前是部署在A节点上
					sv.cpuA += occupiedCpu;
					sv.memA += occupiedMem;
				}else{
					sv.cpuB += occupiedCpu;
					sv.memB += occupiedMem;
				}
			}
			//temp_dep_infos.erase(vmId);//暂时不删,不然做编号映射时会出问题。最后统一删除部署信息
			svRunVmsNumber[svId]--;
			vector<string> &VMIDS=svRunVms[svId];
			VMIDS.erase(remove(VMIDS.begin(), VMIDS.end(), vmId), VMIDS.end());//没错删除就是这么复杂
		}
	}

	vector<string> day_buy_info{""};//保存购买信息
	set<string> diffSvType;//这个东西可以记录买了多少种
	//为了编号映射,好球麻烦哟
	unordered_map<int, Server> server_map;
	unordered_map<int, vector<string>> sv_run_vms_map;
	unordered_map<int, int> svRunVmsNumber_map;
	unordered_map<int, int> svId_map;
	int ID = old_sv_number;
	for(int i=old_sv_number; i<svResources.size(); i++){
		string svType= svResources[i].type;
		if(diffSvType.find(svType)!=diffSvType.end()) continue;
		int number=1;//每种买了多少
		diffSvType.insert(svType);
		server_map[ID]=svResources[i];
		svRunVmsNumber_map[ID]=svRunVmsNumber[i];
		sv_run_vms_map[ID]=svRunVms[i];
		svId_map[i]=ID++;
		for(int j=i+1; j<svResources.size(); j++){
			if(svResources[j].type==svType){
				server_map[ID]=svResources[j];
				svRunVmsNumber_map[ID]=svRunVmsNumber[j];
				sv_run_vms_map[ID]=svRunVms[j];
				svId_map[j]=ID++;
				number++;
			}
		}
		day_buy_info.emplace_back("(" + svType + ", " + to_string(number) + ")\n");
	}
	//存一下每日的购买信息
	day_buy_info[0] = "(purchase, " + to_string(diffSvType.size()) + ")\n";//将购买种类存到数组的首位
	purchase_info.emplace_back(day_buy_info);//存一下每日的购买请求

	//根据映射,为svResources和svRunVmsNumber和svRunVms中新增的部分重新赋值。是不是感觉很麻烦,是的
	for(int i=old_sv_number; i<svResources.size(); i++){
		svResources[i]=server_map[i];
		svRunVmsNumber[i]=svRunVmsNumber_map[i];
		svRunVms[i]=sv_run_vms_map[i];
	}

	//使用 svId_map 映射去修改部署信息。并更新到全局部署信息中
	for(auto &pair : temp_dep_infos){
		string vmId = pair.first;
		DeployInfo depInfo = pair.second;
		depInfo[0] = svId_map[depInfo[0]];//修改部署到的服务器 Id
		vmDeployInfos[vmId] = depInfo;//更新到全局变量
	}
}
/*********************************************************/

/*************************** 复杂的迁移策略 ***************************************/
vector<int> findBestServerForMigrate(vector<int> const &svIds, DeployInfo const &vmDepInfo, int start=0){//迁移时,虚拟机应该放在哪台服务器上比较好。返回所在svIds中的索引,找不到就返回-1。并且可以指定从哪个位置开始往后找
	int index = -1, node = -1;
	int minRest = ~(1<<(sizeof(int)*8 - 1));
	int isDual = vmDepInfo.size()==3 ? 1 : 0;
	int needCpu=vmDepInfo[1], needMem=vmDepInfo[2];
	int rest = 0;
	if(isDual){//之前是双节点部署的
		for(int i=start; i<svIds.size(); i++){
			if(svIds[i]==vmDepInfo[0]) continue;//双部署虚拟机不能迁移到本身所在的服务器上
			Server const &sv = svResources[svIds[i]];
			if(sv.rest < 2*(needCpu+needMem)) continue;
			if(sv.cpuA>=needCpu && sv.cpuB>=needCpu && sv.memA>=needMem && sv.memB>=needMem){
				rest = sv.rest - 2*(needCpu+needMem);//放入后的剩余资源量 cpu + mem
				if(rest<minRest){
					index = i;
					minRest=rest;
				}
			}
		}
	}else{//之前是单节点部署的
		for(int i=start; i<svIds.size(); i++){
			Server const &sv = svResources[svIds[i]];
			if(sv.cpuA>=needCpu && sv.memA>=needMem){
				//if(svIds[i]==vmDepInfo[0] && vmDepInfo[3]==0) continue;//单部署虚拟机不能迁移到本身所在的服务器的相同节点上
				rest = sv.cpuA+sv.memA - needCpu - needMem;
				if(rest < minRest){//保证部署后的剩余资源量最小
					index = i;
					node = 0;
					minRest=rest;
				}
			}
			if(sv.cpuB>=needCpu && sv.memB>=needMem){
				//if(svIds[i]==vmDepInfo[0] && vmDepInfo[3]==1) continue;//单部署虚拟机不能迁移到本身所在的服务器的相同节点上
				rest = sv.cpuB+sv.memB - needCpu - needMem;
				if(rest < minRest){//保证部署后的剩余资源量最小
					index = i;
					node = 1;
					minRest=rest;
				}
			}
		}
	}
	if(isDual) return vector<int>{index};//返回可迁移到的服务器索引。不可能返回原服务器。
	else return vector<int>{index, node};//返回可迁移到的服务器索引,以及迁移到的节点。不可能返回原服务器和相同节点
}
void migrate(int day){
	vector<string> day_migrate_info{""};//用来存每日的迁移信息。先加入一个空字符串留给 migrate_num
	int max_migrate_num = (3*vmDeployInfos.size())/100;//当日最大可迁移数量,不超过 max_migrate_num
	int migrate_num = 0;//当日已迁移数量

	vector<int> svIds;//收集服务器的Id,并计算每台服务器上的剩余资源量
	for(int i=0; i<svResources.size(); i++) {
		Server &sv = svResources[i];
		sv.rest = sv.cpuA + sv.cpuB + sv.memA + sv.memB;//计算服务器上的剩余资源量	
		svIds.emplace_back(i);
	}

	//按服务器上的剩余资源量排序,第二序为其上运行的虚拟机个数。前提是服务器的rest属性值已被更新过,不然会出错
	sort(svIds.begin(), svIds.end(), [&](int const &id1, int const &id2){
		Server const &sv1 = svResources[id1];
		Server const &sv2 = svResources[id2];
		if(sv1.rest == sv2.rest){
			return svRunVmsNumber[id1] < svRunVmsNumber[id2];
		}
		return sv1.rest > sv2.rest;
	});

	vector<int> canMigSvIds;//能够转移到的服务器
	set<string> migratedVmIds;//已迁移过的虚拟机
	set<string> delVmIds;//当天即将删除的虚拟机
	for(Request const &req : daysReqs[day]) if(req.type=="del") delVmIds.emplace(req.vmId);

	//迁移时会动态更新服务器上的剩余资源量,即rest属性的值
	for(int i=0; i<svIds.size(); i++){//从前往后遍历
		int svId = svIds[i];
		Server &sv = svResources[svId];
		if((float)(sv.rest)/(sv.cpuCore+sv.memSize) < 0.018) continue;//如果这台服务器上的资源很满,那其上的虚拟机就不考虑迁移了
		vector<string> vmIds = svRunVms[svId];//服务器上的虚拟机们

		//按已部署虚拟机占用的资源量排序
		sort(vmIds.begin(), vmIds.end(), [&](string const &vm1, string const &vm2){
			DeployInfo const &dep1 = vmDeployInfos[vm1];
			DeployInfo const &dep2 = vmDeployInfos[vm2];
			if(dep1[1]+dep1[2] == dep2[1]+dep2[2]){
				return dep1[1] > dep2[1];
			}
			return dep1[1]+dep1[2] > dep2[1]+dep2[2];
		});

		for(string const &vmId : vmIds){//遍历每台虚拟机
			if(migrate_num == max_migrate_num) goto STOP_MIGRATE;//达到最大迁移次数
			if(delVmIds.find(vmId)!=delVmIds.end()) break;//这台虚拟机即将被删除,就不再迁移了
			if(migratedVmIds.find(vmId)!=migratedVmIds.end()) break;//这台虚拟机之前迁移过,就不再迁移了
			DeployInfo &deInfo = vmDeployInfos[vmId];//虚拟机部署信息
			int dual = deInfo.size()==3 ? 1 : 0;
			int occupiedCpu=deInfo[1], occupiedMem=deInfo[2];

			vector<int> ans = findBestServerForMigrate(svIds, deInfo, i+1);//找到最合适迁移到的服务器
			int index=ans[0], goal_svId;
			if(index < 0) {//表示找不到能放得下的可以迁移到的服务器
				ans = findBestServerForMigrate(canMigSvIds, deInfo, 0);//继续在canMigSvIds里面找
				index = ans[0];
				if(index < 0 || canMigSvIds[index]==svId){//暂时考虑完全避免往自己上面迁移
					if (canMigSvIds.size()==0 || canMigSvIds[canMigSvIds.size()-1]!=svId) canMigSvIds.emplace_back(svId);
					continue;
				}else{
					goal_svId = canMigSvIds[index];//目标服务器Id
				}
			}else{
				goal_svId = svIds[index];//目标服务器Id
			}
			migratedVmIds.emplace(vmId);//记录该虚拟机已迁移过
			Server &goal_sv = svResources[goal_svId];//目标服务器,一旦找到就表示肯定能放下
			if(dual){//双节点部署
				sv.cpuA += occupiedCpu;//原服务器资源增加
				sv.cpuB += occupiedCpu;
				sv.memA += occupiedMem;
				sv.memB += occupiedMem;
				svRunVmsNumber[svId]--;
				sv.rest = sv.cpuA+sv.cpuB+sv.memA+sv.memB;//更新剩余资源量
				vector<string> &VMIDS=svRunVms[svId];
				VMIDS.erase(remove(VMIDS.begin(), VMIDS.end(), vmId), VMIDS.end());//没错删除就是这么复杂
				goal_sv.cpuA -= occupiedCpu;//目标服务器资源减少
				goal_sv.cpuB -= occupiedCpu;
				goal_sv.memA -= occupiedMem;
				goal_sv.memB -= occupiedMem;
				goal_sv.rest = goal_sv.cpuA+goal_sv.cpuB+goal_sv.memA+goal_sv.memB;//更新剩余资源量
				svRunVmsNumber[goal_svId]++;
				svRunVms[goal_svId].emplace_back(vmId);
				//修改部署信息
				deInfo[0] = goal_svId;
				migrate_num++;//当日已迁移数量 + 1
				day_migrate_info.emplace_back("(" + vmId + ", " + to_string(goal_svId) + ")\n");
			}else{//单节点部署
				if(deInfo[3]==0){//原来部署在A节点还是B节点
					sv.cpuA += occupiedCpu;
					sv.memA += occupiedMem;
				}else{
					sv.cpuB += occupiedCpu;
					sv.memB += occupiedMem;
				}
				svRunVmsNumber[svId]--;
				sv.rest = sv.cpuA+sv.cpuB+sv.memA+sv.memB;//更新剩余资源量
				vector<string> &VMIDS=svRunVms[svId];
				VMIDS.erase(remove(VMIDS.begin(), VMIDS.end(), vmId), VMIDS.end());//没错删除就是这么复杂
				int node=ans[1];//现在可迁移到的节点
				if(node==0){
					goal_sv.cpuA -= occupiedCpu;
					goal_sv.memA -= occupiedMem;
					svRunVmsNumber[goal_svId]++;
					svRunVms[goal_svId].emplace_back(vmId);
					goal_sv.rest = goal_sv.cpuA+goal_sv.cpuB+goal_sv.memA+goal_sv.memB;//更新剩余资源量
					//修改部署信息
					deInfo[0] = goal_svId;
					deInfo[3] = 0;
					migrate_num++;//当日已迁移数量 + 1
					day_migrate_info.emplace_back("(" + vmId + ", " + to_string(goal_svId) + ", A)\n");
				}else{
					goal_sv.cpuB -= occupiedCpu;
					goal_sv.memB -= occupiedMem;
					svRunVmsNumber[goal_svId]++;
					svRunVms[goal_svId].emplace_back(vmId);
					goal_sv.rest = goal_sv.cpuA+goal_sv.cpuB+goal_sv.memA+goal_sv.memB;//更新剩余资源量
					//修改部署信息
					deInfo[0] = goal_svId;
					deInfo[3] = 1;
					migrate_num++;//当日已迁移数量 + 1
					day_migrate_info.emplace_back("(" + vmId + ", " + to_string(goal_svId) + ", B)\n");
				}
			}
		}
	}

STOP_MIGRATE:
//	if(day==0 || day%10==0){
//		cout<<day<<" --> "<<max_migrate_num<<" --> "<<migrate_num<<endl;
//	}
	day_migrate_info[0] = "(migration, " + to_string(migrate_num) + ")\n";//将migrate_num转化成字符串放到数组的首位
	MIGRATE_NUMBER += migrate_num;
	migrate_info.emplace_back(day_migrate_info);//存一下每日的迁移信息
}
/*********************************************************************************/

/********************* 虚拟机请求资源(vmCpu + vmMem)排序 *********************/
void quickSort_cpumem_down(vector<Request> &reqs, int l, int r){
	if (l < r){
		int i=l, j=r;
		Request req=reqs[l];
		while(i < j){
			while(reqs[j].vmCpu+reqs[j].vmMem < req.vmCpu+req.vmMem && i<j) j--;
			if(i < j) reqs[i++] = reqs[j];
			while(reqs[i].vmCpu+reqs[i].vmMem >= req.vmCpu+req.vmMem && i<j) i++;
			if(i < j) reqs[j--] = reqs[i];
		}
		reqs[i] = req;
		quickSort_cpumem_down(reqs, l, i-1);
		quickSort_cpumem_down(reqs, i+1, r);
	}
}
void segmentSort(vector<Request> &reqs){//以删除请求分段排序,为的是不打乱删除的顺序
	int start=0;
	for(int i=0; i<reqs.size(); i++){
		if(reqs[i].type=="del"){
			vector<Request> speReqs,dualReqs,sgleReqs;
			for(int j=start; j<i; j++){
				float ratio = (float)reqs[j].vmCpu/reqs[j].vmMem;
				if(ratio<=0.28 || ratio>=6.8){
					speReqs.emplace_back(reqs[j]);
					continue;
				}
//				if(ratio<0.28 || ratio>6.8){
//					speReqs.emplace_back(reqs[j]);
//					continue;
//				}
				if(reqs[j].isDual==1) dualReqs.emplace_back(reqs[j]);
				else sgleReqs.emplace_back(reqs[j]);
			}
			quickSort_cpumem_down(speReqs, 0, speReqs.size()-1);
			quickSort_cpumem_down(dualReqs, 0, dualReqs.size()-1);
			quickSort_cpumem_down(sgleReqs, 0, sgleReqs.size()-1);
			for(Request const &req:sgleReqs) reqs[start++]=req;
			for(Request const &req:dualReqs) reqs[start++]=req;
			for(Request const &req:speReqs) reqs[start++]=req;
			//quickSort_cpumem_down(reqs, start, i-1);
			start=i+1;
		}
	}
	//之前就是忘了这一步
	quickSort_cpumem_down(reqs, start, reqs.size()-1);
}
/*********************************************************************************/

//直接部署时。
vector<int> findBestServer(vector<int> const &svIds, Request const &req, int start=0){
	int index = -1, node = -1;
	int needCpu = req.isDual ? req.vmCpu/2 : req.vmCpu;
	int	needMem = req.isDual ? req.vmMem/2 : req.vmMem;
	int minRest = ~(1<<(sizeof(int)*8 - 1));
	int rest = 0;
	if(req.isDual){//双节点
		for(int i=start; i<svIds.size(); i++){
			Server const &sv = svResources[svIds[i]];
			if(sv.cpuA>=needCpu && sv.cpuB>=needCpu && sv.memA>=needMem && sv.memB>=needMem){
				float ratio1 = (float)(sv.cpuA-needCpu)/(sv.memA-needMem);
				float ratio2 = (float)(sv.cpuB-needCpu)/(sv.memB-needMem);
				if(ratio1<0.13 || ratio1>7.5) continue;//保证A节点不失衡
				if(ratio2<0.13 || ratio2>7.5) continue;//保证B节点不失衡
				rest = sv.cpuA+sv.cpuB+sv.memA+sv.memB - req.vmCpu - req.vmMem;
				if(rest < minRest){//保证部署后的剩余资源量最小
					index = i;
					minRest=rest;
				}
			}
		}
	}else{//单节点
		for(int i=start; i<svIds.size(); i++){
			Server const &sv = svResources[svIds[i]];
			if(sv.cpuA>=needCpu && sv.memA>=needMem){
				float ratio1 = (float)(sv.cpuA-needCpu)/(sv.memA-needMem);
				if(ratio1<0.13 || ratio1>7.5) continue;//保证在A节点上部署后不失衡
				rest = sv.cpuA - req.vmCpu;//只考虑cpu的剩余量效果竟然要好一些
				if(rest < minRest){//保证部署后的剩余资源量最小
					index = i;
					node = 0;
					minRest=rest;
				}
			}
			if(sv.cpuB>=needCpu && sv.memB>=needMem){
				float ratio2 = (float)(sv.cpuB-needCpu)/(sv.memB-needMem);
				if(ratio2<0.13 || ratio2>7.5) continue;//保证在B节点上部署后不失衡
				rest = sv.cpuB - req.vmCpu;
				if(rest < minRest){//保证部署后的剩余资源量最小
					index = i;
					node = 1;
					minRest=rest;
				}
			}
		}
	}
	if(req.isDual) return vector<int>{index};//返回找到的服务器索引
	else return vector<int>{index, node};//返回找到的服务器索引,以及部署节点
}

//处理每日请求
void handleOneDayRequests(int day){
	vector<Request> sortedReqs = daysReqs[day];//复制一份数据,保持原数据不变
	segmentSort(sortedReqs);//分段排序

	migrate(day);//迁移

	//分下类
	vector<int> emptySvIds;//空服务器Id
	vector<int> notEmptySvIds;//非空服务器Id
	for(int i=0; i<svResources.size();  i++){
		if(svRunVmsNumber[i] != 0) notEmptySvIds.emplace_back(i);
		else emptySvIds.emplace_back(i);
	}

	//能放的就放,放不下的就记录下来,稍后购买。(该记录中可能含有del,主要是可能有该天添加,该天删除的虚拟机)
	vector<Request> cantHoldReqs;
	for(Request const &req:sortedReqs){
		string vmId=req.vmId;
		if(req.type=="add"){//添加请求
			vector<int> ans = findBestServer(notEmptySvIds, req);//先在非空服务器上查找
			int svId, index=ans[0];
			if(index < 0){
				ans = findBestServer(emptySvIds, req);//再在空服务器上查找
				index = ans[0];
				if(index < 0) {cantHoldReqs.emplace_back(req);continue;}//实在是放不下时,记录下该请求
				svId = emptySvIds[index];//服务器Id
				//将该服务器Id从空移到非空
				emptySvIds.erase(remove(emptySvIds.begin(), emptySvIds.end(), svId), emptySvIds.end());
				notEmptySvIds.emplace_back(svId);
			}else{
				svId = notEmptySvIds[index];//服务器Id
			}
			int isDual = req.isDual;
			int needCpu = isDual ? req.vmCpu/2 : req.vmCpu;
			int needMem = isDual ? req.vmMem/2 : req.vmMem;
			Server &sv = svResources[svId];//涉及到修改资源量了
			if(isDual){//双节点部署
				sv.cpuA -= needCpu;
				sv.cpuB -= needCpu;
				sv.memA -= needMem;
				sv.memB -= needMem;
				vmDeployInfos[vmId] = DeployInfo{svId, needCpu, needMem};
				svRunVmsNumber[svId]++;
				svRunVms[svId].emplace_back(vmId);
			}else{//单节点部署
				int node = ans[1];//在返回的结果中看看它该部署到哪个节点合适
				if(node==0){
					sv.cpuA -= needCpu;
					sv.memA -= needMem;
					vmDeployInfos[vmId] = DeployInfo{svId, needCpu, needMem, 0};
					svRunVmsNumber[svId]++;
					svRunVms[svId].emplace_back(vmId);
				}else{
					sv.cpuB -= needCpu;
					sv.memB -= needMem;
					vmDeployInfos[vmId] = DeployInfo{svId, needCpu, needMem, 1};
					svRunVmsNumber[svId]++;
					svRunVms[svId].emplace_back(vmId);
				}
			}
		}else{//删除请求
			if(vmDeployInfos.find(vmId) == vmDeployInfos.end()){//该虚拟机是本应该当天部署的,但是没部署上,被存起来了
				cantHoldReqs.emplace_back(req);//那么当天对该虚拟机的删除也需要存起来。保持相对处理位置不变
				continue;
			}
			DeployInfo vmDeInfo = vmDeployInfos[vmId];
			int svId=vmDeInfo[0], occupiedCpu=vmDeInfo[1], occupiedMem=vmDeInfo[2];//获取部署时的关键信息
			Server &sv=svResources[svId];//涉及到修改资源量了
			if(vmDeInfo.size() == 3){//之前是双节点部署的
				sv.cpuA += occupiedCpu;
				sv.cpuB += occupiedCpu;
				sv.memA += occupiedMem;
				sv.memB += occupiedMem;
			}else{//之前是单节点部署的
				if(vmDeInfo[3] == 0){//之前是部署在A节点上
					sv.cpuA += occupiedCpu;
					sv.memA += occupiedMem;
				}else{
					sv.cpuB += occupiedCpu;
					sv.memB += occupiedMem;
				}
			}
			//vmDeployInfos.erase(vmId);//暂时不删,不然做编号映射时会出问题。最后统一删除部署信息
			svRunVmsNumber[svId]--;
			vector<string> &VMIDS=svRunVms[svId];
			VMIDS.erase(remove(VMIDS.begin(), VMIDS.end(), vmId), VMIDS.end());//没错删除就是这么复杂
			if(svRunVmsNumber[svId]==0){//如果该服务器为空了,将该服务器Id从非空移到空
				notEmptySvIds.erase(remove(notEmptySvIds.begin(), notEmptySvIds.end(), svId), notEmptySvIds.end());
				emptySvIds.emplace_back(svId);
			}
		}
	}

	//考虑处理前面放不下的请求。下面的操作会改变 svResources 的大小,也就是会新买一批服务器紧接着放在后面
	buyServersForReqs(day, cantHoldReqs);//买服务器,放请求,并做好映射,更新全局部署信息

	//保存当日的部署信息
	vector<string> day_deploy_info;
	for(Request const &req : daysReqs[day]){//顺序重新遍历当日请求
		if(req.type=="add"){//添加请求
			string vmId=req.vmId;
			DeployInfo depInfo = vmDeployInfos[vmId];//得到部署信息
			string svId = to_string(depInfo[0]);
			if(depInfo.size()==3){
				day_deploy_info.emplace_back("(" + svId + ")\n");
			}else{
				string nodeInfo = depInfo[3]==0 ? "A" : "B";
				day_deploy_info.emplace_back("(" + svId + ", " + nodeInfo + ")\n");
			}
		}else{//删除请求没有任何输出
			//但是在这里统一删除,需要删除的部署信息。我简直是太机智了。如果在前面就删掉了某台虚拟机的部署信息,那么在这里遍历时就可能找不到它的部署信息了,那我怎么输出日志嘛
			vmDeployInfos.erase(req.vmId);
		}
	}
	deploy_info.emplace_back(day_deploy_info);//存一下每日的部署请求
}

//计算每日的耗电成本
void computePowerCost(){
	for(int i=0; i<svRunVmsNumber.size(); i++){
        if(svRunVmsNumber[i] > 0){
            POWERCOST += svResources[i].powerCost;
        }
    }
}

//打印输出结果
void print(int startDay, int endDay){
	for(int day=startDay; day<endDay+1; day++){//遍历[startDay, endDay]
		for(string &info:purchase_info[day]){//每天购买
			cout<<info;
		}
		for(string &info:migrate_info[day]){//每天迁移
			cout<<info;
		}
		for(string &info:deploy_info[day]){//每天部署
			cout<<info;
		}
	}
}

void cloudResourceSchedulingAlgorithm(){
	readServerAndVmInfos();//读取服务器和虚拟机信息
	int dayReqNumber=0;
	scanf("%d", &reqDays);//请求有多少天
	scanf("%d", &windowDays);//天数窗口,可以理解为每天处理请求时,只知道该窗口内的后续请求

	//先读入 windowDays 天的请求
	for(int day=0; day<windowDays; day++){
		scanf("%d", &dayReqNumber);
		readDayRequests(day, dayReqNumber);
	}

	//再一边处理现有请求,一边输出决策,一边读入后续请求数据。复赛要求这么做
	for (int day=0; day<(reqDays - windowDays); day++){
		//处理现有请求
		handleOneDayRequests(day);

		computePowerCost();
		if(day==0 || day%50==0) printf("Finished requests of day : %d\n", day);

		//输出当天决策,刷新输出缓存区,相应赛题的要求
		//print(day, day);
		//fflush(stdout);

		//读入后续请求
		scanf("%d", &dayReqNumber);
		readDayRequests(windowDays+day, dayReqNumber);
	}

	//处理剩余天的请求
	for(int day=reqDays-windowDays; day<reqDays; day++){
		handleOneDayRequests(day);
		computePowerCost();
		if(day==0 || day%50==0) printf("Finished requests of day : %d\n", day);
	}
	//输出剩余天的决策
	//print(reqDays-windowDays, reqDays-1);

}

int main(){
//	freopen(OUTPUT_REDIRECTION, "w", stdout);
	clock_t start, finish;

	//第一份文件
	start = clock();
	freopen(INPUT_REDIRECTION_1, "r", stdin);
	cloudResourceSchedulingAlgorithm();
	finish = clock();
	TOTALCOST = SERVERCOST + POWERCOST;//总成本
	SC += SERVERCOST, PC += POWERCOST, TC += TOTALCOST;
	TOTAL_MIGRATE += MIGRATE_NUMBER;
	printf("\nCompute Time: %f s \n", double(finish-start)/CLOCKS_PER_SEC);
	printf("Server Cost: %lld \nPower Cost: %lld \nTotal Cost: %lld \n", SERVERCOST, POWERCOST, TOTALCOST);
	printf("Migrate number: %d\n\n", MIGRATE_NUMBER);

	//变量清零
	svInfos.clear();
	vmInfos.clear();
	svCostServers.clear();
	pwCostServers.clear();
	daysReqs.clear();
	svResources.clear();
	vmDeployInfos.clear();
	svRunVmsNumber.clear();
	svRunVms.clear();
	purchase_info.clear();
	migrate_info.clear();
	deploy_info.clear();
	SERVERCOST=0;
	POWERCOST=0;
	TOTALCOST=0;
	MIGRATE_NUMBER=0;

	//第二份文件
	start = clock();
	freopen(INPUT_REDIRECTION_2, "r", stdin);
	cloudResourceSchedulingAlgorithm();
	finish = clock();
	TOTALCOST = SERVERCOST + POWERCOST;//总成本
	SC += SERVERCOST, PC += POWERCOST, TC += TOTALCOST;
	TOTAL_MIGRATE += MIGRATE_NUMBER;
	printf("\nCompute Time: %f s \n", double(finish-start)/CLOCKS_PER_SEC);
	printf("Server Cost: %lld \nPower Cost: %lld \nTotal Cost: %lld \n", SERVERCOST, POWERCOST, TOTALCOST);
	printf("Migrate number: %d\n", MIGRATE_NUMBER);

	//打印两份文件的总成本
	printf("\nServer Cost: %lld \nPower Cost: %lld \nTotal Cost: %lld \n", SC, PC, TC);
	printf("Migrate number: %d \n\n", TOTAL_MIGRATE);
	return 0;
}

文章作者: YangChongZhi
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 YangChongZhi !
评论
 上一篇
十大经典排序算法 十大经典排序算法
   引言: 排序算法是《数据结构与算法》中最基本的算法之一。这里对十大经典的排序算法做一下解释说明。不知道的道友可以来这里扫下盲。转自“菜鸟教程”。 说明  排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部
2021-05-02
下一篇 
最长递增子序列 最长递增子序列
   引言: 最长递增子序列问题是一个很基本、较常见的小问题,但这个问题的求解方法却并不那么显而易见,需要较深入的思考和较好的算法素养才能得出良好的算法。这个问题能运用学过的基本的算法分析和设计的方法与思想,能够锻炼设计较复杂算法的思维。转
2021-03-04
  目录