P5380 鸭棋 题解
normalpcer · · 题解
P5380 鸭棋 题解
题意简述
一款在中国象棋基础上制作的游戏“鸭棋”,把棋子“炮”替换为了“鸭”。其中,“鸭”的移动规则是沿着
接下来,给出
- 这一步移动的棋子,输出所处阵营和棋子类型,例如
red horse。 - 这一步吃掉的棋子。
- 这一步后是否构成“将军”。
- 这一步后游戏是否结束。
如果不合法,输出 Invalid command。
其中,“不合法”的情况包括:
- 没有选中或者选中敌方棋子。
- 试图攻击己方棋子。
- 试图移动到棋盘外。
- 游戏已经结束。
一方的王(将帅)被吃掉则游戏结束,一步之内可以使游戏结束被称为“将军”。特别地,王被吃掉的那一轮不能算作将军。
题目分析
本题数据范围很小,按照题意模拟即可。主要包括以下步骤:
- 选中棋子。
- 判断合法性。
- 移动棋子。
- 判断是否将军和游戏是否结束。
这篇题解将会在判断合法性时采用较为新颖的方法,可以优化编码体验。同时,也会带有一定的面向对象编程的思想。对于模拟题,我的代码通常以追求可读性为主。
接下来按照我会大致按照自己的编码顺序,从大体框架开始介绍。
大体框架
对于棋盘的存储,我选择了直接存储所有棋子及其位置信息。
考虑把棋子抽象成一个类。那么,它需要包括三个属性:类型,位置和队伍。进一步,位置包含
接下来就可以一层一层地定义了。向量类:
struct Vec2d {
int x, y;
Vec2d operator+(const Vec2d &other) const { return {x+other.x, y+other.y}; } // 加
Vec2d operator-(const Vec2d &other) const { return {x-other.x, y-other.y}; } // 减
Vec2d operator*(const int other) const { return {x*other, y*other}; } // 数乘
Vec2d operator/(const int other) const { return {x/other, y/other}; } // 乘以 1/other
int operator*(const Vec2d &other) const { return x*other.x + y*other.y; } // 数量积
int cross(const Vec2d &other) const { return x*other.y - y*other.x; } // 叉乘的模长
bool operator==(const Vec2d &other) const { return x==other.x and y==other.y; } // 相等
bool operator!=(const Vec2d &other) const { return x!=other.x or y!=other.y; } // 不等
int sqrDistance(const Vec2d &other) const { // 两点间欧几里得距离的平方
return (x-other.x)*(x-other.x)+(y-other.y)*(y-other.y);
}
int manhattan(const Vec2d &other) const { return abs(x-other.x)+abs(y-other.y); } // 曼哈顿距离
int chebyshev(const Vec2d &other) const { return std::max(abs(x-other.x), abs(y-other.y)); } // 切比雪夫距离
int sqrAbs() const { return x*x+y*y; } // 模长的平方
};
这里,我们实现了两点间的三种距离也是为下文做准备。叉乘的结果是一个三维向量,这里为了方便直接返回模长。对于欧几里得距离(和模长),我们省略了开平方操作(这会引入浮点数的误差),直接采用开方前的结果。
阵营枚举:
enum Team {
Red,
Blue,
};
按照定义顺序,Team::Red 和 Team::Blue 分别为 0 和 1。所以我们可以直接使用“异或”操作进行阵营的轮换。
接下来我们定义棋子类和棋子列表。
struct Piece; // 预先声明,解决循环引用问题
vector<Piece> pieces;
struct Piece {
string name; // 棋子类型,如 horse, elephant
Vec2d pos; // 棋子位置
Team team; // 棋子阵营
bool reachable(Vec2d dest) { // 能否到达目标地点
// To be done.
}
};
这里有一个小问题,后面我们在实现 reachable 方法的时候会用到 pieces 数组,所以采用了预先声明的办法解决循环引用问题。当然,也可以在类外定义 reachable 方法。
绝大多数定义在这一步就完成了。
判断合法性
我们先不考虑其他因素,只处理两点的位置和“别脚”问题。
对于相对位置,我们使用上文的三种距离筛选。我们先简要介绍一下这三种距离。
对于两个点
感性地理解,曼哈顿距离下的“圆”是一个斜着的正方形,切比雪夫距离下的“圆”是一个正置的正方形。
接下来,我们用
- 王:
d=1 - 士:
m=2, c=1 (或者d=\sqrt{2} ) - 象:
m=4, c=2 (或者d=2\sqrt{2} ) - 马:
d=\sqrt{5} - 车:
m=c\neq 0 - 鸭:
d=\sqrt{13} - 兵:
c=1
接下来处理“别脚”的情况。这里我们就用到向量筛选。我们设
对于象,不难发现
接下来考虑别马脚。注意到,别马脚的子距离起点长度一定为
红色直线上的点符合数量积要求,蓝色圆上的点符合模长要求,同时由于两个轴上的坐标都是整数,所以点
对于鸭子,有了处理别马脚的经验,也能很快的想出结论。第一种可能,
对于车,要求中间没有挡道的棋子。不难想到,如果一个棋子要“挡道”,需要符合以下条件:
使用向量运算描述一下,即为以下条件:
最后,再检测一下落点是否在棋盘上,以及是否在攻击己方棋子就可以了。
代码如下:
struct Piece {
// ...
// 检查是否可以到达目标位置,但是只对位置进行判断
bool reachable_pos(Vec2d dest) {
if (name == "captain") {
return pos.sqrDistance(dest) == 1;
} else if (name == "guard") {
return pos.manhattan(dest) == 2 and pos.chebyshev(dest) == 1;
} else if (name == "elephant") {
return pos.manhattan(dest) == 4 and pos.chebyshev(dest) == 2 and
not pieces.some(lambda(p, (p.pos-pos)*2==(dest-pos)));
} else if (name == "horse") {
return pos.sqrDistance(dest) == 5 and
not pieces.some(lambda(p,
p.pos.sqrDistance(pos) == 1 and (p.pos-pos)*(dest-pos)==2));
} else if (name == "car") {
auto a = dest-pos;
return pos.chebyshev(dest) == pos.manhattan(dest) and not pieces.some([&](auto p) {
auto b = p.pos-pos;
return b.cross(a)==0 and b.sqrAbs()<a.sqrAbs() and b*a>0;
});
} else if (name == "soldier") {
return pos.chebyshev(dest) == 1;
} else { // duck
auto a = dest-pos;
return pos.sqrDistance(dest) == 13 and not pieces.some([&](auto p) {
auto b = p.pos-pos;
return (a*b==8 and b.sqrAbs()==5) or (a*b==3 and b.sqrAbs()==1);
});
}
}
bool reachable(Vec2d dest) { // 能否到达目标地点
return 0<=dest.x and dest.x<_X and
0<=dest.y and dest.y<_Y and
reachable_pos(dest) and
not pieces.some(lambda(p, p.pos==dest and p.team==team));
}
}
这里为了方便,我继承了 std::vector,即代码中的 vector,并添加了 some(func) 方法,用于判断是否存在任意一个元素 i,使得 func(i) 返回真。
template <typename T> struct vector: public std::vector<T> {
template <class Func>
bool some(const Func&& f) {
for (auto& i: *this) {
if (f(i)) return true;
}
return false;
}
};
而 lambda 是一个宏,定义如下:
#define lambda(arg, expr) [&](auto arg){return expr;}
其他流程
这里就不多加阐述了,可以见代码注释
void solve() {
init();
// 双方的王
Piece& kingRed = pieces[0];
Piece& kingBlue = pieces[1];
bool round = 0;
bool gameOver = false;
from(_, 1, Q) {
if (gameOver) { // 游戏结束一定不合法
cout << "Invalid command\n";
continue;
}
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
auto src = Vec2d{x1, y1}; // 起点
auto dest = Vec2d{x2, y2}; // 终点
auto selected = std::find_if(pieces.begin(), pieces.end(),
lambda(p, p.pos==src and p.team==round)); // 筛选,要求位置一致并且阵营正确
if (selected == pieces.end()) { // 没找到
cout << "Invalid command\n";
continue;
}
bool available = selected->reachable(dest); // 判断终点位置是否可达
if (not available) {
cout << "Invalid command\n"; continue;
}
// 主语
cout << (selected->team==Team::Red? "red ": "blue ") << selected->name << ';';
// 宾语
bool isKingKilled = false; // 王被击杀,判断游戏结束
auto attackedIter = std::find_if(pieces.begin(), pieces.end(), lambda(p, p.pos==dest));
selected->pos = dest;
if (attackedIter == pieces.end()) {
cout << "NA;"; // 移到空格子
} else {
isKingKilled = (attackedIter->name=="captain"); // 游戏结束
// 输出宾语
cout << (attackedIter->team==Team::Red? "red ": "blue ") << attackedIter->name << ';';
pieces.erase(attackedIter); // 移除被吃的子
}
bool isKingInDanger = false; // 被将军
isKingInDanger &= !isKingKilled; // 特判,王已经死了
isKingInDanger |= pieces.some(lambda(p, p.reachable(kingRed.pos) and p.team==Team::Blue));
isKingInDanger |= pieces.some(lambda(p, p.reachable(kingBlue.pos) and p.team==Team::Red));
cout << (isKingInDanger? "yes;": "no;") << (isKingKilled? "yes": "no") << '\n';
gameOver |= isKingKilled;
round ^= 1; // 当且仅当操作合法,进行轮换
}
}
初始化
初始化主要用于处理棋子的初始位置。
我采用了一些宏定义简化代码。宏定义有时可以减少枯燥乏味的代码,不过也要慎用,避免出现一些很难调的 bug。
int Q;
void init() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
// 添加一个子
#define add(name,px,py,team) pieces.push_back(Piece{name, {px, py}, Team::team});
// 添加四个中心对称的子
#define four(name,dx,dy) add(name,4-dx,4+dy,Red) add(name,4-dx,4-dy,Red) \
add(name,5+dx,4-dy,Blue) add(name,5+dx,4+dy,Blue)
// 添加底线上的一个子
#define bottom(name,dy) four(name,4,dy);
add("captain",0,4,Red) add("captain",9,4,Blue)
bottom("car",4)bottom("horse",3)bottom("elephant",2)bottom("guard",1)
four("duck",2,4)
four("soldier",1,4)four("soldier",1,2)add("soldier",3,4,Red)add("soldier",6,4,Blue)
cin >> Q;
}
那么这个代码就完成了。
警示后人
在写这个代码时,我被一个引用造成的 bug 卡了四个小时,原因是我在引用了 vector 上的一个元素之后,删除了另一个元素,导致内存上这个位置发生了变化,于是这个引用指向了其他元素。引用其实也是指向一个内存上的地址,和指针一样,所以不要掉以轻心,尤其是对可变容器中一个对象的引用,更要万分小心。(例如,vector.push_back 看上去人畜无害,但是如果触发了扩容,这个 vector 上的所有引用都会失效)
总结
本文中,我们采用了一些数学方法转换和简化了检查棋子的过程,感觉思路比较新颖,希望管理能够通过。
模拟题最重要的是对于题意的理解和保持思路的清晰。不过,这么长的代码,如果出现 bug,调起来还是很头疼的,所以还是要细心一些,并且记录一些平时遇到的 bug,在以后的代码中也多注意这些问题。
在文化课中学习向量时,我有时想,“这么抽象的东西到底有什么用”,直到编写这份代码的时候,我鬼使神差地给两个位置做了减法——这就是向量。于是,那些尘封在课本中的知识涌入脑海,前文的那些步骤也很自然了。把这些冗长的规则,都转变成了一个个简短的式子,也许这就是数学的魅力吧。
不知不觉也写了不少了,感谢您的观看。
完整代码
/**
* @link https://www.luogu.com.cn/problem/P5380
*/
#include <bits/stdc++.h>
#define from(i,b,e) for(int i=(b);i<=(e);i++)
#define lambda(arg, expr) [&](auto arg){return expr;}
using std::string, std::cin, std::cout;
template <typename T> struct vector: public std::vector<T> {
template <class Func>
bool some(const Func&& f) {
for (auto& i: *this) {
if (f(i)) return true;
}
return false;
}
};
const int _X = 10;
const int _Y = 9;
struct Vec2d {
int x, y;
Vec2d operator+(const Vec2d &other) const { return {x+other.x, y+other.y}; } // 加
Vec2d operator-(const Vec2d &other) const { return {x-other.x, y-other.y}; } // 减
Vec2d operator*(const int other) const { return {x*other, y*other}; } // 数乘
Vec2d operator/(const int other) const { return {x/other, y/other}; } // 乘以 1/other
int operator*(const Vec2d &other) const { return x*other.x + y*other.y; } // 数量积
int cross(const Vec2d &other) const { return x*other.y - y*other.x; } // 叉乘的模长
bool operator==(const Vec2d &other) const { return x==other.x and y==other.y; } // 相等
bool operator!=(const Vec2d &other) const { return x!=other.x or y!=other.y; } // 不等
int sqrDistance(const Vec2d &other) const { // 两点间欧几里得距离的平方
return (x-other.x)*(x-other.x)+(y-other.y)*(y-other.y);
}
int manhattan(const Vec2d &other) const { return abs(x-other.x)+abs(y-other.y); } // 曼哈顿距离
int chebyshev(const Vec2d &other) const { return std::max(abs(x-other.x), abs(y-other.y)); } // 切比雪夫距离
int sqrAbs() const { return x*x+y*y; } // 模长的平方
};
enum Team {
Red,
Blue,
};
struct Piece; // 预先声明,解决循环引用问题
vector<Piece> pieces;
struct Piece {
string name; // 棋子类型,如 horse, elephant
Vec2d pos; // 棋子位置
Team team; // 棋子阵营
// 检查是否可以到达目标位置,但是只对位置进行判断
bool reachable_pos(Vec2d dest) {
if (name == "captain") {
return pos.sqrDistance(dest) == 1;
} else if (name == "guard") {
return pos.manhattan(dest) == 2 and pos.chebyshev(dest) == 1;
} else if (name == "elephant") {
return pos.manhattan(dest) == 4 and pos.chebyshev(dest) == 2 and
not pieces.some(lambda(p, (p.pos-pos)*2==(dest-pos)));
} else if (name == "horse") {
return pos.sqrDistance(dest) == 5 and
not pieces.some(lambda(p,
p.pos.sqrDistance(pos) == 1 and (p.pos-pos)*(dest-pos)==2));
} else if (name == "car") {
auto a = dest-pos;
return pos.chebyshev(dest) == pos.manhattan(dest) and not pieces.some([&](auto p) {
auto b = p.pos-pos;
return b.cross(a)==0 and b.sqrAbs()<a.sqrAbs() and b*a>0;
});
} else if (name == "soldier") {
return pos.chebyshev(dest) == 1;
} else { // duck
auto a = dest-pos;
return pos.sqrDistance(dest) == 13 and not pieces.some([&](auto p) {
auto b = p.pos-pos;
return (a*b==8 and b.sqrAbs()==5) or (a*b==3 and b.sqrAbs()==1);
});
}
}
bool reachable(Vec2d dest) { // 能否到达目标地点
return 0<=dest.x and dest.x<_X and
0<=dest.y and dest.y<_Y and
reachable_pos(dest) and
not pieces.some(lambda(p, p.pos==dest and p.team==team));
}
};
namespace Solution {
int Q;
void init() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
// 添加一个子
#define add(name,px,py,team) pieces.push_back(Piece{name, {px, py}, Team::team});
// 添加四个中心对称的子
#define four(name,dx,dy) add(name,4-dx,4+dy,Red) add(name,4-dx,4-dy,Red) \
add(name,5+dx,4-dy,Blue) add(name,5+dx,4+dy,Blue)
// 添加底线上的一个子
#define bottom(name,dy) four(name,4,dy);
add("captain",0,4,Red) add("captain",9,4,Blue)
bottom("car",4)bottom("horse",3)bottom("elephant",2)bottom("guard",1)
four("duck",2,4)
four("soldier",1,4)four("soldier",1,2)add("soldier",3,4,Red)add("soldier",6,4,Blue)
cin >> Q;
}
void solve() {
init();
// 双方的王
Piece& kingRed = pieces[0];
Piece& kingBlue = pieces[1];
bool round = 0;
bool gameOver = false;
from(_, 1, Q) {
if (gameOver) { // 游戏结束一定不合法
cout << "Invalid command\n";
continue;
}
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
auto src = Vec2d{x1, y1}; // 起点
auto dest = Vec2d{x2, y2}; // 终点
auto selected = std::find_if(pieces.begin(), pieces.end(),
lambda(p, p.pos==src and p.team==round)); // 筛选,要求位置一致并且阵营正确
if (selected == pieces.end()) { // 没找到
cout << "Invalid command\n";
continue;
}
bool available = selected->reachable(dest); // 判断终点位置是否可达
if (not available) {
cout << "Invalid command\n"; continue;
}
// 主语
cout << (selected->team==Team::Red? "red ": "blue ") << selected->name << ';';
// 宾语
bool isKingKilled = false; // 王被击杀,判断游戏结束
auto attackedIter = std::find_if(pieces.begin(), pieces.end(), lambda(p, p.pos==dest));
selected->pos = dest;
if (attackedIter == pieces.end()) {
cout << "NA;"; // 移到空格子
} else {
isKingKilled = (attackedIter->name=="captain"); // 游戏结束
// 输出宾语
cout << (attackedIter->team==Team::Red? "red ": "blue ") << attackedIter->name << ';';
pieces.erase(attackedIter); // 移除被吃的子
}
bool isKingInDanger = false; // 被将军
isKingInDanger &= !isKingKilled; // 特判,王已经死了
isKingInDanger |= pieces.some(lambda(p, p.reachable(kingRed.pos) and p.team==Team::Blue));
isKingInDanger |= pieces.some(lambda(p, p.reachable(kingBlue.pos) and p.team==Team::Red));
cout << (isKingInDanger? "yes;": "no;") << (isKingKilled? "yes": "no") << '\n';
gameOver |= isKingKilled;
round ^= 1; // 当且仅当操作合法,进行轮换
}
}
}
int main() {
Solution::solve();
return 0;
}