算法题 | 你追我,如果你追到我……那就算你赢了

1,018 阅读9分钟

大家好,欢迎阅读周末算法题专题。

今天选择的算法题来源于昨天同一套题中的D题,这题全场通过的人数在2600人左右。虽然通过的人数更少了一些,但是题目的难度却并没有增加很多,但是趣味度增加了。我也是第一次遇见这样的问题。

题目链接:https://codeforces.com/contest/1405/problem/D

废话不多说,我们一起来看这题的题意。

题意

我们都知道数据结构当中的树有这样一个性质,如果树当中有n个点,那么它应该由n-1条无向边组成。并且树当中是一定没有环的,如果有环的话n-1条边就不够了。

今天是说有两个人分别叫做Alice和Bob在一棵树上玩游戏,这两个人名是业内的惯例。凡是两个人玩游戏的题目,主人公的名字很多都叫Alice和Bob,我也不知道这个惯例的由来,大家知道这么回事就好了。Alice和Bob两人各自占据了树上的一个点,然后两个人交替移动。Alice先手,Bob后手。

Alice每一次移动最多可以移动da距离,Bob最多可以移动db距离,两个人也可以放弃移动。假设两人都绝顶聪明每一次都会选择最佳策略,请问经过最多个回合之后,Alice能否捉到Bob呢?如果可以则Alice获胜,否则Bob获胜。注意在Bob的回合,它可以经过Alice的节点,这并不会被视为捉到。

不知道为什么看到这道题的时候,总是会想起费玉清的段子,不知道你们有没有这种感觉。

样例

第一行输入一个数t,表示测试数据的组数。

对于每组数据首先有5个数,分别是n, a, b, da, db。n表示树节点的个数,a和b表示游戏开始时a和b出生点的位置。da和db表示Alice和Bob每回合最多移动的距离。这几个数的最大范围都是,并且多组数据当中所有的n相加不超过

我们来看两组样例:

4 3 2 1 2
1 2
1 3
1 4
6 6 1 2 5
1 2
6 5
2 3
3 4
4 5

第一组数据是这样的:

蓝色表示Alice,红色是Bob。由于Alice先手,只要Alice移动到1节点,无论Bob如何移动,他都必输无疑。

第二组数据:

由于Alice最多只能移动两格,第一回合移动到3,Bob选择不动。只要Alice移动,不论是一格还是两格,Bob都可以直接移动到1节点。也就是说最终Bob就在1和6节点之间来回移动,躲开Alice的追捕。

题解

看到Alice和Bob两人游戏,并且两人都绝顶聪明会选择最佳策略,首先想到的就是博弈论。但是观察一下你会发现这题和博弈论没有什么关系,因为博弈论往往都是公平博弈,局面会有状态转移。但这题当中不是,这题不存在胜负状态转移,一定会有一个确定的结果,就是是谁胜谁负,所以这题不满足博弈论的前提。

类似博弈论的题面只是障眼法而已,如果你信了,并且真的往这方面努力,那么你就被出题人骗了。不过这个陷阱还是比较明显的,很容易看出来。接下来我们又遇到了另外一个陷阱,这个陷阱也很明显,就是执行的回合数量。题目给的是,这个数是真正的天文数字,比宇宙当中所有的粒子数都要多。所以我们可以完全可以理解成无限游戏。

如果出题人阴险一点,完全可以给一个这个量级的回合数,会更有欺骗性一些。其实我们想一下就会发现,树上节点最多只有个,当它们玩追逐游戏的时候,只会有两种情况,一种是永远也追不上,还有一种是很快就追上。所以这个回合数没什么意义,就是唬人的。

洞见

首先,第一个洞见是这道题我们使用模拟是不可行的。所谓的模拟也就是模拟题意的运行情况,去一步一步地分析每一个玩家的选择,做出最好的决策,最后得出游戏的结果。不可行的原因也很简单,因为会超时。这个非常明显,就不深入解释了。

除此之外,我们还可以得到其他一些洞见。首先第一个很简单的洞见是,如果Alice和Bob出生的位置相距小于da的话,那么Alice必胜。这个也很好理解,一开始的距离就在Alice的移动范围内,那Bob一上来就被抓住了。没有讨论的余地。另外一个洞见是如果da >= db,那么Alice必胜。

也就是如果Bob跑得比Alice慢的话,他也一样必败。这也很简单,因为地图的范围是有限的,一个追一个逃,逃的速度比追的慢,显然他逃不出去。这个结论虽然简单,但是反过来一想会得到一个问题。如果Bob跑得比Alice快的话,他一定可以获胜吗

我们看第一个案例就知道这个答案不一定,因为地形也会影响最终的结果。树意味着每一个节点都全连通,也意味着每两点的路线只有一条。换句话说每一条路线都是思路,即使Bob跑得快如果不反向跑甩掉Alice的话,那么他也是一样会被Alice追上的。

所以我们接下来要做的就是深入分析这里的情况,把剩下的问题捋清楚。

回味样例

codeforces的问题有一个特点就是我们一定要深入分析样例,因为很多出题人很良心,给出的样例都是关键样例。我们可以借助样例的情况帮助我们分析问题。比如今天说的这题就是一个。

我们来分析一下第一个样例:

这就是典型的Bob跑得比Alice快的样例,Bob不仅跑得比Alice快,甚至还是Alice的两倍。但是我们都知道结果还是Alice获胜,原因也很简单,Alice移动到1号节点之后,Bob只有两个选择要么1号节点要么4号节点,这两个节点都距离1号节点只有一个距离,仍然在Alice追上的范围内。

在这个样例当中Bob的逃跑空间勉强是够的,但还是被追上了,说明他的逃跑速度是不够的。不够的原因也很简单,因为一开始当Alice移动到1节点之后,他距离Bob的距离是1,也就是一个da。这时Bob无路可走只能折返甩开Alice,这时候他最多能够走一个db,他要想不被Alice捉住,首先他需要先走过da这一段距离,接着他需要走出至少da+1的距离,才能保证Alice折返追他也追不到。那么我们可以得出一个条件是db > 2da。

但我们发现仅仅db > 2da还是不够的,因为在这个例子当中我们可以看得出来不够开阔,即使db=3,Bob也没处可去。也就是说在这棵树上至少有一条链路它的长度要大于2da才行,否则即使Bob再能跑也会被地形限制住。对于一棵树而言,求它的最长链路还是比较简单的,我们也在之前的文章当中讲解过,其实就是对于每一个节点都求一个到叶子节点的最长距离和次长距离之和。所有的节点的距离最大的那个就是整棵树上的最长链路。

我们整理一下思路,一共发现了3个条件,只有满足这3个条件,Bob才可能跑掉,否则一定会被Alice捉住。这三个条件分别是:

  1. 起始的时候Bob和Alice距离大于da
  2. 树的直径大于2da
  3. db大于2da

所以我们要求的就只有两点,第一点是一开始它们之间的距离,以及树的直径(树上最长的链路长度)。好在这两点都可以通过递归实现。都理出来了之后代码就不难写了:

t = int(input())

depth = [0 for _ in range(200050)]
for _ in range(t):
    n, a, b, da, db = list(map(int, input().split(' ')))
    edges = [[] for _ in range(n+2)]
    for i in range(n-1):
        u, v = list(map(int, input().split(' ')))
        edges[u].append(v)
        edges[v].append(u)

    diameter = 0

    depth[a] = 0
    def dfs(u, f):
        global diameter
        l = 0
        for v in edges[u]:
            if v == f:
                continue
            # depth数组记录每个节点的树深
            depth[v] = depth[u] + 1
            cur = 1 + dfs(v, u)
            # cur + l即u节点到叶子节点的最长距离和次长距离
            # 直径就是这两者和之中最大的一个
            diameter = max(diameter, cur + l)
            l = max(l, cur)

        return l

 # 以Alice所在的点作为树根,这样depth[b]即Alice和Bob的距离
    dfs(a, -1)
    if depth[b] <= da or da * 2 >= diameter or da*2 >= db:
        print('Alice')
    else:
        print('Bob')

我们把整个思路说穿了是不是有一种一文不值的感觉?但是自己思考要想明白还是不太容易的,codeforces的问题就是这样,经常需要我们在纸上画一画看一看。有时候一些点靠自己想很难完全想明白,但是找一个例子试一试一下就清楚了。这也是codeforces问题有趣的地方之一。

衷心祝愿大家每天都有所收获。如果还喜欢今天的内容的话,请来一个三连支持吧~(点赞、关注、转发

原文链接,求个关注

本文使用 mdnice 排版

- END -