2025 图灵杯做题记录

2025 图灵杯做题记录

老师让我们打高级组,罚坐 3h,获得了 \(27 + 10 + 0 + 0 = 37\) 分的高分,获得了 rk69 的好成绩!

最后 50min 的时候觉得罚坐也没什么意义,然后报名了中级组,花 40min 通过了中级组 T1,但没时间看 T2 了/kk

各组别使用的题:

  • 初级组:ABCD
  • 中级组:CDEF
  • 高级组:EFGH

C. 登机

注意到问题的关键点在于乘客前进时会被先走的人挡住。一个乘客在前进的途中可能被多个人挡住,这是不好处理的。关键的观察在于:只有最后一个挡住该乘客的人是值得关注的。因为即使忽略前面所有挡住他的人,到最后他还是会被挡住。

那么关键在于求出最后一个挡住第 \(i\) 个乘客的人。先把乘客按 \(p_i\) 从小到大排序并重标号,记 \(t_{j}\) 表示从前往后第 \(j\) 个乘客就座的时间,\(q_j\) 表示就坐的位置。按时间顺序处理,对所有坐在第 \(i\) 个乘客前面的人 \(j\),判断 \(i\) 会不会被 \(j\) 挡住,如果会被挡住,记 \(x\) 表示最后一个挡住 \(i\) 的人就坐的时间。那么

\[x = \max_{j} \{\max(s_i + q_j, t_j)\} \]

也就是说,\(i\) 到达第 \(j\) 个人的位置的时间是 \(s_i + q_j\),如果被挡住,就用 \(t_j\) 更新 \(x\)。注意着其实也处理了没有被人挡住的情况。取 \(\max\) 的过程就求出了最后一个挡住 \(i\) 的人 \(j\)。那么 \(i\) 就坐的时间

\[ans_{i} = x + (p_i - q_j) + a_i \]

为了分离 \(ans\) 中的 \(j\),修改 \(x\) 的定义为

\[\begin{aligned} x &= \max_{j} \{\max(s_i, t_j - q_j)\} \\ &= \max(s_i, \max_{j}\{t_{j} - q{j}\}) \end{aligned} \]

\[ans_{i} = x + p_i + a_i \]

\(x\) 可以通过在树状数组上查询前缀最小值得到。时间复杂度 \(O(n \log n)\)

(注意树状数组可以维护前缀最小值——虽然我原则上知道这一点,但实际使用上老是忘记,赛时还是写了线段树。)

Code
#include<bits/stdc++.h>

using namespace std;

typedef long long i64;

struct SGT {
    #define lson (id << 1)
    #define rson (id << 1 | 1)
    const int n;
    vector<i64> c;
    SGT(int _n): n(_n), c(n << 2) {}
    void update(int id) {
        c[id] = max(c[lson], c[rson]);
    }
    void change(int id, int l, int r, int pos, i64 x) {
        if(l == r) {
            c[id] = x;
            return;
        }
        int mid = (l + r) >> 1;
        if(pos <= mid) change(lson, l, mid, pos, x);
        else change(rson, mid + 1, r, pos, x);
        update(id);
    }
    void change(int pos, i64 x) { change(1, 1, n, pos, x); }
    i64 query(int id, int l, int r, int L, int R) {
        if(l == L && r == R) {
            return c[id];
        }
        int mid = (l + r) >> 1;
        if(R <= mid) return query(lson, l, mid, L, R);
        else if(L > mid) return query(rson, mid + 1, r, L, R);
        else return max(query(lson, l, mid, L, mid), query(rson, mid + 1, r, mid + 1, R));
    }
    i64 query(int L, int R) { return query(1, 1, n, L, R); }
};

int main() {
    cin.tie(nullptr) -> sync_with_stdio(false);

    int n;
    cin >> n;
    vector<int> s(n + 1), p(n + 1), a(n + 1);
    for(int i = 1; i <= n; i++) {
        cin >> s[i];
    }
    for(int i = 1; i <= n; i++) {
        cin >> p[i];
    }
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
    }

    vector<int> q = p;
    sort(q.begin() + 1, q.end());

    vector<i64> ans(n + 1);
    SGT tr(n);
    for(int i = 1; i <= n; i++) {
        auto it = lower_bound(q.begin() + 1, q.end(), p[i]);
        int id = (int)(it - q.begin());
        i64 x = max((i64)s[i], tr.query(1, id));
        ans[i] = x + a[i] + p[i];
        tr.change(id, ans[i] - q[id]);
    }

    for(int i = 1; i <= n; i++) {
        cout << ans[i] << '\n';
    }

    return 0;
}

D. Bougainvillea

给定一个无向带权图,多次询问两点之间是否存在长度为 \(w\) 的倍数的路径。

好题。不需要任何复杂的算法,只需要对性质的深刻观察。

\(o = 1\)

等价于询问两点之间是否可以只通过边权为 \(0\) 的边到达。用 \(0\) 边 dfs/bfs 预处理一遍连通块即可。

\(o = 2\)

等价于询问两点之间是否存在长度为偶数的路径。套路性地想到二分图,因为二分图满足性质:同色点之间的路径长度必定为偶数,异色点的路径长度必定为奇数。尝试对一个连通块进行黑白染色,如果成功,说明该连通块是二分图,询问时判断两点的颜色是否相同即可。否则说明连通块中存在奇环。此时,对于 \(u, v\) 之间的任意一条路径,如果长度为偶数,则我们已经找到了一个解;否则,设 \(x\) 是路径上的任意一个点,\(y\) 是奇环上的任意一个点,在原路径上到达 \(x\) 时,从 \(x\) 出发到达 \(y\),走过整个奇环回到 \(y\),再从 \(y\) 原路返回 \(x\),然后沿着原来的路径走。\(x \mapsto y\)\(y \mapsto x\) 两条路径完全相同,长度之和是偶数,不影响总长度的奇偶性;而奇环改变了奇偶性,所以路径长度由奇数改变成了偶数。这就说明:如果图中存在奇环,则任意两点之间必定存在长度为偶数的路径。

\(w \le 3\)

本题中最关键的一个部分分。先假设边权都为 \(1\)\(w = 1\) 的情况相当于询问两点之间是否连通,这是容易的;\(w = 2\) 的情况已经在上一个部分讨论过;关键在于 \(w = 3\) 的情况。我们观察到:

如果 \(k\) 是奇数,\(u, v\) 之间存在长度为 \(k\) 的倍数的路径等价于两点连通。

证明 对于 \(u \mapsto v\) 的任意一条路径,设其长度为 \(s\),我们反复走这条路径 \(k\) 次:

\[\underbrace{u \mapsto v \mapsto u \mapsto v \mapsto \cdots \mapsto v}_{k \ \text{times}} \]

由于 \(k\) 是奇数,所以最终一定会回到 \(v\),而路径总长度为 \(k \cdot s\),是 \(k\) 的倍数。\(\Box\)

这说明 \(w\) 是奇数的情况都是平凡的:只要判断两点是否连通即可。

这个部分分中边权可能不为 \(1\),这只影响 \(w = 2\) 的情况。处理是容易的:在二分图染色时加上边权的影响即可:如果两点之间的边权为偶数,则染成同色,否则染成不同颜色。这样染出来的二分图仍然满足性质:同色点之间的路径长度为偶数,异色点为奇数。

\(o = 3\)

我们已经知道奇数询问是平凡的。进一步,设 \(k\)\(w\) 质因数分解中 \(2\) 的幂次(或者说 \(2^{k}\)\(w\)\(\operatorname{lowbit}\)),\(w = a \cdot 2^{k}\),那么 \(a\) 是奇数。扩展之前的结论,从 \(w\) 中除掉一个奇数不影响答案,那么最终只用考虑 \(w = 2^{k}\)\(k \ge 1\))的情况。

设连通块中所有边权的 \(\operatorname{lowbit}\) 的最小值为 \(2^{b}\),则如果 \(2^{k} \mid 2^{b}\),则任意路径长度都是 \(2^{k}\) 的倍数,一定有解。否则把所有边权都除以 \(2^{b}\),那么连通块中一定存在奇数边权。此时询问变成了是否存在 \(u \mapsto v\) 的长度为 \(2^{k - b}\) 的倍数的路径。还是二分图染色,由于连通块是树,所以一定可以染色。如果 \(u, v\) 异色,则 \(u, v\) 之间的任意路径长度都是奇数,一定无解。否则,由于图中存在奇数边权,可以走到这条边上一直走,调整路径长度模 \(2^{k - b}\) 的余数。每在这条边上往返一次,都会让路径长度模 \(2^{k - b}\) 的余数增加 \(2\)(这是边权长度为奇数决定的),所以往返有限次以后一定可以把路径长度调整为 \(2^{k - b}\) 的倍数。这就说明我们可以根据两点的颜色来判断是否存在合法路径。

一个小细节:任意整数都是 \(0\) 的因数,所以规定 \(\operatorname{lowbit}(0) = +\infty\),这是自然的。

一般情况

一般情况与 \(o = 3\) 的唯一区别在于连通块可能不是树,因此二分图染色不一定成功。我们沿用 \(o = 2\) 的做法:如果连通块不是二分图,说明存在奇环,因此可以不断走奇环调整路径长度模 \(2^{k}\) 的余数。设要在奇环上走 \(x\) 次,由于奇环的长度 \(s\) 为奇数,因此 \(s \perp 2^{k}\)。根据裴蜀定理, 对任意整数 \(y\)\(s \cdot x \equiv y \pmod{2^{k}}\) 一定有解,所以总是存在合法路径。

时间复杂度:预处理 \(O(n + m)\),单次询问 \(O(1)\)

Code
#include<bits/stdc++.h>

using namespace std;

typedef long long i64;
constexpr i64 max_bit = 1LL << 62;
int n, m, o;
struct Edge {
    int v; i64 w;
};
vector<vector<Edge>> G;

vector<int> comp, comp0, col, conf, vis;
// component id, color, conflict
vector<i64> g(n + 1);
// lowbit of all edges in the component
int cur;

i64 lowbit(i64 x) {
    return x & -x;
}

void dfs(int u) {
    comp[u] = cur;
    for(auto [v, w]: G[u]) {
        if(w != 0) {
            i64 x = lowbit(w);
            g[cur] = min(g[cur], x);
        }
        if(!comp[v]) {
            dfs(v);
        }
    }
}

void dfs1(int u) {
    vis[u] = 1;
    for(auto [v, w]: G[u]) {
        if(!vis[v]) {
            col[v] = col[u] ^ (int)((w / g[cur]) & 1);
            dfs1(v);
        } else if(col[v] != (col[u] ^ (int)((w / g[cur]) & 1))) {
            conf[cur] = 1;
        }
    }
}

void dfs0(int u) {
    comp0[u] = cur;
    for(auto [v, w]: G[u]) {
        if(w == 0 && !comp0[v]) dfs0(v);
    }
}

void prework() {
    comp0.resize(n + 1);
    for(int i = 1; i <= n; i++) {
        if(!comp0[i]) cur++, dfs0(i);
    }

    cur = 0, comp.resize(n + 1), vis = col = conf = comp;
    g.resize(n + 1, max_bit);
    for(int i = 1; i <= n; i++) {
        if(!comp[i]) {
            cur++, dfs(i);
            if(g[cur] != max_bit) {
                dfs1(i);
            }
        }
    }
}

bool query(int u, int v, i64 w) {
    if(w == 0) {
        return comp0[u] == comp0[v];
    }
    if(comp[u] != comp[v]) {
        return false;
    }
    if(w & 1) {
        return true;
    }

    int id = comp[u];
    i64 x = lowbit(w);
    if(x <= g[id] || conf[id]) {
        return true;
    }
    return col[u] == col[v];
}

int main() {
    cin.tie(nullptr) -> sync_with_stdio(false);

    int q;
    cin >> n >> m >> q >> o;
    G.resize(n + 1);
    for(int i = 1, u, v; i <= m; i++) {
        i64 w;
        cin >> u >> v >> w;
        G[u].push_back({v, w}), G[v].push_back({u, w});
    }

    prework();

    for(int i = 1, u, v; i <= q; i++) {
        i64 w;
        cin >> u >> v >> w;
        cout << (query(u, v, w) ? "bougain" : "villea") << '\n';
    }

    return 0;
}
posted @ 2025-05-26 12:48  DengStar  阅读(139)  评论(0)    收藏  举报