LeetCode 913. Cat and Mouse
核心:BFS + 记忆化(标程) / 极大极小搜索 + alpha-beta剪枝(近似解)
URL:【LeetCode 913】cat-and-mouse
Difficulty : Hard Discuss (32)
ACCEPTED 1,341 SUBMISSIONS 6,005
Description :
A game on an undirected graph is played by two players, Mouse and Cat, who alternate turns.
The graph is given as follows: graph[a]
is a list of all nodes b
such that ab
is an edge of the graph.
Mouse starts at node 1 and goes first, Cat starts at node 2 and goes second, and there is a Hole at node 0.
During each player's turn, they must travel along one edge of the graph that meets where they are. For example, if the Mouse is at node 1
, it must travel to any node in graph[1]
.
Additionally, it is not allowed for the Cat to travel to the Hole (node 0.)
Then, the game can end in 3 ways:
- If ever the Cat occupies the same node as the Mouse, the Cat wins.
- If ever the Mouse reaches the Hole, the Mouse wins.
- If ever a position is repeated (ie. the players are in the same position as a previous turn, and it is the same player's turn to move), the game is a draw.
Given a graph
, and assuming both players play optimally, return 1
if the game is won by Mouse, 2
if the game is won by Cat, and 0
if the game is a draw.
Example 1:
Input: [[2,5],[3],[0,4,5],[1,4,5],[2,3],[0,2,3]]
Output: 0
Explanation:
4---3---1
| |
2---5
\ /
0
Note:
- 3 <= graph.length <= 50
- It is guaranteed that graph[1] is non-empty.
- It is guaranteed that graph[2] contains a non-zero element.
Analysis & AC code:
一、题目大意
- 输入一个无向图代表迷宫,然后迷宫里面有一只猫和一只老鼠
- 老鼠的初始位置是顶点1,猫的初始位置是顶点2
- 猫和老鼠轮流沿着图的边移动(老鼠先移动)
- 猫移动到老鼠所在的格子则猫获胜;老鼠移动到顶点0则老鼠胜
- 假设双方都足够聪明,问最终是猫获胜还是老鼠获胜还是陷入僵局
二、思路产生
要解决这道题,首先要理解“足够聪明”这个词。也就是说老鼠和猫都有能力“穷举”所有未来的情况,然后选择对自己最好的决策。这给了我们一个思路提示,也就是:
我们应该从最终的胜利局面为基础,一步步倒推,得到某些中间过程也是必胜局面,然后再以这些局面倒推,再产生其他的必胜局面,一直重复这个过程直到到无法再推出新的局面为止。至此,所有已经被推到的局面的集合就成为了猫或老鼠的必胜局面集合,而未被推到的局面集合(也就是必胜局面集合对于全集的补集)就是僵持局面集合。我们只要看我们输入的局面属于哪个集合,就可以知道这个局面接下来会“足够聪明地”发展成为老鼠胜、猫胜还是僵持。
这个“不断倒推/扩展”其实也就是BFS。然后为了表示所有局面(一个局面由老鼠位置、猫位置、当前轮到老鼠移动还是猫移动这三个量唯一确定),我们还需要开一个三维数组分别表示这三个量。这样也就可以进行记忆化搜索。
三、初始化和搜索规则
- ①【初始化】
- 把老鼠在0且猫不在0的所有局面标记为老鼠的必胜局面;把猫鼠都在非0点的所有局面标记为猫的必胜局面
- 把其他局面标记为僵持局面(可以默认用0表示)
- 标记必胜局面的同时把它们入队
- 计算一下由局面扩展关系构成的图的每个顶点(代表一个局面)的度(代表有多少个父局面可以扩展到本局面),在之后的BFS更新过程中会用到度。
- ②【BFS队列中存放的局面】
- 始终存放着猫或鼠的必胜局面
- ③【如何扩展子局面】
- 根据当前行动者是猫/鼠,扩展到当前猫/鼠所在位置的所有邻接点,同时调换当前行动者,成为子局面
- ④【何时停止搜索】
- 队列中最后一个局面出队,然后扩展子局面,发现其扩展出的子局面均已经访问过,说明此时已经找寻了所有可能达到的必胜局面,可以停止搜索了。
四、关键搜索过程
出队一个必胜的父局面,然后扩展其子局面。遍历子局面,对于一个不是必胜局面的子局面:
- 如果子局面当前行动者是猫:
- 如果父局面是猫的必胜局面,那么这个子局面也是猫的必胜局面。标记历史表,然后把这个子局面入队。
- 如果父局面是鼠的必胜局面,那么让子局面的度自减。自减之后如果度变成了0,说明所有可能扩展到此子局面的父局面都是鼠的必胜局面,由此可以推导出此子局面必定是猫的必败局面,也就是鼠的必胜局面。标记历史表,然后把这个局面入队(这个局面也是必胜局面,是鼠的必胜局面)。
- 如果子局面当前行动者是鼠:
- 如果父局面是鼠的必胜局面,那么这个子局面也是鼠的必胜局面。标记历史表,然后把这个子局面入队。
- 如果父局面是猫的必胜局面,那么让子局面的度自减。自减之后如果度变成了0,说明所有可能扩展到此子局面的父局面都是猫的必胜局面,由此可以推导出此子局面必定是鼠的必败局面,也就是猫的必胜局面。标记历史表,然后把这个局面入队(这个局面也是必胜局面,是猫鼠的必胜局面)。
按照上述搜索方式进行搜索,最后查记忆表即可得到答案。
这是AC代码:
class Solution
{
public:
enum PLAYER_FLAG
{
NO_PLAYER,
MOUSE,
CAT,
PLAYER_CNT
};
struct Choice
{
int mouse_pos;
int cat_pos;
int now_player;
Choice(void) { }
Choice(int mouse_pos, int cat_pos, int now_player) :
mouse_pos(mouse_pos), cat_pos(cat_pos), now_player(now_player) { }
};
struct State
{
int mouse_pos;
int cat_pos;
int now_player;
int win_player;
State(void) { }
State(int mouse_pos, int cat_pos, int now_player, int win_player) :
mouse_pos(mouse_pos), cat_pos(cat_pos),
now_player(now_player), win_player(win_player) { }
};
int V;
char winner[55][55][PLAYER_CNT]; // history
char degree[55][55][PLAYER_CNT];
State queue[12345];
int head, tail;
vector<vector<int>> edge;
void initSearch(void)
{
/* 初始化局面构成的图的度,注意这里的图不是猫鼠的路径图,是局面之间扩展关系形成的图。
* 图的顶点是一个局面
* 顶点的度表示有多少个父局面可以扩展到本局面
*/
for (int mouse_pos=0; mouse_pos<V; ++mouse_pos)
{
for (int cat_pos=0; cat_pos<V; ++cat_pos)
{
degree[mouse_pos][cat_pos][MOUSE] = edge[mouse_pos].size();
degree[mouse_pos][cat_pos][CAT] = edge[cat_pos].size();
for (int dest : edge[cat_pos])
{
if (dest == 0)
{
degree[mouse_pos][cat_pos][CAT]--;
}
}
}
}
head = tail = 0;
for (int v=0; v<V; ++v)
{
for (int now_player = MOUSE; now_player <= CAT; ++now_player)
{
winner[0][v][now_player] = MOUSE;
queue[tail++] = State(0, v, now_player, MOUSE);
if (v != 0)
{
winner[v][v][now_player] = CAT;
queue[tail++] = State(v, v, now_player, CAT);
}
}
}
}
int memSearch(void)
{
while (head != tail)
{
const State &now_state = queue[head++];
Choice ava_choices[233];
int ava_cnt = generPrevChoices(ava_choices, now_state.mouse_pos, now_state.cat_pos, now_state.now_player);
for (Choice *it=ava_choices, *E=it+ava_cnt; it!=E; ++it)
{
int prev_mouse_pos = it->mouse_pos;
int prev_cat_pos = it->cat_pos;
int prev_player = it->now_player;
if (winner[prev_mouse_pos][prev_cat_pos][prev_player] == NO_PLAYER)
{
if (prev_player == now_state.win_player) // 存在父局面是必胜局面,prev_player必胜
{
winner[prev_mouse_pos][prev_cat_pos][prev_player] = now_state.win_player;
queue[tail++] = State(prev_mouse_pos, prev_cat_pos, prev_player, now_state.win_player);
}
else
{
degree[prev_mouse_pos][prev_cat_pos][prev_player]--;
if (!degree[prev_mouse_pos][prev_cat_pos][prev_player]) // 所有可能的父局面全部都是必败局面,连拖延都没法拖延,prev_player必败
{
winner[prev_mouse_pos][prev_cat_pos][prev_player] = PLAYER_CNT - prev_player;
queue[tail++] = State(prev_mouse_pos, prev_cat_pos, prev_player, PLAYER_CNT - prev_player);
}
}
}
}
}
return winner[1][2][MOUSE];
}
int generPrevChoices(Choice *avaChoices, int mouse_pos, int cat_pos, int now_player)
{
int cnt = 0;
if (now_player == CAT)
{
for (auto prev_mouse_pos : edge[mouse_pos])
avaChoices[cnt++] = Choice(prev_mouse_pos, cat_pos, MOUSE);
}
else
{
for (auto prev_cat_pos : edge[cat_pos])
if (prev_cat_pos)
avaChoices[cnt++] = Choice(mouse_pos, prev_cat_pos, CAT);
}
return cnt;
}
int catMouseGame(const vector<vector<int>>& graph)
{
memset(winner, 0, sizeof(winner));
memset(degree, 0, sizeof(degree));
memset(queue, 0, sizeof(queue));
head = tail = 0;
this->V = graph.size();
this->edge = graph;
initSearch();
return memSearch();
}
};
不过这道题还有一种近似求法(利用极大极小搜索+AB剪枝),这个具体代码可以看BHOJ这道题,这种解法我就..懒得再在LeetCode上再写一次了。