「联合省选 2023」过河卒
题目描述
有一个 \(n\) 行 \(m\) 列的棋盘。我们用 \((i,j)\) 表示第 \(i\) 行第 \(j\) 列的位置。棋盘上有一些 障碍,还有一个黑棋子和两个红棋子。
游戏的规则是这样的: 红方先走,黑方后走,双方轮流走棋。红方每次可以选择一个红棋子,向棋盘的相邻一格走一步。具体而言,假设红方选择的这个棋子位置在 \((i,j)\),那么它可以走到 \((i-1,j),(i+1,j),(i,j-1),(i,j+1)\) 中的一个,只要这个目的地在棋盘内且没有障碍且没有红方的另一个棋子。
黑方每次可以将自己的棋子向三个方向之一移动一格。具体地,假设这个黑棋子位置在 \((i,j)\),那么它可以走到 \((i-1,j),(i,j-1),(i,j+1)\) 这三个格子中的一个,只要这个目的地在棋盘内且没有障碍。
在一方行动之前,如果发生以下情况之一,则立即结束游戏,按照如下的规则判断胜负(列在前面的优先):
- 黑棋子位于第一行。此时黑方胜。
- 黑棋子和其中一个红棋子在同一个位置上。此时进行上一步移动的玩家胜。
- 当前玩家不能进行任何合法操作。此时对方胜。
现在假设双方采用最优策略,不会进行不利于自己的移动。也就是说:
- 若存在必胜策略,则会选择所有必胜策略中,不论对方如何操作,本方后续获胜所需步数最大值最少的操作。
- 若不存在必胜策略,但存在不论对方如何行动,自己都不会落败的策略,则会选择任意一种不败策略。
- 若不存在不败策略,则会选择在所有策略中,不论对方如何操作,对方后续获胜所需步数最小值最大的操作。
如果在 \(100^{100^{100}}\) 个回合之后仍不能分出胜负,则认为游戏平局。请求出游戏结束时双方一共移动了多少步,或者判断游戏平局。
数据范围与提示
对于所有的数据,保证:\(1 \leq T \leq 10 ; 2 \leq n \leq 10 ; 1 \leq m \leq 10 ; \text{id}\) 等于测试点编号。
对于每组数据保证:棋盘上的黑棋恰好有一个,红棋恰好有两个,且黑棋不在第一 行。
- 测试点 \(1 \sim 4\):保证要么平局,要么红方在开始时无法移动。
- 测试点 \(5 \sim 6\):保证 \(n \geq 4\) 。保证棋盘上第 \(n-1\) 行的每一个格子都是障碍物,且 棋盘上其他行没有障碍物。保证黑棋在前 \(n-2\) 行,有一个红棋在前 \(n-2\) 行,另一个红棋在第 \(n\) 行。
- 测试点 \(7 \sim 9\):保证 \(m=1\)。
- 测试点 \(10 \sim 13\):保证要么平局,要么存在策略可以在 \(9\) 步之内结束游戏。
- 测试点 \(14 \sim 20\):无特殊限制。
题解
观察
首先发现问题规模是 \(2n^3m^3=2\cdot 10^6\) 的,边数不超过 \(1.6\cdot 10^7\)。
问题可以抽象成一般有向图博弈,关键在处理环。
手动尝试消环后能感受到,胜负基本稳定后,剩下环就都是平局的情况。
消环
- 假设「轮到红方移动的状态 \(u\) 」以及 \(u\) 能到的某一个「轮到黑方移动的状态 \(v\)」。
-
\(v\) 如果是「红方必胜态」那么 \(u\) 直接推向 \(v\),状态 \(u\) 就是「红方必胜态」。
-
否则无法直接通过 \(v\) 决定 \(u\)。
- 假设「轮到黑方移动的状态 \(u\) 」以及 \(u\) 能到的某一个「轮到红方移动的状态 \(v\)」。
-
\(v\) 如果是「红方必败态」那么 \(u\) 直接推向 \(v\),状态 \(u\) 就是「红方必败态」。
-
否则无法直接通过 \(v\) 决定 \(u\)。
-
当一个状态无法以上面两种操作确定值,但所有出边值都能确定时,可以直接确定值。
-
当以上三种都不能确定值时,一定是一整个环都不确定,此时双方都会选择在环上绕下去,因为把状态移出环不会赢,所以剩下的环全是平局。
我们通过在反向图上类 bfs 完成「消环」步骤,此时可以知道胜负态。
步数
直接类似最短路松弛的话会出现一个状态没取到最小值但另一个状态把这个值拿去作最大值的问题。
仔细观察发现红方必胜的话,倒回去的每个状态都是必胜状态,红方必败同理。
- 假设「轮到红方移动的状态 \(u\) 」以及 \(u\) 能到的某一个「轮到黑方移动的状态 \(v\)」。
-
\(v\) 如果是「红方必胜态」那么 \(u\) 直接推向 \(v\),步数直接转移过去。
因为 bfs 的过程走的是「红方必胜路径」,保证了是第一次到达的最短路径。
-
否则无法直接通过 \(v\) 决定 \(u\)。
- 假设「轮到黑方移动的状态 \(u\) 」以及 \(u\) 能到的某一个「轮到红方移动的状态 \(v\)」。
-
\(v\) 如果是「红方必败态」那么 \(u\) 直接推向 \(v\),步数直接转移过去。
因为 bfs 的过程走的是「红方必败路径」,保证了是第一次到达的最短路径。
-
否则无法直接通过 \(v\) 决定 \(u\)。
-
当一个状态无法以上面两种操作确定值,但所有出边值都能确定时,可以直接确定值和步数。
-
当以上三种都不能确定值时,一定是一整个环都不确定,此时双方都会选择在环上绕下去,因为把状态移出环不会赢,所以剩下的环全是平局。
卡常
-
预处理输入局面可能到的局面。
-
bfs 时如果确定了输入局面的值直接退出。
可以通过。
细节
仔细看题,红棋不能走到一起。
实现
没有预处理输入局面可能到的局面的代码。
#include <bits/stdc++.h>
#define ll long long
#define For(a,n) for(int a=0;a<n;++a)
using namespace std;
int tot, id[10][10][10][10][10][10][2];
constexpr int S = 2e6 + 5, E = S * 8;
bool valid[S], who[S];
int n, m;
string grid[10];
bool free(int x, int y) {
if (0 <= x && x < n && 0 <= y && y < m && grid[x][y] != '#') {
return 1;
} else {
return 0;
}
}
bool check(int a, int b, int c, int d, int e, int f, int g) {
if ((a == c && b == d) || (e == 0 && g == 1)) {
return 0;
}
if (free(a, b) && free(c, d) && free(e, f)) {
return 1;
} else {
return 0;
}
}
struct Edge {
int tot, head[S], nxt[E], to[E];
Edge() {
clear();
}
void clear() {
tot = 0;
memset(head, 0, sizeof head);
}
void add(int u, int v) {
to[++tot] = v;
nxt[tot] = head[u];
head[u] = tot;
}
} e1, e2;
int dx[4] = {0, 0, -1, 1};
int dy[4] = {-1, 1, 0, 0};
int f[S], g[S], deg[S], Q[S], front;
bool vis[S];
bool ending(int a, int b, int c, int d, int e, int f, int g) {
bool flag = 1;
if (g == 0) {
For(i, 4) {
if (free(a + dx[i], b + dy[i]) || free(c + dx[i], d + dy[i])) {
flag = 0;
break;
}
}
} else {
For(i, 3) {
if (free(e + dx[i], f + dy[i])) {
flag = 0;
break;
}
}
}
if (flag) {
return 1;
}
if ((a == e && b == f) || (c == e && d == f) || e == 0) {
return 1;
} else {
return 0;
}
}
void work() {
cin >> n >> m;
for (int i = 0; i < n; ++i) {
cin >> grid[i];
}
tot = 0;
For(a, n) {
For(b, m) {
For(c, n) {
For(d, m) {
For(e, n) {
For(f, m) {
For(g, 2) {
if (make_pair(a, b) <= make_pair(c, d)) {
id[a][b][c][d][e][f][g] = ++tot;
valid[tot] = check(a, b, c, d, e, f, g);
who[tot] = g;
} else {
id[a][b][c][d][e][f][g] = id[c][d][a][b][e][f][g];
}
}
}
}
}
}
}
}
e1.clear();
e2.clear();
For(a, n) {
For(b, m) {
For(c, n) {
For(d, m) {
For(e, n) {
For(f, m) {
if (make_pair(a, b) <= make_pair(c, d)) {
if (check(a, b, c, d, e, f, 0) && !ending(a, b, c, d, e, f, 0)) {
For(i, 4) {
int g = a + dx[i];
int h = b + dy[i];
if (check(g, h, c, d, e, f, 1)) {
e1.add(id[a][b][c][d][e][f][0], id[g][h][c][d][e][f][1]);
e2.add(id[g][h][c][d][e][f][1], id[a][b][c][d][e][f][0]);
}
g = c + dx[i];
h = d + dy[i];
if (check(a, b, g, h, e, f, 1)) {
e1.add(id[a][b][c][d][e][f][0], id[a][b][g][h][e][f][1]);
e2.add(id[a][b][g][h][e][f][1], id[a][b][c][d][e][f][0]);
}
}
}
if (check(a, b, c, d, e, f, 1) && !ending(a, b, c, d, e, f, 1)) {
For(i, 3) {
int g = e + dx[i];
int h = f + dy[i];
if (check(a, b, c, d, g, h, 0)) {
e1.add(id[a][b][c][d][e][f][1], id[a][b][c][d][g][h][0]);
e2.add(id[a][b][c][d][g][h][0], id[a][b][c][d][e][f][1]);
}
}
}
}
}
}
}
}
}
}
for (int i = 1; i <= tot; ++i) {
deg[i] = 0;
for (int j = e1.head[i]; j; j = e1.nxt[j]) {
deg[i]++;
}
}
memset(f, 0, sizeof f);
memset(g, 0, sizeof g);
memset(vis, 0, sizeof vis);
front = 0;
for (int i = 1; i <= tot; ++i) {
if (valid[i] && deg[i] == 0) {
Q[++front] = i;
int a = who[i];
if (a == 0) {
f[i] = -1;
} else {
f[i] = 1;
}
}
}
int A, B, C, D, E, F;
bool flag = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (grid[i][j] == 'O') {
if (flag == 0) {
A = i, B = j;
flag = 1;
} else {
C = i, D = j;
}
}
if (grid[i][j] == 'X') {
E = i, F = j;
}
}
}
int End = id[A][B][C][D][E][F][0];
for (int T = 1; T <= front; ++T) {
int u = Q[T];
if (vis[End]) {
break;
}
for (int i = e2.head[u]; i; i = e2.nxt[i]) {
int v = e2.to[i];
--deg[v];
if (vis[v] == 0) {
int a = who[u];
if (a == 0 && f[u] == -1) {
f[v] = -1;
g[v] = g[u] + 1;
vis[v] = 1;
Q[++front] = v;
} else if (a == 1 && f[u] == 1) {
f[v] = 1;
g[v] = g[u] + 1;
vis[v] = 1;
Q[++front] = v;
} else if (deg[v] == 0) {
int b = who[v];
if (b == 0) {
int mx = 0;
f[v] = -1;
for (int i = e1.head[v]; i; i = e1.nxt[i]) {
int w = e1.to[i];
f[v] = max(f[v], f[w]);
mx = max(mx, g[w] + 1);
}
if (f[v] == -1) {
g[v] = mx;
}
} else {
int mx = 0;
f[v] = 1;
for (int i = e1.head[v]; i; i = e1.nxt[i]) {
int w = e1.to[i];
f[v] = min(f[v], f[w]);
mx = max(mx, g[w] + 1);
}
if (f[v] == 1) {
g[v] = mx;
}
}
vis[v] = 1;
Q[++front] = v;
}
}
}
}
if (f[End] == 1) {
cout << "Red " << g[End] << "\n";
} else if (f[End] == 0) {
cout << "Tie\n";
} else {
cout << "Black " << g[End] << "\n";
}
}
int main() {
int id, t;
cin >> id >> t;
while (t--) {
work();
}
return 0;
}