2023“钉耙编程”中国大学生算法设计超级联赛(7)H. HEX-A-GONE Trails (思维/博弈)

文章讲述了在一个树形结构游戏中,两个人交替移动,目标是确保先手玩家能在特定条件下确保胜利。通过分析路径长度和先手优势,提出了利用线段树数据结构来判断玩家是否能赢得游戏的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原题链接:H. HEX-A-GONE Trails


无关吐槽:这题赛中开了思路很快就出了,但是写法有问题,有一堆信息很难进行维护。赛后疯狂拿 s t d std std 对拍改细节, d e de de 了六个小时还没 d e de de 出来,索性放弃这个写法。换了一种写法又搞了四个多小时,终于在凌晨一点把这题过了。(我哪来的毅力)

题目大意:


给出一棵树,有 n n n 个节点, n − 1 n-1 n1 条边。有两个人在进行游戏,一个人最初在编号为 x x x 的点,另一个人在 y y y 号点。

玩家轮流执行如下的操作:

  • 轮到自己的回合时,自己必须进行移动。移动的方式是走到与自己相邻的顶点。
  • 所有被玩家(两个人)走过的顶点,就不能再走第二次。
  • 不能走到另一个玩家所在的点上。

两个人轮流行动,当一个玩家不能移动时就判定为输。假设他们都足够聪明,请你判断在游戏最后 x x x 点上的玩家能否获胜。输出 1 1 1 则是胜利, 0 0 0 则是失败。

解题思路:


这题样例没什么参考价值。自己手动画了几个图,很快就摸到了思路。

一般这种树上问题,先考虑从子树入手,再推进到整棵树。

下面给出一张图,假设我们的玩家 x = 8 x=8 x=8,玩家 y = 1 y=1 y=1

在这里插入图片描述

为了减少各种树上问题的讨论,我们假设以玩家 x = 8 x = 8 x=8 为根,构建出一颗有根树。那么 8 8 8 号节点的树上问题就分为以 10 10 10 号节点和以 5 5 5 号节点为根的树上问题。

(不能动的玩家就判负,即玩家要找到一条路径使得这一条路径比另一名玩家所有可选的路径都要长。)

首先发现,对于两个点 x x x y y y 来说,两者都不能走到另一方的子树。

举个例子,假设我们的 8 8 8 号不用移动,我们的 1 1 1 号节点是无论如何都走不到 8 → 10 → 13 → 14 8 \rightarrow 10 \rightarrow 13 \rightarrow 14 8101314 这一条链上的,同理,我们的 8 8 8 号节点也是走不到 1 → 3 → 7 → 12 1 \rightarrow 3 \rightarrow 7 \rightarrow 12 13712 这条链上的。

除此之外的部分,在不相互影响的条件下,两个点都能走到。

那么思路就很好想了,假设我们为 x x x 节点来进行选择路径的讨论:

  • 假设 x x x 选择走子树,从中选择出一条最长路径,且这一条路径严格大于 y y y x x x 子树外的所有的可选路径,那么 x x x 必胜。 (注意是严格大于,因为当长度相等,而 x _{x} x是先手的情况,当两者都走到尽头之后 x _{x} x将无法做出任何移动)

  • 否则 x x x 则不走子树,将会走两者公共链上的部分。

对于上图,假设我们的 x = 8 x = 8 x=8 节点走子树部分,那么最长的路径则会是 3 3 3 ,而 y = 1 y = 1 y=1 号节点在剩余的路径中最长的路径则会是 5 5 5 。那么 x x x 显然会走公共链部分。

我们再进一步进行考虑。

我们把图简化一下,去掉 x x x 的子树:

在这里插入图片描述
我们将公共链部分分进行如下分解:

  • 将所有节点分为 x → y x \rightarrow y xy 这条链上的节点和链外节点。
  • 以及从链上节点出发,走链外节点,能到达的最长的路径长度(用 l e n len len 数组表示)。

那么 8 → 5 → 4 → 2 → 1 8 \rightarrow 5 \rightarrow 4 \rightarrow 2 \rightarrow 1 85421 这一条链中: l e n [ 5 ] = 2 len[5] = 2 len[5]=2 l e n [ 4 ] = 0 len[4] = 0 len[4]=0 l e n [ 2 ] = 1 len[2] = 1 len[2]=1 以此类推。

而对两个端点 x = 8 x=8 x=8 y = 1 y=1 y=1 来说 l e n [ x ] = 3 len[x]=3 len[x]=3 l e n [ y ] = 3 len[y]=3 len[y]=3 ,即子树的最长路径长度。

 

根据上述情况,我们的 x x x 此时一定会走公共链部分。我们考虑在 x x x 走公共链时,什么时候玩家必胜:

  • 我们让两个玩家轮流进行操作,那么对于每一个玩家来说自己都是先手状态:当前状态我们可以沿着链继续向前,也可以停下走链外节点

  • 假设我们停下,开始走链外节点,我们会必胜当且仅当另一名玩家走剩下所有节点时,其能够增加的步数 m o v mov mov,小于我们能够增加的步数 l e n [ x ] len[x] len[x]。即 l e n [ x ] > m o v len[x]>mov len[x]>mov 时,我们必胜。

  • 否则此时一定 l e n [ x ] ≤ m o v len[x] \leq mov len[x]mov ,由于我们是先手状态,如果我们当前停下必定会输,则我们会继续向前走,尝试看看有没有必胜策略。如果前方也没有必胜策略,那么我们无论什么时候停下都是必输。

  • 假设双方在链上情况都没有必胜策略,那么此时 x x x y y y 一定会在链上相遇,这时我们只能走链外节点,判断 l e n [ x ] len[x] len[x] l e n [ y ] len[y] len[y] 与先后手状态即可知道胜负关系。

 

利用双指针 l l l r r r 代表 x x x y y y 当前移动到的位置, x x x 从左向右移动, y y y 从右向左移动。

判断 x x x 当前是否要停下,则查询 y y y 能扩展到的最大距离即可。 我们对 y y y 维护 l e n [ i ] − i len[i] - i len[i]i max ⁡ \max max 值。然后用 ( max ⁡ l + 1 ≤ i ≤ r l e n [ i ] − i ) + r (\max_{l+1 \leq i \leq r} len[i] - i)+r (maxl+1irlen[i]i)+r 即可查询 y y y 能扩展的最大距离。

同理对 y y y 我们维护 l e n [ i ] + i len[i]+i len[i]+i max ⁡ \max max 值,用 ( max ⁡ l ≤ i ≤ r − 1 l e n [ i ] + i ) − l (\max_{l \leq i \leq r-1} len[i] + i)-l (maxlir1len[i]+i)l 即可查询 x x x 能扩展的最大距离。

维护 m a x max max 值的方法很多,这里给出用线段树的方法,具体解释都放到代码里了。

上述测试用例是先手必胜,输出 1 1 1 ,代码最下方给出测试用方便自测。

时间复杂度: O ( n log ⁡ n ) O(n\log n) O(nlogn)

AC代码:



#include <bits/stdc++.h>
#define YES return void(cout << "Yes\n")
#define NO return void(cout << "No\n")
using namespace std;

using u64 = unsigned long long;
using PII = pair<int, int>;
using i64 = long long;

const int N = 1e5 + 1, M = 1e7;

const int MIN = INT32_MIN;

int seg[N << 2][2];

vector<int> g[N];
int path[N], T[N][2];
bool vis[N];

#define lson k << 1, l, mid
#define rson k << 1 | 1, mid + 1, r
void build(int k, int l, int r) {
    if (l == r) {
        seg[k][0] = T[l][0];
        seg[k][1] = T[l][1];
        return;
    }
    int mid = l + r >> 1;
    build(lson), build(rson);
    int ls = k << 1, rs = k << 1 | 1;
    seg[k][0] = max(seg[ls][0], seg[rs][0]);
    seg[k][1] = max(seg[ls][1], seg[rs][1]);
}

int qry(int k, int l, int r, int x, int y, bool t) {
    if (l >= x && r <= y) return seg[k][t];
    int mid = l + r >> 1, res = MIN;//查询时注意初始化负无穷
    if (x <= mid) res = max(res, qry(lson, x, y, t));
    if (y > mid) res = max(res, qry(rson, x, y, t));
    return res;
}

void DFS(int u, int ufa) {
    for (auto& v : g[u]) {
        if (v == ufa) continue;
        path[v] = u;
        DFS(v, u);
    }
};

int DFS2(int u, int ufa) {
    int res = 0;
    for (auto& v : g[u]) {
        if (vis[v] || v == ufa) continue;
        res = max(DFS2(v, u) + 1, res);
    }
    return res;
}

void solve() {

    int n, x, y;
    cin >> n >> x >> y;

    //常规多测清空
    path[x] = 0;//我们的根节点没有前驱
    for (int i = 1; i <= n; ++i) {
        g[i].clear(), vis[i] = false;
    }

    for (int i = 1, u, v; i <= n - 1; ++i) {
        cin >> u >> v;
        g[u].emplace_back(v);
        g[v].emplace_back(u);
    }

    //我们先算出以 x 为根的所有节点的前驱
    //便于标记 x->y 链和 计算len数组
    DFS(x, 0);
    

    //我们利用path回溯整条链把所有节点处理出来
    //并且标记链上节点不能走 后续用来计算 len数组
    int t = y;
    vector<int> mov;//存链上节点
    while (t) {
        vis[t] = true;
        mov.emplace_back(t);
        t = path[t];
    }
    mov.emplace_back(0);//保证下标从 1 开始
    reverse(mov.begin(), mov.end());//倒着存要翻转过来
    int m = mov.size() - 1;

    //我们用一个简单dp算出每个链上每个点的len数组
    vector<int> len(m + 1);
    for (int i = 1; i <= m; ++i) {
        int v = mov[i];
        len[i] = DFS2(v, v);
    }

    //处理出我们要维护的数组,用来建树
    for (int i = 1; i <= m; ++i) {
        T[i][0] = len[i] + i;
        T[i][1] = len[i] - i;
    }

    //建线段树操作
    build(1, 1, m);

    //用cur来交换先后手操作 1是x操作 0是y操作
    //l 是 x 当前的位置,r 是 y 当前的位置
    int cur = 0, l = 1, r = m;

    while (true) {
        if (cur ^= 1) {
            //如果双方没有必胜策略且相遇了
            if (r - l == 1) {
                cout << (len[l] > len[r]) << '\n';
                return;
            }
            //判断停下是否必胜 len[l] > max(len[i]-i) + r
            if (len[l] > qry(1, 1, m, l + 1, r, 1) + r) {
                cout << 1 << '\n';
                return;
            }
            ++l;//否则继续向前
        }
        else {
            //如果双方没有必胜策略且相遇了
            if (r - l == 1) {
                cout << (len[l] >= len[r]) << '\n';
                return;
            }
            //判断停下是否必胜 len[r] > max(len[i]+i) - r
            if (len[r] > qry(1, 1, m, l, r - 1, 0) - l) {
                cout << 0 << '\n';
                return;
            }
            --r;//否则继续向前
        }
    }
}

signed main() {

    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    int t = 1; cin >> t;
    while (t--) solve();

    return 0;
}

/*
1
14
8 1
2 1
3 1
4 2
5 4
6 2
7 3
8 5
9 5
10 8
11 9
12 7
13 10
14 13

*/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬味的橙汁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值