第一次接触到这个比赛应该是研究生刚入学的时候,在教研室看到了师姐的一份简历,上面就有华为软挑的参赛经历。研一利用空余时间加强C++和STL的学习,看完了《C++ primer》《Effective STL》,自己也写了一些demo,感觉这个比赛可以一试,于是邀请一好友组队搞比赛了。这一个月以来,感觉自己收获很多,但也有一些遗憾,最终成绩是成渝赛区64强。简单总结一下吧:
一、赛题
由于租户对ECS实例(虚拟机,VM)请求的行为具有一定规律,可以通过对历史ECS实例请求的分析,预测到未来一段时间的ECS实例请求,然后对预测的请求分配资源(如图1所示),这样可以找到一个接近最优的分配策略,实现资源最大化利用,同时也能参考预测的结果制定云数据中心的建设计划。
简单来讲,有两个任务:预测和分配。首先训练集(题目提供)里提供了所有种类虚拟机在一段时间内的使用量,然后题目让你预测下对于给定的几种虚拟机,它们在接下来一段时间内(1-2周)的使用量的和。然后把预测的虚拟机放在服务器(已指定CPU和MEM)上,使得某个维度的资源(CPU或MEM)最大化利用,同时要保证另外一个维度的资源不超分。
二、赛题分析
题目描述很长,细节非常多,所以花了一晚上时间静下心来读题目,分析题目,不放过一个细节。搞清题目之后,就开始做第一个部分:预测。
这是一个时间序列预测问题,我觉得首先应该分析数据,刚好那段时间学了点python,于是使用python很快地就画出了每种虚拟机的使用数量趋势图。很遗憾,似乎没有什么明显规律,只是发现了有一些虚拟机的零点数很多,大部分虚拟机趋势图都是起伏不定,并伴随着一些噪声点。
import pylab as pl
import numpy as np
file_name = 'E:\\初始训练集.txt'
file_object = open(file_name)
s = file_object.read()
rows = s.split('\n')
list1 = []
list2 = []
str1 = "2015-01-01"
for row in rows:
split_row = row.split('\t')
split_row[2] = (split_row[2].split(' ')[0])
if split_row[2] != str1:
str1 = split_row[2]
list1.append(list2.count('flavor5'))
list2.clear()
list2.append(split_row[1])
else:
list2.append(split_row[1])
print(list1.count(0))
x = range(len(list1))
y = list1
pl.title("test")
pl.plot(x, y)
pl.show()
1、箱形图法判断异常点
在进行数据处理之前首先要做一下数据清洗工作,也就是判断数据中的一些异常值并做相应的处理。
箱形图(英文:Box plot),又称为盒须图、盒式图、盒状图或箱线图,是一种用作显示一组数据分散情况资料的统计图。因型状如箱子而得名。在各种领域也经常被使用,常见于品质管理,快速识别异常值。
箱形图最大的优点就是不受异常值的影响,能够准确稳定地描绘出数据的离散分布情况,同时也利于数据的清洗。
以一组序列数为例:12,15,17,19,20,23,25,28,30,33,34,35,36,37讲解几个重要因“数”
(1)下四分位数Q1
确定四分位数的位置。Qi所在位置=i(n+1)/4,其中i=1,2,3。n表示序列中包含的项数。
根据位置,计算相应的四分位数。
例中:
Q1所在的位置=(14+1)/4=3.75,
Q1=0.25×第三项+0.75×第四项=0.25×17+0.75×19=18.5;
(2)上四分位数Q3
计算方法同下四分位数。
例中:
Q3所在的位置=3(14+1)/4=11.25,
Q3=0.75×第十一项+0.25×第十二项=0.75×34+0.25×35=34.25。
(3)上限
上限是非异常范围内的最大值。
首先要知道什么是四分位距如何计算的?
四分位距IQR=Q3-Q1,那么上限=Q3+1.5IQR。
(4)下限
下限是非异常范围内的最小值。
下限=Q1-1.5IQR。
2、二次移动平均法预测
去噪之后就要进行预测了,这学期选修了一门金融学,里面讲到了一种方法叫移动平均法,也是一种时间序列预测方法,所以就先把这种方法尝试了一下。后来发现这种方法遇到瓶颈了,分数一直提不上去,徘徊在40-50名左右。所以就查了下这种方法是否还有优化的余地,于是就看到了二次移动平均法,就是在原来的一次移动平均法的基础上再进行一次移动平均。这种方法也不是很复杂,一张图就可以说明:
其实二次移动平均法就是在简单移动平均法或加权移动平均法的基础上,计算变动趋势值,并对变动趋势值进行移动平均,求出若干期的变动趋势平均值,再利用此趋势平均值修正简单移动平均或加权移动平均预测值,以消除原预测值的滞后影响的一种计算方法。
3、最佳适应递减分配
分配问题其实是一个装箱问题,恰好在我的一本数据结构与算法分析书上看到了装箱问题,描述了很多算法,大多是采用贪心策略,其中效果较好的是最佳适应递减算法,所以就先采用了这个算法。
装箱问题是NP问题,即在多项式时间内无法精确求解,一般采用近似算法,即启发式算法,这样可以迅速得到满意解,而不一定是最优解。
常见的算法:NF(Next Fit)近似算法,FF(First Fit)近似算法,FFD(First Fit Decreasing)近似算法,BF(best Fit),BFD(Best Fit Deceasing)等。
下次适应算法(NF):NF算法是最简单也是最早研究的一个算法,它的处理方法是始终维持一个当前打开的箱子,对于每一个要装入的物品,检查该物品是否可以放入当前打开的箱子,如果无法装入,则打开一个空箱子,装入该物品,以该箱子作为当前的箱子,由于每个物品在装入时,只有当前打开的箱子和空箱可以选择,这必然造成装箱的效率低下。
首次适应算法(FF):针对下次适应算法的缺陷,首次适应算法处理当前物品的时候,检查所有非空箱子,找到第一个能够放下当前物品的箱子并将该物品放入,否则则开启新的箱子。
最佳适应算法(BF):与首次适应算法类似,唯一的区别时当物品装箱时,不是直接装在第一个可装入的箱子中,而是装入在最适合该物体的箱子中,如果没有该箱子,则开启空箱。
首次适应算法和最佳适应算法有一个缺陷,即由于物品没有实现排序,则可能由于先装入小的物品,使大的物品在后来放入时无法装入,只得开启新的箱子,造成了空间的浪费,因此才有了这两种算法的改进算法。
首次适应算法递减(FFD):先对物品按降序排序,再按照首次适应算法进行装箱。
最佳适应递减算法(BFD):先对物品按降序排序,再按照最佳适应算法进行装箱。
4、排名情况
由于这学期课很多,所以拖了很久才开始写代码,那时候看到很多队都提交了而且分数很高,所以就想尽快先提交一版代码上去,于是采用了箱型图去噪+一次移动平均+最佳适应递减,提交后调整了一下参数,排名在40左右。然后分数一直上不去,徘徊了一周后,感觉需要做一些调整了,于是把一次移动平均法换成了二次移动平均法,调整下参数后分数就上去了,排名在20左右,在正式赛之前一直很稳定,基本在前36,后来就没再优化代码了(为后来的失误埋下伏笔了)。
正式赛开始后,一提交代码感觉不对劲,分数有点低,下载日志看了下,第八个高级用例没通过。当时感觉压力真的大,正式赛只有两天,一天只能提交五次。于是开始疯狂找bug,这个过程简直是煎熬,身心俱疲,因为只有一个用例报错,bug就非常难找。第一天的下午定位到bug了,但是这个bug很奇怪,经过分析之后,原因是模型本身存在一个缺陷,刚好被这个用例撞上了。当时感觉很失望,觉得一个月的辛苦都要白费了,差点要放弃了。冷静下来后,想到了还有原始版本的代码,所以就把之前的代码提交了,最后还是进了前64,也算是一点欣慰吧。
三、比赛总结
最后总结下这次比赛吧,遗憾肯定是有的,更多的还是收获吧。
1. 提高了分析问题、解决问题的能力,也是对自己毅力的一次锻炼。
2. 更加熟悉了如何使用C++和STL进行泛型编程,解决面向过程的问题,C++语言在解决这些问题上效率还是非常高的。这次比赛我也使用了C++的很多特性:string流输入读取文件、使用tuple类型返回多个值、STL的容器(vector、map等)、STL的泛型算法、使用swap技巧去除多余空间(Effective STL)、map以value排序、lambda表达式、string类的一些操作(append、erase、stoi、to_string)等等。
3. 上面使用到的一些知识点基本都在我以前的博客中可以找到,这也让我更加意识到平时积累知识的重要性,要开阔视野。
4. 这次比赛也让我意识到一些不足:感觉自己还是存在一些惰性和侥幸心理,以为之前的排名会很稳妥,所以就没有尝试其他的算法,更多的时间是在进行调参,所以正式赛出现了问题就显得很被动。
总的来说,有这样一次比赛经历还是可以的,以后要继续努力,多写代码,多看书。最后附上比赛的代码:
#include "predict.h"
#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <fstream>
#include <set>
#include <map>
#include <utility>
#include <numeric>
#include <algorithm>
using namespace std;
struct t_m
{
int y;
int m;
int d;
};
int r_n(int y)//判断闰年
{
if ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0))
return 1;
else
return 0;
}
int d_m(int y, int m)//计算每个月有多少天
{
vector<int> day = { 31,28,31,30,31,30,31,31,30,31,30,31 };
if (m != 2)
return day[m - 1];
else
return 28 + r_n(y);
}
long long int d_p(t_m &t)//计算每个日期离1年1月的天数
{
long long int day = t.d;
for (int i = 1; i < t.y; ++i)
day += 365 + r_n(i);
for (int i = 1; i < t.m; ++i)
day += d_m(t.y, i);
return day;
}
//箱型图去噪
tuple<double, double> qrange(vector<double> vec)
{
double q1, q3, iqr;
const int s = vec.size();
sort(vec.begin(), vec.end());
if (s % 2 != 0)//判断奇偶性
{
auto a = (s + 1) / 4;
int i1 = static_cast<int>(a);
int i3 = static_cast<int>(3 * a);
q1 = vec[i1 - 1] * (a - i1) + vec[i1] * (1 - a + i1);
q3 = vec[i3 - 1] * (3 * a - i3) + vec[i3] * (1 - 3 * a + i3);
iqr = q3 - q1;
const double range_high = q3 + 1.5*iqr;
const double range_low = q1 - 1.5*iqr;
return make_tuple(range_high, range_low);
}
else
{
auto a = s / 4.0;
int i1 = static_cast<int>(a);
int i3 = static_cast<int>(3 * a);
q1 = vec[i1 - 1] * (a - i1) + vec[i1] * (1 - a + i1);//下四分位数
q3 = vec[i3 - 1] * (3 * a - i3) + vec[i3] * (1 - 3 * a + i3);//上四分位数
iqr = q3 - q1;
//两个临界值
const double range_high = q3 + 1.5*iqr;
const double range_low = q1 - 1.5*iqr;
return make_tuple(range_high, range_low);
}
}
//二次移动平均法预测
double qmove(vector<double> &vec)
{
const int winsize = 10;
vector<double> vec2;
vector<double> vec3;
vector<double> vec4;
vector<double> vec5;
for (int i = 0; i <= vec.size() - winsize; ++i)
{
//计算一次移动平均数
vec2.push_back(accumulate(vec.begin() + i, vec.begin() + i + winsize, 0.0) / winsize);
}
for (int i = 0; i <= vec2.size() - winsize; ++i)
{
//计算二次移动平均数
vec3.push_back(accumulate(vec2.begin() + i, vec2.begin() + i + winsize, 0.0) / winsize);
}
for (int i = 0; i < vec3.size(); ++i)
{
//计算at
vec4.push_back(2 * vec2[winsize - 1 + i] - vec3[i]);
}
for (int i = 0; i < vec3.size(); ++i)
{
//计算bt
vec5.push_back((vec2[winsize - 1 + i] - vec3[i]) * 2 / (winsize - 1));
}
//预测值
return *(vec4.end() - 1) + *(vec5.end() - 1);
}
//预测部分
int Predict(string flavor, int day, char * data[MAX_DATA_NUM], int data_num)
{
string word;
vector<string> vec_temp;//临时容器
string str_temp = "2015-01-01";//初始时间
vector<double> vec_count;//纪录下每种规格的虚拟机出现的次数
vector<string> vec_flavor;//纪录下某一天出现的所有虚拟机
for (int i = 0; i < data_num; ++i)
{
istringstream input_string(*&data[i]);//保存来自一行的所有输入。
while (input_string >> word)//string流输入
{
vec_temp.push_back(word);
}
if (vec_temp[2] != str_temp)//如果当前字符串与上一个不同(即天数发生了改变)
{
str_temp = vec_temp[2];
int cnt = 0;//录下上一天某规格虚拟机出现的次数
for (auto it = vec_flavor.begin(); it != vec_flavor.end(); ++it)
{
if (*it == flavor)
++cnt;
}
vec_count.push_back(cnt);
vector<string>().swap(vec_flavor);//清空当前的容器
vec_flavor.push_back(vec_temp[1]);//重新纪录这一天出现的所有虚拟机
}
else
{
vec_flavor.push_back(vec_temp[1]);//记录下某一天出现的所有虚拟机
}
vector<string>().swap(vec_temp);//清空当前的容器
}
double avg = accumulate(vec_count.begin(), vec_count.end(), 0.0) / vec_count.size();
double f1;
double f3;
std::tie(f3, f1) = qrange(vec_count);//获取tuple数据
for (auto &v : vec_count)
{
if (v > f3 || v < f1)//超出范围即为异常点
v = avg * 3;//此数据为调参所得
}
//二次移动平均法循环预测
vector<double> vec_week;
for (int i = 1; i <= day; ++i)
{
vec_week.push_back(qmove(vec_count));
vec_count.push_back(qmove(vec_count));
}
return round(accumulate(vec_week.begin(), vec_week.end(), 0.0));
}
//放置部分
void Optimization(map<string, int> &flavor1, map<string, int> &flavor2,
map<string, int> &flavor_count, int &server1, int &server2, int &num, char * filename)
{
string output_data;//输出数据文件
map<string, int> flavor_count2 = flavor_count;
//以cpu为标准排序,递减排序(map以value排序)
vector<pair<string, int>> flavor_cpu_vec;
for (auto it = flavor1.begin(); it != flavor1.end(); ++it)
{
flavor_cpu_vec.push_back(make_pair(it->first, it->second));
}
sort(flavor_cpu_vec.begin(), flavor_cpu_vec.end(),
[](const pair<string, int> &x, const pair<string, int> &y)
-> int { return x.second > y.second; }
);
//删除原来的map中的元素,节约空间
map<string, int>().swap(flavor1);
//输出数据放在vector中
vector<map<string, int>> output_vec;
//计数已用的服务器数量
int server_cnt = 1;
//计数已用的虚拟机
int flavor_cnt = 0;
//纪录虚拟机是否已用完的标志位
bool flag = false;
//预测的虚拟机总数
int flavor_sum = 0;
/*********************************************************************************
* 最佳适应递减算法(BFD):
* 1、三种情况必须回到大循环(while):
*(1)不管哪一个虚拟机放在服务器上都会引起CPU超分,这时候必须退出到大循环,重开服务器。
*(2)不管哪一个虚拟机放在服务器上都会引起MEM超分,即使CPU没有占满,
* 这时候也必须退出到大循环,重开服务器。
*(3)CPU没有占满,MEM也没有占满,但是不管哪一个虚拟机放在服务器上都会引起CPU或MEME超分。
* 这时候也只能退出到大循环,重开服务器。
* 2、一种情况必须结束大循环(while):
*(1)当所有虚拟机已用完,不管是在小循环(while)还是大循环内,必须退出到大循环外。
*********************************************************************************/
while (true)
{
map<string, int> map_temp;
//计数每轮已用的CPU
int server_cpu_cnt = 0;
//计数每轮已用的MEM
int server_mem_cnt = 0;
//在每一次分配服务器之前,纪录下当前虚拟机的数量
map<string, int> flavor_temp = flavor_count;
while (true)
{
int temp = 0;
for (auto it = flavor_cpu_vec.begin(); it != flavor_cpu_vec.end(); ++it)
{
//如果虚拟机数量为0,则不需要放置
if (flavor_count[it->first] != 0)
{
server_cpu_cnt += it->second;
server_mem_cnt += flavor2[it->first];
//如果当前CPU的总数不超分
if (server_cpu_cnt <= server1)
{
if (server_mem_cnt <= server2)
{
//某种虚拟机已用完
if (flavor_count[it->first] == 0)
{
continue;
}
else
{
--flavor_count[it->first];
}
}
//CPU没用完但是MEM用完了,直接跳过,往后遍历(情况1-2),
//保证循环结束后MEM不超分
else
{
//记录下最后一个使得MEM超分的内存大小
server_mem_cnt -= flavor2[it->first];
continue;
}
}
//保证了MEM不超分,但是CPU超分
else
{
++temp;//记录下是否所有虚拟机都会引起CPU超分
server_mem_cnt -= flavor2[it->first];
server_cpu_cnt -= it->second;
}
}
else
{
++temp;
}
}
//所有虚拟机都会引起CPU超分,CPU必须要被占满(情况1-1或者1-3)
if (temp == num)
{
//重开服务器
++server_cnt;
//纪录已用虚拟机的数据
for (auto it = flavor_temp.begin(); it != flavor_temp.end(); ++it)
{
flavor_sum += (it->second - flavor_count[it->first]);
if (it->second - flavor_count[it->first] != 0)
{
map_temp.insert({ it->first, it->second - flavor_count[it->first] });
}
}
//放入输出容器中,每个元素都是map
output_vec.push_back(map_temp);
//清除临时的map,节约空间
map<string, int>().swap(map_temp);
//重新遍历
break;
}
//所有虚拟机已用完。
for (auto it = flavor_count.begin(); it != flavor_count.end(); ++it)
{
if (it->second == 0)
{
++flavor_cnt;
}
}
//所有虚拟机数量都为0,则说明所有服务器已分配完
if (flavor_cnt == num)
{
flag = true;
break;
}
else
{
flavor_cnt = 0;
}
}
//最后一个服务器上放置了剩余虚拟机,虚拟机已全部用完(情况2-1)
if (flag)
{
for (auto it = flavor_temp.begin(); it != flavor_temp.end(); ++it)
{
flavor_sum += (it->second - flavor_count[it->first]);
if (it->second - flavor_count[it->first] != 0)
{
map_temp.insert({ it->first, it->second - flavor_count[it->first] });
}
}
output_vec.push_back(map_temp);
map<string, int>().swap(map_temp);
break;
}
}
//输出数据
output_data.append(to_string(flavor_sum) + "\n");
for (auto it = flavor_count2.begin(); it != flavor_count2.end(); ++it)
{
output_data.append(it->first + " " + to_string(it->second) + "\n");
}
output_data.append("\n" + to_string(server_cnt));
string str;
for (int i = 1; i <= server_cnt; ++i)
{
output_data.append("\n" + to_string(i) + " ");
for (auto it = output_vec[i - 1].begin(); it != output_vec[i - 1].end(); ++it)
{
str += it->first + " " + to_string(it->second) + " ";
}
//将所有输出字符放在字符串中,通过erase操作删除最后的一个空格
output_data.append(str.erase(str.size() - 1, 1));
str.clear();//清除字符串
}
auto result_file = output_data.c_str();//转换为c风格字符串
write_result(result_file, filename);
}
void predict_server(char * info[MAX_INFO_NUM], char * data[MAX_DATA_NUM], int data_num, char * filename)
{
//读取文件内容
string input_word;
vector<vector<string>> input_vec;
vector<string> input_vec_temp;
for (int i = 0; i <= 2; ++i)
{
string a = *&info[i];
if (a != "\n")
{
istringstream input_string(a);
while (input_string >> input_word)
{
input_vec_temp.push_back(input_word);
}
input_vec.push_back(input_vec_temp);
vector<string>().swap(input_vec_temp);
}
}
int testnum = stoi(*&info[2]);
for (int i = 3; i <= testnum + 7; ++i)
{
string a = *&info[i];
if (a != "\n")
{
istringstream input_string(a);
while (input_string >> input_word)
{
input_vec_temp.push_back(input_word);
}
input_vec.push_back(input_vec_temp);
vector<string>().swap(input_vec_temp);
}
}
//物理服务器的CPU大小
int server_cpu = stoi(input_vec[0][0]);
//物理服务器的内存大小
int server_mem = stoi(input_vec[0][1]);
//需要预测的天数
int num = stoi(input_vec[2][0]);
t_m t1, t2;
t1.y = stoi((input_vec[num + 6][0]).substr(0, 4));
t1.m = stoi((input_vec[num + 6][0]).substr(5, 2));
t1.d = stoi((input_vec[num + 6][0]).substr(8, 2));
t2.y = stoi((input_vec[num + 7][0]).substr(0, 4));
t2.m = stoi((input_vec[num + 7][0]).substr(5, 2));
t2.d = stoi((input_vec[num + 7][0]).substr(8, 2));
long long int day1 = d_p(t1);
long long int day2 = d_p(t2);
long long int day = day2 - day1;
//需要分配的虚拟机的数量
//对于给定的虚拟机,得到预测数量
vector<int> vec_predict;
for (int i = 3; i < num + 3; ++i)
{
vec_predict.push_back(Predict(input_vec[i][0], day, data, data_num));
}
//需要优化的某种资源(CPU或MEM)
string resources = input_vec[num + 4][0];
//虚拟机型号与数量
map<string, int> flavor_count;
for (int i = 3; i < num + 3; ++i)
{
flavor_count.insert({ input_vec[i][0], vec_predict[i - 3] });
}
//复制一份,便于最后输出预测的虚拟机总数和不同规格虚拟机名称及数量
map<string, int> flavor_count2 = flavor_count;
//虚拟机的型号与cpu大小
map<string, int> flavor_cpu;
for (int i = 3; i < num + 3; ++i)
{
flavor_cpu.insert({ input_vec[i][0], stoi(input_vec[i][1]) });
}
//虚拟机的型号与内存大小
map<string, int> flavor_mem;
for (int i = 3; i < num + 3; ++i)
{
flavor_mem.insert({ input_vec[i][0], stoi(input_vec[i][2]) / 1024 });
}
//优化CPU
if (resources == "CPU")
{
Optimization(flavor_cpu, flavor_mem, flavor_count, server_cpu, server_mem, num, filename);
}
else
{
Optimization(flavor_mem, flavor_cpu, flavor_count, server_mem, server_cpu, num, filename);
}
}