猫和老鼠 I
-
DFS+DP
普通的做法是考虑步数,当步数达到某个上线maxn,就认为是平局。一般根据抽屉原理,认为上线就是2n^2,即所有的状态数。
若上线设置过大,一般会超时,但在leectcode中用2*n就够了。
因为加上步数维度后,可以把所有状态看成一个节点,连接从一个状态到下一个状态,就可形成一个有向图。
通过DFS遍历当前状态所有可到达的对手状态,若对手存在必输态,则该状态为必胜态,其次若对手存在平局态,则该状态为平局态,否则为必输态。
int dp[51][51][2][51], maxn;
class Solution {
public:
vector<vector<int>> g;
int DFS(int x, int y, int turn, int step){
if (step == maxn) return 0; //达到上线步数,平局
if (x == 0) return 1; //老鼠进洞,老鼠赢,也就是猫输
if (x == y) return 2; //猫鼠同一位置,无论谁的回合,都是猫赢
if (dp[x][y][turn][step] != -1) return dp[x][y][turn][step]; //处理过的状态不再处理
int ret = -1;
if (turn == 0){
bool win = false, draw = false;
for (auto nx : g[x]){
int res = DFS(nx, y, 1, step);
if (res == 1){
win = true;
break;
}
else if (res == 0) draw = true;
}
if (win) ret = 1;
else if (draw) ret = 0;
else ret = 2;
}
else{
bool win = false, draw = false;
for (auto ny : g[y]){
if (ny == 0) continue;
int res = DFS(x, ny, 0, step + 1);
if (res == 2){
win = true;
break;
}
else if (res == 0) draw = true;
}
if (win) ret = 2;
else if (draw) ret = 0;
else ret = 1;
}
return dp[x][y][turn][step] = ret;
}
int catMouseGame(vector<vector<int>>& G) {
g = G, maxn = G.size();
memset(dp, -1, sizeof(dp));
return DFS(1, 2, 0, 0);
}
};
-
拓扑排序+BFS
该算法应该是最正确的做法,把所有状态抽象成节点(不考虑步数),若一个状态能到另一状态则连上有向边,就可形成一幅有向图。
有向图分析:
- 若图中存在有向环,说明是平局的状态。
- 而有些状态节点时可直接得到状态的。如老鼠进洞,猫吃老鼠。这些状态节点出度为0。
状态推断:
- 对于必败态的节点,所有可到达该节点的节点状态都为必胜态。
- 对于必胜态的节点,可以删除所有可到达该节点的有向边,即让相邻节点的出度减一,当相邻节点的出度为0且未处理过时,则相邻节点状态为必败态。
算法步骤:
- 计算所有状态节点的出度(即该状态节点可到达所有状态节点数量)。
- 把所有确定的状态放入队列。(1)猫鼠同一位置时,猫必胜。(2)老鼠进洞时,猫任意位置,老鼠必胜。
- 队列不断弹出,若为弹出结点状态为必败态,将相邻结点置为必胜态,并放入队列。若弹出结点状态为必胜态,将相邻结点的出度减一,当相邻结点出度为0且未处理过时,将相邻结点状态置为必败态,并放入队列。
- 处理完后,答案就在dp[1][2][0],意思为鼠在1,猫在2,鼠先手的状态,初始状态为0,若到最后都未处理过,就是平局0。
int dp[51][51][2], in[51][51][2];// dp[鼠位置][猫位置][0鼠先手], in 每个状态的出度
class Solution {
public:
int catMouseGame(vector<vector<int>>& g) {
int n = g.size();
memset(dp, 0, sizeof(dp)); //初始状态为0,若到最后都未处理过则为平局0.
for (int i = 0; i < n; ++i)//谁先手,就谁操作,出度为先手者能到达的下个状态的数量
for (int j = 1; j < n; ++j)//注意猫不能进洞,故要去掉0的位置
in[i][j][0] = g[i].size(), in[i][j][1] = g[j].size() - (g[j][0] == 0);
queue<tuple<int, int, int>> q;
for (int i = 1; i < n; ++i){//同一位置时,猫主动抓到老鼠,或老鼠送上门。
dp[i][i][0] = 2, dp[i][i][1] = 2, dp[0][i][1] = 1;//老鼠进洞后,必然是猫操作
q.emplace(i, i, 0), q.emplace(i, i, 1), q.emplace(0, i, 1);
}
while (!q.empty()){
auto [m, c, op] = q.front(); q.pop();
if (op == 1){ //该状态是猫先手
for (auto nm : g[m]){ //说明上个状态是老鼠先手
--in[nm][c][op ^ 1]; //减少相邻节点出度
if (dp[nm][c][op ^ 1]) continue; //!!!处理过,切记要跳过!!!!
if (dp[m][c][op] == 1){ //若为必败态(猫先手,却鼠赢,故必败态)
dp[nm][c][op ^ 1] = 1; //相邻节点为必胜态(相邻节点,鼠先手,鼠赢)
q.emplace(nm, c, op ^ 1);
}
else if (in[nm][c][op ^ 1] == 0){ //出度为0且未处理,必败态
dp[nm][c][op ^ 1] = 2; //相邻节点,鼠先手,必败态即是猫赢
q.emplace(nm, c, op ^ 1);
}
}
}
else{ //处理逻辑类似
for (auto nc : g[c]){
if (nc == 0) continue; //千万要注意猫不能去0位置,即洞里
--in[m][nc][op ^ 1];
if (dp[m][nc][op ^ 1]) continue;
if (dp[m][c][op] == 2){
dp[m][nc][op ^ 1] = 2;
q.emplace(m, nc, op ^ 1);
}
else if (in[m][nc][op ^ 1] == 0){
dp[m][nc][op ^ 1] = 1;
q.emplace(m, nc, op ^ 1);
}
}
}
}
return dp[1][2][0];
}
};
猫和老鼠 II
-
DFS+DP
处理方式和 猫和老鼠 I 类似,只是多了些预处理步骤,不过多解释了。
int dp[8][8][8][8][2][80]; //dp[鼠x][鼠y][猫x][猫y][0鼠先手][回合数]
class Solution {
public:
int dir[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
int n, m, fx, fy, md, cd, MAXN; //fx,fy实物位置。md,cd鼠猫能跳的最远距离。maxn设定最大步数
vector<string> g;
int DFS(int mx, int my, int cx, int cy, int turn, int step){
if (step == MAXN) return 0; // 步数到达上线平局
if (turn == 1 && mx == fx && my == fy) return 0; //鼠主动吃食,到猫回合,猫输
if (turn == 0 && cx == fx && cy == fy) return 0; //猫主动吃食,到鼠回合,鼠输
if (turn == 0 && mx == cx && my == cy) return 0; //猫主动吃鼠,到鼠回合,鼠输
if (turn == 1 && mx == cx && my == cy) return 1; //鼠送给猫吃,到猫回合,猫输
if (dp[mx][my][cx][cy][turn][step] >= 0) //处理过直接返回
return dp[mx][my][cx][cy][turn][step];
int ret = 0;
if (turn == 0){//鼠回合
if (DFS(mx, my, cx, cy, 1, step) == 0) ret = 1;//鼠停留原地,下状态猫输,则鼠赢
else{
for (int i = 0; !ret && i < 4; ++i)
for (int d = 1; !ret && d <= md; ++d){
int nx = mx + dir[i][0] * d, ny = my + dir[i][1] * d;
if (nx < 0 || nx >= n || ny < 0 || ny >= m || g[nx][ny] == '#') break;
if (DFS(nx, ny, cx, cy, 1, step) == 0) ret = 1;
}
}
}
else{
if (DFS(mx, my, cx, cy, 0, step + 1) == 0) ret = 1;
else{
for (int i = 0; !ret && i < 4; ++i)
for (int d = 1; !ret && d <= cd; ++d){
int nx = cx + dir[i][0] * d, ny = cy + dir[i][1] * d;
if (nx < 0 || nx >= n || ny < 0 || ny >= m || g[nx][ny] == '#') break;
if (DFS(mx, my, nx, ny, 0, step + 1) == 0) ret = 1;
}
}
}
return dp[mx][my][cx][cy][turn][step] = ret;
}
bool canMouseWin(vector<string>& G, int Cd, int Md) {
g = G;
memset(dp, -1, sizeof(dp));
n = g.size(), m = g[0].size(), cd = Cd, md = Md, MAXN = n * m + 3;
int mx, my, cx, cy;
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
if (g[i][j] == 'M')
mx = i, my = j;
else if (g[i][j] == 'C')
cx = i, cy = j;
else if (g[i][j] == 'F')
fx = i, fy = j;
return DFS(mx, my, cx, cy, 0, 1);
}
};
-
拓扑排序+BFS
操作很复杂,需要预处理很多东西,代码详尽解释。
int dp[8][8][8][8][2], in[8][8][8][8][2], step[8][8][8][8][2];
//dp[鼠x][鼠y][猫x][猫y][0鼠先手], in 对应状态的出度
vector<pair<int, int>> neighbor[8][8][2]; //neighbor为猫鼠在各个位置,能到达的下个位置的集合
class Solution {
private:
static constexpr int dir[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
public:
bool canMouseWin(vector<string>& g, int cj, int mj) {
int n = g.size(), m = g[0].size(), MX, MY, CX, CY, fx, fy;
//MX, MY, CX, CY初始鼠猫位置,fx, fy食物位置
memset(dp, -1, sizeof(dp));
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
if (g[i][j] == 'M')
MX = i, MY = j;
else if (g[i][j] == 'C')
CX = i, CY = j;
else if (g[i][j] == 'F')
fx = i, fy = j;
//获得相邻节点
auto getNeighbors = [&](int x, int y, int bound) -> vector<pair<int, int>>{
vector<pair<int, int>> res = {{x, y}};
for (int i = 0; i < 4; ++i)
for (int d = 1; d <= bound; ++d){
int nx = x + dir[i][0] * d, ny = y + dir[i][1] * d;
if (nx < 0 || nx >= n || ny < 0 || ny >= m || g[nx][ny] == '#') break;
res.emplace_back(nx, ny);
}
return res;
};
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j){//保存猫鼠在各个位置,能到达的下个位置的集合
if (g[i][j] == '#') continue;
neighbor[i][j][0] = getNeighbors(i, j, mj);
neighbor[i][j][1] = getNeighbors(i, j, cj);
}
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j){
if (g[i][j] == '#') continue;
for (int k = 0; k < n; ++k)
for (int l = 0; l < m; ++l){
if (g[k][l] == '#') continue;//出度!出度!出度!出度!
in[i][j][k][l][0] = neighbor[i][j][0].size();//鼠操作,鼠可到达的下个位置数
in[i][j][k][l][1] = neighbor[k][l][1].size();//猫操作,猫可到达的下个位置数
}
}
queue<tuple<int, int, int, int, int>> q;
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j){
if (g[i][j] != '#' && g[i][j] != 'F'){
dp[i][j][i][j][0] = 0; //猫主动吃鼠,到鼠回合,鼠输
dp[i][j][i][j][1] = 1; //鼠送给猫吃,都猫回合,猫赢
q.emplace(i, j, i, j, 0);
q.emplace(i, j, i, j, 1);
dp[fx][fy][i][j][1] = 0; //鼠主动吃食,到猫回合,猫输
dp[i][j][fx][fy][0] = 0; //猫主动吃食,到鼠回合,鼠输
q.emplace(fx, fy, i, j, 1);
q.emplace(i, j, fx, fy, 0);
}
}
while (!q.empty()){
auto [mx, my, cx, cy, turn] = q.front(); q.pop();
if (turn == 1){//该状态是猫操作,说明上步是鼠操作而来
for (auto [nx, ny] : neighbor[mx][my][turn ^ 1]){
--in[nx][ny][cx][cy][turn ^ 1]; //相邻节点出度减1
if (dp[nx][ny][cx][cy][turn ^ 1] != -1) continue; //!!处理过记得跳过!!
if (dp[mx][my][cx][cy][turn] == 0){//猫回合,猫输,上状态鼠回合,鼠必赢
dp[nx][ny][cx][cy][turn ^ 1] = 1;
q.emplace(nx, ny, cx, cy, turn ^ 1);
}
else if (in[nx][ny][cx][cy][turn ^ 1] == 0){
dp[nx][ny][cx][cy][turn ^ 1] = 0; //出度为0,且未处理过,必输
q.emplace(nx, ny, cx, cy, turn ^ 1);
}
}
}
else{//处理逻辑类似
for (auto [nx, ny] : neighbor[cx][cy][turn ^ 1]){
--in[mx][my][nx][ny][turn ^ 1];
if (dp[mx][my][nx][ny][turn ^ 1] != -1) continue;
if (dp[mx][my][cx][cy][turn] == 0){
dp[mx][my][nx][ny][turn ^ 1] = 1;
q.emplace(mx, my, nx, ny, turn ^ 1);
}
else if (in[mx][my][nx][ny][turn ^ 1] == 0){
dp[mx][my][nx][ny][turn ^ 1] = 0;
q.emplace(mx, my, nx, ny, turn ^ 1);
}
}
}
}
return dp[MX][MY][CX][CY][0] == 1;
}
};