本文为翻译文章
原文链接:https://cp-algorithms.com/game_theory/games_on_graphs.html
Games on Arbitrary Graphs
两个选手在任意的一个图 G G G上做游戏,当前游戏的状态表示为图上的一个点。两名选手轮流行动,每一次行动可以把当前的点移动到有一条边相连的位置上。根据游戏规则的不同,第一个不能移动的选手赢或者输。
我们考虑最一般的情况,即在一个带环的有向图上的游戏。给定一个初始的状态,假设两位选手都使用最好的策略,任务是判断谁能赢得游戏,或者这个游戏是平局。
这个问题可以被很高效地解决掉。我们可以用 O ( m ) O(m) O(m)的时间(m为图中边的数量)得到图上所有点作为初始点的结果。
算法描述
在两名选手都使用最佳策略的情况下,如果某个点作为初始点先手能获得胜利,那就把这个点叫做胜利点。类似的,如果某个点作为初始点,先手失败,那这个点就叫失败点。
图上所有没有出边的点,我们是可以马上判断出该点是胜利点还是失败点。
同时还有以下的判断规则:
- 如果一个顶点存在一条到达失败点的边,那么这个点是一个胜利点。
- 如果一个顶点的所有出边都会到达胜利点,那这个点就是一个失败点。
- 如果最后还有一些点上面的两个条件都不符合,那么在两名选手都使用最佳的策略时,这个点作为起始点,游戏会平局。
我们马上就可以找到一个 O ( n m ) O(nm) O(nm)的算法。就是要一遍又一遍地遍历所有结点,判断每个结点是否满足前两个条件,如果在一次遍历中再也找不到新得结点可以满足前两个条件,那么停止遍历。
这个和我们想要的时间复杂度相差太大,我们需要把时间复杂度降到 O ( m ) O(m) O(m)。
首先我们先遍历一般所有的结点,找到那些开始就确定是胜利点和失败点的结点。从每个这样的点开始跑一遍dfs。dfs过程是从已知结点扩展到未知结点的过程。如果当前结点的上一个结点是失败点,那么就是上面的第一种情况,即这个结点是成功点。如果当前结点的上一个结点是成功点,还需要判断所有到达它的结点是否是成功点,这个过程可以通过增加一个degree
数组在
O
(
1
)
O(1)
O(1)的时间内实现,每次遇到当前的结点且上一个结点为胜利点时,degree
数组减一,如果degree
数组减到了0,那么说明该结点满足上面第二个条件。
对于每个成功点和失败点我们都访问1次,对于那些平局点我们不访问。总共的时间复杂度为 O ( m ) O(m) O(m)。
代码实现
下面是用DFS的代码实现。其中变量adj_rev
保存的是反向边。边
(
i
,
j
)
(i,j)
(i,j)在adj_rev
中保存为边
(
j
,
i
)
(j,i)
(j,i)。同时假设所有边的出度已经计算过了。
vector<vector<int>> adj_rev;
vector<bool> winning;
vector<bool> losing;
vector<bool> visited;
vector<int> degree;
void dfs(int v) {
visited[v] = true;
for (int u : adj_rev[v]) {
if (!visited[u]) {
if (losing[v])
winning[u] = true;
else if (--degree[u] == 0)
losing[u] = true;
else
continue;
dfs(u);
}
}
}