2020编码大赛题目 https://blog.youkuaiyun.com/nameofcsdn/article/details/109323428
初赛,是392个队伍选出32个队伍。
目录
一,读题
- 12 * 12的矩形,地图中存在的元素有虚拟资源、比赛双方机器人、比赛双方基地、障碍物
- 每个虚拟资源区只存在一种矿产类型,有多个资源单位,每单位虚拟资源单位大小为1024KB,资源机器人自身初始容量2048KB
- 理想压缩比例:1:3,理想情况装载6份
- 每个资源机器人初始能量100,且最大上限也为100
- 能量掠夺者购买金额 50 金币,移动至对方资源机器人周边 上、下、左、右 每次掠夺能量值5,如果对方资源机器人能量不足5,则只能按对方资源机器人实际能量掠夺
- 金币作用:可购买 “能量掠夺者”、可购买能量进行充能,每花费一个金币可以购买一个单位能量
- 每压缩一单位资源消耗能量 +1,每移动一个位置消耗1个单位能量
- 红 4 绿 2 蓝 1
二,初步思路
1,掠夺者的任务:掠夺能量最多,保护我方机器人
2,机器人路线策略:要考虑路径长度、减少被偷能量
3,机器人每一次都要返回基地
4,尽可能早的购买掠夺者
5,金币用法:尽早购买掠夺者,剩下所有金币都用来购买能量
6,机器人先装载红色的,再装载绿色的,最后蓝色
7,平衡点:压缩蓝色资源,消耗能量换金币,消耗金币换能量,比直接装载要少2个能量,但是多4积分
三,系统思考
首先,这个问题由两部分组成,一个是挖矿类的路径规划类的博弈游戏,一个是文本压缩算法。
这两个原本完全解耦的问题,被强行耦合到了一起。
其次,大概可以预估一下,博弈部分和压缩部分的工作量各占一半。
最后,简单分类一下,压缩是通用无损文本压缩算法,面向单文件,不涉及文件夹,博弈是几乎完全信息的博弈。
四,需求分析
整个需求,可以分为几块:工程、输入输出、策略、压缩算法、时间控制。
1,工程
包括代码仓、流水线、链接库、配置工程可以本地断点调试、控制宏等等
2,输入输出
Json输入输出,包括数据结构怎么定义
3,策略
包括寻路算法、机器人调度、掠夺者调度、购买策略、地图地形策略、其他策略
4,压缩算法
先选LZW算法,然后主攻调度,然后循环pk一圈评估一下自己的水平,如果能进32强,再抓紧时间研究更强的压缩算法。
5,时间控制
每次调度机器人、调度掠夺者、调度压缩、调度解压,都不能超过10秒
下面,我将一一阐述我在这5个方面都做了什么。
五,工程
1,链接库
很坑的一点就是json库也不给编好,要自己弄,因为版本问题耽误半天。
2,本地断点调试
用平常需求开发用的LLT的工程和服务器整了一下,整了个可以在本地编辑代码,一键同步到服务器并编译,还可以断点调试的工程。
但是这个断点调试有时候不好用,所以我加了自定义输出函数辅助调试。
void myout(int a)
{
cout << a;
}
void myout(string a)
{
cout << a;
}
void myout(Pos a)
{
cout << "x:" << a.x << " y:" << a.y << endl;
}
void myout(vector<Pos> v)
{
for (unsigned int i = 0; i < v.size(); i++) {
myout(v[i]);
}
}
void myout(int themap[M][M])
{
for (unsigned int i = 1; i < M - 1; i++) {
for (unsigned int j = 1; j < M - 1; j++) {
cout << themap[i][j] << " ";
}
cout << endl;
}
}
六,输入输出
1,数据结构
第一次用json,有个思维误区,我把我的数据结构定义成和json的数据结构一模一样,虽然解析的时候很方便,但是很快我就发现这个数据结构有3个问题!
(1)数据冗余
(2)嵌套太深
(3)不符合我自己的习惯
于是改了几处数据结构的定义方式,还把棋盘改了。
数据结构定义:
struct Pos {
int x, y; // 坐标
};
typedef enum {
WALL,
BLUE, // 1
GREEN, // 2
BLANK,
RED, // 4
myBase,
enemyBase
} maptype;
struct ResourceUnit {
int gemType; // 资源类型, maptype中的1,2,4
Pos pos; // 资源位置
string index; // 资源唯一索引。
int sizeOfUnit; // 单位资源大小。当前设置为1024。
int sizeOfCompressionUnit; // 该资源单位在被压缩装载后的大小
};
struct Looter {
string playerName; // 能量掠夺者名称
Pos pos; // 当前掠夺者所在位置
};
struct Worker {
string playerName; // 资源机器人名称
int power; // 当前资源机器人能量
Pos pos; // 当前资源机器人所在位置
int load; // 资源机器人已装载资源大小,单位KB
std::vector<ResourceUnit> loadInfo; // 装载资源详情,如果本回合未回到基地进行资源卸载,则该字段携带
int maxLoad; // 资源机器人可装载资源最大值,单位KB
};
struct OneTeamInfo {
int points; // 当前赚取的积分
int golds; // 当前的总金币
Pos campPos; // 己方基地的位置信息
Worker worker; // 资源机器人信息
Looter looter; // 能量掠夺者信息,未购买前该字段一直为空
};
struct PurchaseCommand {
Pos pos; // 购买命令执行位置(只能为己方基地)
string purchaseType; // 购买的资源类型,BUY_LOOTER:购买掠夺者,BUY_POWER:购买能量
};
struct CollectCommand {
Pos zone; // 收集资源单位位置。
string index; // 资源单位唯一索引,通过指定唯一索引,选择所要装载的资源单位。
string commandType; // 命令类型 COMPRESS_TAKE:对资源进行压缩装载 TAKE:不对资源进行压缩,直接装载
};
这里用到了一个编码技巧,我把红绿蓝的枚举值定义为4 2 1,方便计算。
数据定义:
int g_width; // 地图宽度
int g_height; // 地图高度
int g_mapEntities[M][M]; // 地图详细信息,maptype
int g_mapEntities2[M][M]; // 地图信息备份
int g_power2; // 能量信息备份
int g_round; // 当前回合数
string g_turn; // 当前玩家机器人名称
OneTeamInfo g_myTeam; // 自己队伍信息
OneTeamInfo g_enemyTeam; // 敌方队伍信息
vector<ResourceUnit> g_resourceUnits[M][M]; // 资源单位列表,每个资源区有多个相同类型的单位资源
vector<ResourceUnit> g_collectHistory; // 我方已收集资源列表
棋盘的x和y两个坐标轴的定义和我的习惯不一样(给的是数学坐标轴定义,我的思维习惯早就转换成了C++中数组的坐标轴定义方式了),我把x和y互换了,而且把下标范围从0-11改成了1-12,然后在棋盘周围加一圈墙,这个编程习惯也是我多次编程棋盘类程序早就形成的固定规则。
static void parseJsonPos(Json::Value pos, Pos &p)
{
p.x = pos["y"].asInt() + 1;
p.y = pos["x"].asInt() + 1;
}
好在这个程序输入输出各只有一次,输入在程序最前面,输出在程序最后面,所以倒是不容易引起混乱。
倒是调试的时候稍微有一点点不适应,但是适应了之后还好,程序员无所畏惧!
2,Json输入输出
输入输出本身没什么难点注意点,也不涉及性能。
代码:
static int stringToInt(string s)
{
if (s == "gem_blue")
return BLUE;
if (s == "gem_red")
return RED;
if (s == "gem_green")
return GREEN;
if (s == "blank")
return BLANK;
if (s == "wall")
return WALL;
return -1;
}
static void parseJsonResourceUnit(Json::Value resourceUnits, ResourceUnit &p)
{
p.gemType = stringToInt(resourceUnits["gemType"].asString());
parseJsonPos(resourceUnits["pos"], p.pos);
p.index = resourceUnits["index"].asString();
p.sizeOfUnit = resourceUnits["sizeOfUnit"].asInt();
p.sizeOfCompressionUnit = resourceUnits["sizeOfCompressionUnit"].asInt();
}
static void parseJsonWorker(Json::Value worker, Worker &p)
{
p.playerName = worker["playerName"].asString();
p.power = worker["power"].asInt();
parseJsonPos(worker["pos"], p.pos);
p.load = worker["load"].asInt();
Json::Value loadInfo = worker["loadInfo"];
p.loadInfo.resize(loadInfo.size());
for (int i = 0; i < int(loadInfo.size()); i++)
parseJsonResourceUnit(loadInfo[i], p.loadInfo[i]);
p.maxLoad = worker["maxLoad"].asInt();
}
static void parseJsonLooter(Json::Value looter, Looter &p)
{
p.playerName = looter["playerName"].asString();
parseJsonPos(looter["pos"], p.pos);
if (p.pos.x == 1 && p.pos.y == 1) {
p.pos.x = 0, p.pos.y = 0;
}
}
static void parseJsonOneTeam(Json::Value team, OneTeamInfo &p)
{
p.points = team["points"].asInt();
p.golds = team["golds"].asInt();
parseJsonPos(team["campPos"], p.campPos);
parseJsonWorker(team["worker"], p.worker);
parseJsonLooter(team["looter"], p.looter);
}
static void parseJsonTeams(Json::Value teams)
{
parseJsonOneTeam(teams["yourTeam"], g_myTeam);
parseJsonOneTeam(teams["enemyTeam"], g_enemyTeam);
}
static void parseJsonGameMap(Json::Value gameMap)
{
g_width = gameMap["width"].asInt();
g_height = gameMap["height"].asInt();
Json::Value mapEntities = gameMap["mapEntities"];
Pos p;
for (int i = 0; i < int(mapEntities.size()); i++) {
parseJsonPos(mapEntities[i]["pos"], p);
g_mapEntities[p.x][p.y] = stringToInt(mapEntities[i]["data"].asString());
}
g_mapEntities[g_myTeam.campPos.x][g_myTeam.campPos.y] = myBase;
g_mapEntities[g_enemyTeam.campPos.x][g_enemyTeam.campPos.y] = enemyBase;
}
static void parseJsonResourceZone(Json::Value resourceZone)
{
Json::Value resourceUnits = resourceZone["resourceUnits"];
ResourceUnit r;
for (int i = 0; i < int(resourceUnits.size()); i++) {
parseJsonResourceUnit(resourceUnits[i], r);
g_resourceUnits[r.pos.x][r.pos.y].push_back(r);
}
}
static void parseJsonResourceZones(Json::Value resourceZones)
{
for (unsigned int i = 0; i < resourceZones.size(); i++) {
parseJsonResourceZone(resourceZones[i]);
}
}
static void parseJsonCollectHistory(Json::Value collectHistory)
{
g_collectHistory.resize(collectHistory.size());
for (int i = 0; i < int(collectHistory.size()); i++)
parseJsonResourceUnit(collectHistory[i], g_collectHistory[i]);
}
void parseJson(string fileName)
{
ifstream t(fileName);
string str((istreambuf_iterator<char>(t)), istreambuf_iterator<char>());
JSONCPP_STRING err;
Json::Value root;
Json::CharReaderBuilder jsBuilder;
unique_ptr<Json::CharReader> reader(jsBuilder.newCharReader());
reader->parse(str.c_str(), str.c_str() + str.length(), &root, &err);
g_round = root["round"].asInt();
g_turn = root["turn"].asString();
parseJsonTeams(root["teams"]);
parseJsonGameMap(root["gameMap"]);
parseJsonResourceZones(root["resourceZones"]);
parseJsonCollectHistory(root["collectHistory"]);
cout << "\nbegin ________________root__________________\n";
OUT(str);
cout << "\nend ________________root__________________\n";
}
static Json::Value PosToJson(Pos p)
{
Json::Value ans;
ans["y"] = Json::Value(p.x - 1);
ans["x"] = Json::Value(p.y - 1);
return ans;
}
static Json::Value CollectCommandsToJson(CollectCommand p)
{
Json::Value ans;
ans["zone"] = PosToJson(p.zone);
ans["index"] = Json::Value(p.index);
ans["commandType"] = Json::Value(p.commandType);
return ans;
}
static Json::Value PurchaseCommandsToJson(PurchaseCommand p)
{
Json::Value ans;
ans["pos"] = PosToJson(p.pos);
ans["purchaseType"] = p.purchaseType;
return ans;
}
void writeJson(string fileName)
{
Json::Value root;
Json::Value path;
Json::Value collectCommands;
Json::Value purchaseCommands;
for (unsigned int i = 0; i < g_path.size(); i++)
path.append(PosToJson(g_path[i]));
root["path"] = path;
root["round"] = Json::Value(g_round);
root["turn"] = Json::Value(g_turn);
for (unsigned int i = 0; i < g_collectCommands.size(); i++)
collectCommands.append(CollectCommandsToJson(g_collectCommands[i]));
root["collectCommands"] = collectCommands;
root["collectCommands"].resize(g_collectCommands.size());
for (unsigned int i = 0; i < g_purchaseCommands.size(); i++)
purchaseCommands.append(PurchaseCommandsToJson(g_purchaseCommands[i]));
root["purchaseCommands"] = purchaseCommands;
root["purchaseCommands"].resize(g_purchaseCommands.size());
Json::StreamWriterBuilder jswBuilder;
unique_ptr<Json::StreamWriter> jsWriter(jswBuilder.newStreamWriter());
ofstream os;
os.open(fileName);
jsWriter->write(root, &os);
os.close();
cout << root;
return;
}
七,策略
1,博弈属性
根据我对博弈游戏多年的研究,博弈游戏可以分两种:
第一层逻辑很简单,但是逻辑嵌套深,比如五子棋、围棋
第一层逻辑很复杂,但是逻辑嵌套浅,比如打麻将、大部分桌游(包括三国杀)
这是表象论述,实际上,本质差异在于,像棋类游戏,每一步可以做出的选择是可数且很有限的,虽然围棋棋盘大一点,但是实际上选择并不多,而且也可以理解为只有一种操作,只是参数值不一样。而像桌游类游戏,每一步可以做出的选择很多样化,比如麻将虽然貌似也可以按照围棋的逻辑,理解为只有一种操作(主要是打这一种,且不说碰杠胡等等),但是打筒子,万子,条子,本质上属于完全不同的操作,而像三国杀等桌游,可选的几种操作也是完全不一样。
棋类游戏为什么嵌套深?
因为单层逻辑没有任何意义,对于单步操作进行估值打分会非常局限,每一次操作对格局的影响很大。
桌游类游戏为什么嵌套浅?
因为一步操作就很复杂,很难精准建模,甚至可能一步操作就是NP难问题。如果一步操作都是不精确的,那么逻辑嵌套的推理就会越来越不精确,直至完全没有意义。对于单步操作进行估值打分比较直观,桌游一般是积分制,本次比赛也是积分制,每一次操作对格局的影响不大。
所以,本次比赛就只需要考虑自己怎么规划就行了,不需要嵌套逻辑分析自己每个行为之后对方的行为差异,也就用不上极大极小这一类嵌套型博弈算法了。
2,寻路算法
寻路需要的是一个多点寻路算法,但是没有比较好的多点寻路的思路,所以改成了依次分段两点寻路。
int g_dx[DLEN] = { -1, -1, 0, 1, 1, 1, 0, -1 };
int g_dy[DLEN] = { 0, 1, 1, 1, 0, -1, -1, -1 };
// 获取该方向的下一个位置
Pos getPos(Pos a, int direc)
{
a.x += g_dx[direc], a.y += g_dy[direc];
return a;
}
// 两个坐标是否相同
bool samePos(Pos a, Pos b)
{
return a.x == b.x && a.y == b.y;
}
// 判断是否为可达位置
bool avail(Pos p, int themap[M][M])
{
int x = p.x, y = p.y;
if (x <= 0 || x > g_height || y <= 0 || y > g_width) {
return false;
}
if (themap[x][y] == WALL || themap[x][y] == enemyBase) {
return false;
}
if (samePos(p, g_myTeam.worker.pos) || samePos(p, g_myTeam.looter.pos)) {
return false;
}
if (samePos(p, g_enemyTeam.worker.pos) || samePos(p, g_enemyTeam.looter.pos)) {
return false;
}
return true;
}
// 根据搜索结果抠出a到b的最短路径
static int getPath(int (&len)[M][M], vector<Pos> &path, Pos a, Pos b)
{
int ans = len[b.x][b.y];
path.resize(ans);
for (int i = ans; i > 0; i--) {
path[i - 1] = b;
for (int j = 0; j < DLEN; j++) {
Pos tmp = getPos(b, j);
if (len[tmp.x][tmp.y] == i - 1) {
b = tmp;
break;
}
}
}
return ans;
}
// 启发式比较函数
Pos g_temPos;
int g_len[M][M];
static int guessLen(Pos a)
{
return g_len[a.x][a.y] + max(abs(a.x - g_temPos.x), abs(a.y - g_temPos.y));
}
struct cmp {
bool operator () (Pos a, Pos b)
{
return guessLen(a) > guessLen(b);
}
};
// 求a到b的最短路径,返回路径长度,-1表示无解
int getPath(Pos a, Pos b, int themap[M][M], vector<Pos> &path)
{
g_temPos = b;
priority_queue<Pos, vector<Pos>, cmp> q;
while (!q.empty()) {
q.pop();
}
int len[M][M];
const int maxlen = 100;
memset(len, maxlen, sizeof(len));
len[a.x][a.y] = 0;
q.push(a);
Pos tmp, tmp2;
while (!q.empty()) {
tmp = q.top();
q.pop();
if (samePos(tmp, b)) {
return getPath(len, path, a, b);
}
for (int i = 0; i < DLEN; i++) {
tmp2 = getPos(tmp, i);
if (!avail(tmp2, themap) || len[tmp2.x][tmp2.y] <= len[tmp.x][tmp.y] + 1) {
continue;
}
len[tmp2.x][tmp2.y] = len[tmp.x][tmp.y] + 1;
g_len[tmp2.x][tmp2.y] = len[tmp.x][tmp.y] + 1;
q.push(tmp2);
}
}
return -1; // 无解
}
这里主要是两块代码,一个是启发式搜索算法,即A*,一个是根据搜索结果把路径扣出来。
这种把多点寻路分解成多段两点寻路的做法,相当于贪心,其实并不是很好。
两点之间的最短路有很多,用A*选出搜到的第一条,可能造成原本多点寻路是有解的但是搜不到。
多点寻路:
// 2个vector拼接起来
vector<Pos> join(vector<Pos> &v1, vector<Pos> &v2)
{
vector<Pos> ans(v1.size() + v2.size());
copy(v1.begin(), v1.end(), ans.begin());
copy(v2.begin(), v2.end(), ans.begin() + v1.size());
return ans;
}
// 把经过的点设置为WALL,表示不可重入
void killMap(vector<Pos> path, int (&themap)[M][M])
{
for (unsigned int i = 0; i < path.size(); i++) {
themap[path[i].x][path[i].y] = WALL;
}
}
// 求最短路径,返回路径长度,0表示不动,-1表示无解
int getPath(vector<Pos> target, vector<Pos> &path)
{
if (target.size() < 2) {
path = target;
return -1;
}
int themap[M][M];
for (int i = 0; i < M; i++) {
for (int j = 0; j < M; j++) {
themap[i][j] = g_mapEntities[i][j];
}
}
vector<Pos> tmp;
path.resize(0);
int ans = 0;
int power = g_myTeam.worker.power;
for (unsigned int i = 1; i < target.size(); i++) {
int ret = getPath(target[i - 1], target[i], themap, tmp);
power -= ret + getPathCompressPower(tmp);
ans += ret;
if (ret == -1 || power < 0) {
return -1;
}
killMap(tmp, themap);
path = join(path, tmp);
}
path.insert(path.begin(), target[0]);
return ans;
}
int getPath(vector<Pos> target)
{
int num;
return getPath(target, num);
}
3,机器人调度
机器人调度分为:决定目标矿堆颜色、选取要去的矿堆、选取要去的终点、选矿收集、其他策略
(1)决定目标矿堆颜色
机器人先装载红色的,再装载绿色的,最后装载蓝色。
// 目标矿堆颜色,本函数只需要运算一次
static int targetColor(Pos a, Pos b)
{
if (g_col) {
return g_col;
}
int t = alreadyHaveMine();
if (t) {
return g_col = t;
}
for (int i = 1; i <= g_height; i++) {
for (int j = 1; j <= g_width; j++) {
if (g_mapEntities[i][j] == RED && targetAvail(a, Pos { i, j }, b, t)) {
return g_col = RED;
}
}
}
for (int i = 1; i <= g_height; i++) {
for (int j = 1; j <= g_width; j++) {
if (g_mapEntities[i][j] == GREEN && targetAvail(a, Pos { i, j }, b, t)) {
return g_col = GREEN;
}
}
}
for (int i = 1; i <= g_height; i++) {
for (int j = 1; j <= g_width; j++) {
if (g_mapEntities[i][j] == BLUE && targetAvail(a, Pos { i, j }, b, t)) {
return g_col = BLUE;
}
}
}
return WALL;
}
(2)选取要去的矿堆
选取的依据:
在确保能量足够的情况下,一个个添加矿堆,直到能量不够或者承载空间装满。
每次选取一个矿堆的时候,都根据总路径最短的原则选择一个矿堆。
vector<Pos> g_path; // 当前机器人被调度路径,原地不动需返回当前位置
vector<CollectCommand> g_collectCommands;
vector<PurchaseCommand> g_purchaseCommands;
int g_col = 0; // 目标矿堆颜色
int g_onesize; // 一个资源压缩后的容量
int g_size; // 剩余容量
bool g_startFromCol; // 是否从矿堆出发
Pos g_posFlag;
// 以能量能否支撑从worker到单点到基地的一个来回,作为单点是否可达的核心依据
static int targetAvail(Pos start, Pos a, int &num)
{
vector<Pos> tar(2);
tar[0] = start;
if (samePos(tar[0], a)) {
return true;
}
tar[1] = a;
int ret = getPath(tar, num);
if (ret == -1) {
return 0;
}
return ret;
}
static int targetAvail(Pos start, Pos a, Pos b, int &num)
{
vector<Pos> tar(3);
tar[0] = start;
if (samePos(tar[0], a)) {
return true;
}
if (samePos(b, g_posFlag)) {
return targetAvail(start, a, num);
}
tar[1] = a;
tar[2] = b;
int ret = getPath(tar, num);
if (ret == -1) {
return 0;
}
return ret;
}
static Pos getMinPos(map<pair<int, int>, int> m)
{
int minm = M * M * 8;
Pos ans { 0, 0 };
for (int i = 1; i <= g_height; i++) {
for (int j = 1; j <= g_width; j++) {
if (m[make_pair(i, j)] > 8 && m[make_pair(i, j)] < minm) {
int x = m[make_pair(i, j)];
minm = x, ans = Pos { i, j };
}
}
}
return ans;
}
// 第一次调用,path是worker到基地这2个点
static bool getTarget(vector<Pos> &path)
{
int col;
col = targetColor(path[0], *(path.end() - 1));
if (col == WALL) {
return false;
}
map<pair<int, int>, int> m;
Pos tmp;
int num;
for (int i = 1; i <= g_height; i++) {
for (int j = 1; j <= g_width; j++) {
tmp.x = i, tmp.y = j;
if (g_mapEntities[i][j] == col) {
m[make_pair(i, j)] = targetAvail(*(path.end() - 2), tmp, *(path.end() - 1), num) * 8;
m[make_pair(i, j)] += num;
}
}
}
tmp = getMinPos(m);
vector<Pos> temp;
if (!samePos(tmp, Pos { 0, 0 })) {
getPath(*(path.end() - 2), tmp, g_mapEntities, temp);
killMap(temp, g_mapEntities);
g_myTeam.worker.power -= getPathCompressPower(temp) + temp.size();
path.insert(path.end() - 1, tmp);
return true;
}
return false;
}
getTarget第一次调用的时候,path是worker到基地两个点,每次调用getTarget都会在worker前面插入一个点。
(3)选取要去的终点
如果从起点回到了基地,为了让每一回合都能回基地,每次回基地之后都要出来。
还有直接从基地出发的情况、从矿堆出发等等。
经过梳理,我列出了5个判定:
- worker能否返回基地
- worker从基地出发
- worker是否从矿堆出发
- worker已经携带矿产
- worker到基地还是到新终点
据此,进一步思考代码框架。
首先,根据位置情况,分为两大类,能返回基地的,不能返回基地的(包括从基地出发的)。
不能返回基地的情况下,从起点出发,挖矿,挖到能量没了或者空间满了就停止。
能返回基地的情况下,先回基地再从基地出发,后面这一段的逻辑和不能返回基地的情况下的逻辑是一样的。
其次,如果已经携带矿产,那么就只能收取该颜色的矿,如果没有,那就按照红绿蓝的顺序收取。
最后,如果是从矿堆出发,需要程序特殊处理一下,判断是否要挖这个矿堆的矿,参见g_startFromCol相关代码。
注意:不能返回基地的情况下有一个bug,怎么能肆意妄为一直挖矿,把能量都整没了呢?必须要留一些才对啊。
但是这个bug当时并没有发现,直到很久很久以后,总决赛的时候我才发现。
为什么那么久都没发现呢?因为我考虑了另外一个策略,把这块代码暂时替换掉了。
我发现因为位置不可重入,导致我从基地出来之后明明场上还有红矿但是却挖了绿矿,所以把代码改成,如果是回了基地再出来,只走2步就直接停下来。
这一部分代码:
// 已装载资源类型,1,2,4,或者0表示未装载
static int alreadyHaveMine()
{
if (g_myTeam.worker.loadInfo.size()) {
return g_myTeam.worker.loadInfo[0].gemType;
}
return 0;
}
// 当前场上最大面额的矿
static int maxColor()
{
for (int i = 1; i <= g_height; i++) {
for (int j = 1; j <= g_width; j++) {
if (g_mapEntities[i][j] == RED) {
return RED;
}
}
}
for (int i = 1; i <= g_height; i++) {
for (int j = 1; j <= g_width; j++) {
if (g_mapEntities[i][j] == GREEN) {
return GREEN;
}
}
}
for (int i = 1; i <= g_height; i++) {
for (int j = 1; j <= g_width; j++) {
if (g_mapEntities[i][j] == BLUE) {
return BLUE;
}
}
}
return WALL;
}
static int colNum(int col)
{
int ans = 0;
for (int i = 1; i <= g_height; i++) {
for (int j = 1; j <= g_width; j++) {
if (g_mapEntities[i][j] == col) {
ans++;
}
}
}
return ans;
}
// 剩余容量
static int remainSize()
{
g_onesize = COMSIZE;
if (g_collectHistory.size()) {
int sizesum = 0;
for (unsigned int i = 0; i < g_collectHistory.size(); i++) {
sizesum += g_collectHistory[i].sizeOfCompressionUnit;
}
g_onesize = sizesum / g_collectHistory.size() + 5;
}
return g_size = (g_myTeam.worker.maxLoad - g_myTeam.worker.load) / g_onesize;
}
// 能否直接返回基地
static bool canGotoCamp()
{
vector<Pos> tar(2);
tar[0] = g_myTeam.worker.pos;
tar[1] = g_myTeam.campPos;
bool flag = (getPath(tar) != -1);
return flag;
}
static int getResourceNum(Pos p)
{
if (samePos(p, g_posFlag)) {
return 0;
}
return g_resourceUnits[p.x][p.y].size();
}
// 这条路径要装载多少份资源
static int getPathSize(vector<Pos> p)
{
int ans = 0;
if (g_startFromCol) {
ans = getResourceNum(p[0]);
}
for (unsigned int i = 1; i < p.size(); i++) {
ans += getResourceNum(p[i]);
}
if (ans > remainSize()) {
return remainSize();
}
return ans;
}
// 计算压缩所需能量
int getPathCompressPower(vector<Pos> p)
{
if (p.size() == 0) {
return 0;
}
int size = getPathSize(p);
if (size > 2) {
return size;
}
return 0;
}
static void play(Pos start, Pos end, vector<Pos> &path)
{
vector<Pos> tar(2);
tar[0] = start;
tar[1] = end;
for (int i = 0; i < M; i++) {
for (int j = 0; j < M; j++) {
g_mapEntities2[i][j] = g_mapEntities[i][j];
}
}
g_power2 = g_myTeam.worker.power;
while (getPathSize(tar) < remainSize()) {
if (!getTarget(tar)) {
break;
}
}
g_myTeam.worker.power = g_power2;
for (int i = 0; i < M; i++) {
for (int j = 0; j < M; j++) {
g_mapEntities[i][j] = g_mapEntities2[i][j];
}
}
if (samePos(*(tar.end() - 1), g_posFlag)) {
tar.erase(tar.end() - 1);
}
int ret = getPath(tar, path);
if (ret == -1) {
cout << endl << "play1() ret = -1" << endl;
return;
}
setCollectCommands(tar);
}
// 是否从基地出发
static bool startAtCamp()
{
return samePos(g_myTeam.worker.pos, g_myTeam.campPos);
}
// 机器人调度
static void play()
{
Pos workerPos = g_myTeam.worker.pos;
if (!startAtCamp() && canGotoCamp()) {
g_startFromCol = (g_mapEntities[workerPos.x][workerPos.y] == targetColor(workerPos, g_myTeam.campPos));
play(workerPos, g_myTeam.campPos, g_path);
g_myTeam.worker.power -= g_path.size() - 1 + getPathCompressPower(g_path) - g_col * getPathSize(g_path);
int num[5] = {0};
num[1] = colNum(1), num[2] = colNum(2), num[4] = colNum(4);
killMap(g_path, g_mapEntities);
g_startFromCol = false, workerPos = *(g_path.end() - 1), g_myTeam.worker.load = 0, g_col = 0;
g_myTeam.worker.loadInfo.clear();
vector<Pos> path;
bool flag = (targetColor(workerPos, g_posFlag) == maxColor() || num[maxColor()] == 1);
play(workerPos, g_posFlag, path);
if (path.size()) {
path.erase(path.begin());
}
if (!flag) {
if (path.size() > 2) {
path.resize(2);
}
}
g_path = join(g_path, path);
return;
}
g_startFromCol = (g_mapEntities[workerPos.x][workerPos.y] == targetColor(workerPos, g_posFlag));
return play(workerPos, g_posFlag, g_path);
}
关于机器人的终点位置这块,我做的并不好,完全没有躲避掠夺者,能量被偷的很快。
(4)选矿收集
这里暂时没引入策略,直接按顺序排,依次添加到收集命令。
考虑过决策是否一部分压缩携带,一部分直接携带,没细想。
代码:
// 设置收集命令
static void setCollectCommands(Pos p)
{
CollectCommand c;
for (unsigned int i = 0; i < g_resourceUnits[p.x][p.y].size() && g_size; i++) {
g_size--;
c.zone = p;
c.index = g_resourceUnits[p.x][p.y][i].index;
c.commandType = "COMPRESS_TAKE";
g_collectCommands.push_back(c);
}
}
// 设置收集命令
static void setCollectCommands(vector<Pos> p)
{
for (unsigned int i = 0; i < p.size(); i++) {
if (i == 0 && !g_startFromCol) {
continue;
}
setCollectCommands(p[i]);
}
}
(5)其他策略
在多点寻路的过程中,添加多路径特征打分的机制:
int near(Pos a, Pos b)
{
return a.x >= b.x - 1 && a.x <= b.x + 1 && a.y >= b.y - 1 && a.y <= b.y + 1;
}
static int pathNear(vector<Pos> p, Pos a)
{
int n = 0;
for (unsigned int i = 0; i < p.size(); i++) {
n += near(p[i], a);
}
return n;
}
static int pathCol(vector<Pos> target, vector<Pos> p)
{
int n = 0;
for (auto it = target.end() - 1; it - target.begin() >= 0; it--) {
if (!avail(*it, g_mapEntities) || g_mapEntities[it->x][it->y] != g_col) {
continue;
}
for (auto it2 = p.end() - 1; it2 - p.begin() >= 0; it2--) {
if (!samePos(*it, *it2)) {
continue;
}
for (int i = it2 - p.begin(); i >= 0; i--) {
n += (g_mapEntities[p[i].x][p[i].y] == g_col);
}
break;
}
break;
}
return n;
}
// 求最短路径,返回路径长度,0表示不动,-1表示无解
int getPath(vector<Pos> target, int &num)
{
vector<Pos> tmp;
int ret = getPath(target, tmp);
num = pathNear(tmp, g_myTeam.campPos) + pathCol(target, tmp);
return ret;
}
这里面有2个打分,一个是athNear(tmp, g_myTeam.campPos) 表示经过基地旁边,一个是pathCol(target, tmp)表示经过同色矿却不踩的矿堆数目,其中target是目标矿堆,tmp是搜出的路径。
这2个打分都是用来校准路径的。
还有2个策略,想到了,但是没细想:
- 路径上已经有矿堆的,自动添加到路径。
- 选好点之后,重排or全局路径搜索。
4,掠夺者调度
1,能量方案
掠夺者需要权衡的是:如果有两个方案,一个是偷对方能量x但是无法给自己,一个是偷对方y能量可以给自己,x比y大,那么两个方案如何抉择?
最终我采用的衡量标准是,当x=2y的时候就算两个方案等价。
2,路径规划
寻路这块,掠夺者也是一样的,两点寻路,多点寻路。
但是掠夺者可选的目标位置比较少,而且需要得到最优解,隔一点点就直接隔5个能量了。
所以,在多点寻路的算法之外,还要套一层枚举机制。
首先把路径分为两大段,第一段是涉及能量的,第二段是不涉及能量,直接回到某个位置。
第一段,要么是偷对方能量就结束,要么是偷完还要回到自己机器人旁边。
这里,我引入了一个标识机制:g_posFlag,这是一个超脱棋盘外的点,当寻路遇到g_posFlag的时候,表示寻路已经结束。
第一层的枚举,枚举第一段的终点,要么是我方机器人旁边四个点,要么是g_posFlag表示不需要把能量送回来。
准确的说是在不需要把能量送回来的情况下有可能偷得敌人更多能量,所以通过打分pk看哪个情况最好。
第二层的枚举,就是把第一层枚举的这个终点,和敌方机器人旁边的四个点进行排序,枚举所有排序,如果遇到了不可达点或者第一层枚举的终点,那就是到第一段的终点了。
static void setPurchaseLooterCommand(string s)
{
PurchaseCommand p;
p.pos = g_myTeam.campPos;
p.purchaseType = s;
g_purchaseCommands.push_back(p);
}
static bool hasLooter()
{
return !samePos(g_myTeam.looter.pos, Pos { 0, 0 });
}
static void purchaseLooter()
{
if (!hasLooter()) {
setPurchaseLooterCommand("BUY_LOOTER");
}
}
static int looterPlay(vector<Pos> target)
{
g_path.resize(1);
g_path[0] = g_myTeam.looter.pos;// 其实target[0]也是这个东西,代码写的太差了,大量逻辑冗余
vector<Pos> tmp;
int ans = 0;
for (unsigned int i = 1; i < target.size(); i++) {
int ret = getPath(target[i - 1], target[i], g_mapEntities, tmp);// 分段寻路
if (ret == -1) {
goto here;
}
ans++; // 简单打分统计,每得一份能量加一分
if (near(target[i], g_myTeam.worker.pos)) {// 能量如果送给机器人,打分翻倍
ans *= 2;
}
if (samePos(target[i], g_myTeam.campPos)) {// 经过自己的基地不太好
ans = ans / 2 - 1;
}
killMap(tmp, g_mapEntities);
g_path = join(g_path, tmp);
}
here:
for (int i = 0; i < M; i++) {
for (int j = 0; j < M; j++) {
g_mapEntities[i][j] = g_mapEntities2[i][j];
}
}
return ans;
}
static int looterPlay(Pos start, Pos end)
{
vector<Pos> v0(5);
for (int i = 0; i < 4; i++) {
v0[i] = getPos(g_enemyTeam.worker.pos, i * 2);// 敌人的四个邻居和end混合排序
}
v0[4] = end;
int ret, ans = 0;
vector<Pos> ansv;
for (int i = 0; i < M; i++) {
for (int j = 0; j < M; j++) {
g_mapEntities2[i][j] = g_mapEntities[i][j];
}
}
vector<Pos> v(6);
v[0] = start;
int num[] = { 0, 1, 2, 3, 4 };
do {
for (int i = 0; i < 5; i++) {
v[i + 1] = v0[num[i]];
}
ret = looterPlay(v);
if (ans < ret) {
ans = ret;
ansv = v;
}
} while (next_permutation(num, num + 5));// 枚举排序
return looterPlay(ansv);
}
static void looterPlay()
{
vector<Pos> v(5);
for (int i = 0; i < 4; i++) {
v[i] = getPos(g_myTeam.worker.pos, i * 2); // 先到敌人机器人旁边,再以我方机器人邻居为终点
}
v[4] = g_posFlag;
int ans = 0, k = 0, ret;
for (int i = 0; i < 5; i++) {
ret = looterPlay(g_myTeam.looter.pos, v[i]);
if (ans < ret) {
ans = ret;
k = i;
}
}
looterPlay(g_myTeam.looter.pos, v[k]);
killMap(g_path, g_mapEntities);
vector<Pos> tmp;
Pos p;
for (int i = 0; i < 4; i++) {
p = getPos(g_enemyTeam.campPos, i * 2);
ret = getPath(*(g_path.end() - 1), p, g_mapEntities, tmp);// 最后再走到敌人基地旁边
if (ret != -1) {
break;
}
}
g_path = join(g_path, tmp);
}
5,购买策略
本来是有策略的,但是有bug,懒得调了,直接怼上100个购买能量命令。
static void purchasePower()
{
for (int i = 0; i < 100; i++) {
setPurchaseLooterCommand("BUY_POWER");
}
}
6,地图地形策略
主要有三种策略的攻击和防守:
- 堵对方基地
- 堵地图通道
- 占矿
考虑到堵对方基地,掠夺者一般都会停到对方基地旁边,机器人没弄这块。
堵地图通道是指,分析一些关键的豁口,堵住之后让敌人不能行动,或者不能挖矿,或者需要绕远路。
难度较大,也没有时间投入,这块没弄。
占矿意义不大,也没弄。
7,其他策略
异常5次结束游戏,因为无法知道自己已经异常多少次了,而且异常的惩罚也很重,所以这个不能用做策略。
最后贴上主框架main函数:
static void init(string input)
{
memset(g_mapEntities, WALL, sizeof(g_mapEntities));
g_posFlag.x = -2, g_posFlag.y = -2;
parseJson(input);
}
static bool isLooter()
{
return g_turn == g_myTeam.looter.playerName;
}
int main(int argc, char *argv[])
{
string command(argv[1]);
string input(argv[2]);
string output(argv[3]);
if (command == "-game") {
init(input);
if (isLooter()) {
looterPlay();
} else {
play();
purchaseLooter();
purchasePower();
}
writeJson(output);
} else if (command == "-compress") {
compress(input, output);
} else if (command == "-decompress") {
decompress(input, output);
}
return 0;
}
八,压缩算法
1,首先查阅资料
压缩算法对比 https://blog.youkuaiyun.com/zero__007/article/details/79782846
GZIP压缩原理分析 https://blog.youkuaiyun.com/jison_r_wang
ZIP压缩算法详细分析及解压实例解释 https://www.cnblogs.com/esingchan/p/3958962.html
无损数据压缩算法的历史 https://blog.youkuaiyun.com/kimylrong/article/details/39405981
2,挑选压缩
需要找一个简单的,自己能写出来的,但是压缩率又达标的压缩算法。
Huffman 压缩比2:3
LZW 压缩比2:4
deflate 压缩比2:5
LZMA 压缩比2:6
PPMD 压缩比2:7
PAQ 压缩比2:8
压缩算法,按照我的理解,可以分为三大块知识:直接编码、转换、上下文编码
详细分析参见我的博客:https://blog.youkuaiyun.com/nameofcsdn/article/details/109391616
最终,我选择了LZW压缩算法。
3,LZW压缩算法
https://blog.youkuaiyun.com/nameofcsdn/article/details/109324021
九,时间控制
我的算法都是贪心的,所以很快,不涉及时间控制。