Skip to content

查看源代码:
CSP-S-2025.md

---
title: CSP-S 2025 游记
createTime: 2025/11/2
categories:
    - IT
tags:
    - OI
    - OI-travel-notes
---

## 前

刚学农回来。唯一的复习是学农前打的一把 CF(而且打得不怎么样)

## T1

:::problem 题意
把 $n$ 个人安排进 $m$ 个位置,每个位置最多放 $\left\lceil \dfrac{n}{2} \right\rceil$ 个人。

第 $i$ 个人安排进第 $j$ 个位置的收益是 $a_{i,j}$,求最大收益。
:::

反悔贪心,之前没看到最多放 $\left\lceil \dfrac{n}{2} \right\rceil$,还在想多重反悔的问题。但其实不用。

在安排过程中,最多只有一个位置安排满(且这个满的位置不会变),这就可以简单地反悔贪心。

由于没审好题,目前时间:$40\ \mathrm{min}$。

:::details 代码由 AI 根据思路生成
```cpp
#include <bits/stdc++.h>
using namespace std;

struct Person {
    int a[3];
    int best_dept;   // 当前分配的部门 0,1,2
    int second_dept; // 次优部门
    int loss;        // 离开 best_dept 的损失 = a[best_dept] - a[second_dept]
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t;
    cin >> t;
    while (t--) {
        int n;
        cin >> n;
        vector<Person> persons(n);
        vector<int> cnt(3, 0);
        int total = 0;

        // 读入并初始化分配
        for (int i = 0; i < n; i++) {
            cin >> persons[i].a[0] >> persons[i].a[1] >> persons[i].a[2];
            // 找出最大值和次大值的部门
            int best = 0, second = -1;
            for (int j = 1; j < 3; j++) {
                if (persons[i].a[j] > persons[i].a[best]) {
                    second = best;
                    best = j;
                } else if (second == -1 || persons[i].a[j] > persons[i].a[second]) {
                    second = j;
                }
            }
            // 如果次大还没确定(比如有两个相同最大值)
            if (second == -1) {
                // 找除了best外最大的
                second = (best == 0) ? 1 : 0;
                for (int j = 0; j < 3; j++) {
                    if (j != best && persons[i].a[j] > persons[i].a[second]) {
                        second = j;
                    }
                }
            }
            persons[i].best_dept = best;
            persons[i].second_dept = second;
            persons[i].loss = persons[i].a[best] - persons[i].a[second];
            cnt[best]++;
            total += persons[i].a[best];
        }

        // 每个部门一个最小堆,存 (loss, index)
        vector<priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>>> heaps(3);

        for (int i = 0; i < n; i++) {
            int dept = persons[i].best_dept;
            heaps[dept].push({persons[i].loss, i});
        }

        // 调整超额部门
        for (int dept = 0; dept < 3; dept++) {
            while (cnt[dept] > n / 2) {
                auto [loss, idx] = heaps[dept].top();
                heaps[dept].pop();
                // 将这个人调整到次优部门
                int old_dept = persons[idx].best_dept;
                int new_dept = persons[idx].second_dept;
                // 如果新部门也满了,需要递归调整?但题目最多一个部门满,所以新部门不会满
                // 直接调整
                cnt[old_dept]--;
                cnt[new_dept]++;
                total -= loss;
                // 更新这个人的 best_dept 和 second_dept
                persons[idx].best_dept = new_dept;
                // 需要更新其次优部门:在剩下的两个部门里选最大的
                int sec = -1;
                for (int j = 0; j < 3; j++) {
                    if (j != new_dept) {
                        if (sec == -1 || persons[idx].a[j] > persons[idx].a[sec]) {
                            sec = j;
                        }
                    }
                }
                persons[idx].second_dept = sec;
                persons[idx].loss = persons[idx].a[new_dept] - persons[idx].a[sec];
                // 加入新部门的堆
                heaps[new_dept].push({persons[idx].loss, idx});
            }
        }

        cout << total << '\n';
    }
    return 0;
}
```
:::

## T2

:::problem 题意
有一个 $|V|=n \le 10^4, |E|=m \le 10^6$ 的连通图,有 $k \le 10$ 个结点是额外结点。额外结点的启用是可选的,启用第 $i$ 个额外结点的代价为 $c_i$。

求 最小生成树大小 $+$ 总启用代价 的最小可能值。
:::

看见额外结点 $\le 10$ 就知道要枚举所有启用情况。首先给非额外结点跑一遍最小生成树,把边规模压缩到 $O(n)$。

然后?我只知道在最小生成树加一条边的解法。加很多条边就不知道了。

直接 Kruskal(或 Prim)是 $O(m \log m + 2^k kn \log n)$ 的。

这个解法大概率会在大测试点 $\color{#052242}\textbf{TLE}$,考场上想了一会儿(其实还挺久的)没想到更好的,于是打了。

犹豫了很久,目前时间:$2 \mathrm{h}$。然后去上了个厕所中场休息。

:::details 代码由 AI 根据思路生成
```cpp
#include <bit/stdc++.h>
using namespace std;

struct Edge {
    int u, v;
    long long w;
    bool operator<(const Edge& other) const {
        return w < other.w;
    }
};

struct DSU {
    vector<int> parent, rank;
    DSU(int n) : parent(n), rank(n, 0) {
        iota(parent.begin(), parent.end(), 0);
    }
    int find(int x) {
        while (parent[x] != x) {
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }
    bool unite(int x, int y) {
        x = find(x), y = find(y);
        if (x == y) return false;
        if (rank[x] < rank[y]) swap(x, y);
        parent[y] = x;
        if (rank[x] == rank[y]) rank[x]++;
        return true;
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m, k;
    cin >> n >> m >> k;

    vector<Edge> edges(m);
    for (int i = 0; i < m; i++) {
        cin >> edges[i].u >> edges[i].v >> edges[i].w;
        edges[i].u--; edges[i].v--;
    }

    // 原图 MST
    sort(edges.begin(), edges.end());
    DSU dsu0(n);
    vector<Edge> mstEdges;
    long long baseCost = 0;
    for (const auto& e : edges) {
        if (dsu0.unite(e.u, e.v)) {
            mstEdges.push_back(e);
            baseCost += e.w;
        }
    }
    // mstEdges 现在有 n-1 条边

    vector<long long> c(k);
    vector<vector<long long>> a(k, vector<long long>(n));
    for (int j = 0; j < k; j++) {
        cin >> c[j];
        for (int i = 0; i < n; i++) {
            cin >> a[j][i];
        }
    }

    long long ans = baseCost; // 不使用任何乡镇的情况

    // 枚举乡镇选择
    for (int mask = 1; mask < (1 << k); mask++) {
        int townCount = __builtin_popcount(mask);
        int totalNodes = n + townCount;
        vector<Edge> allEdges = mstEdges; // 原 MST 边

        // 分配乡镇编号: n, n+1, ...
        vector<int> townId;
        long long activateCost = 0;
        for (int j = 0; j < k; j++) {
            if (mask >> j & 1) {
                townId.push_back(j);
                activateCost += c[j];
            }
        }

        // 加入乡镇到城市的边
        for (int idx = 0; idx < townId.size(); idx++) {
            int j = townId[idx];
            int townNode = n + idx;
            for (int city = 0; city < n; city++) {
                allEdges.push_back({townNode, city, a[j][city]});
            }
        }

        // 跑新图的 MST
        sort(allEdges.begin(), allEdges.end());
        DSU dsu(totalNodes);
        long long mstCost = 0;
        int connected = 0;
        for (const auto& e : allEdges) {
            if (dsu.unite(e.u, e.v)) {
                mstCost += e.w;
                connected++;
                if (connected == totalNodes - 1) break;
            }
        }

        ans = min(ans, activateCost + mstCost);
    }

    cout << ans << '\n';
    return 0;
}
```
:::

洛谷 $\color{orange}\text{80 pts}$。

::::caution 赛时唐
其实可以用归并把时间复杂度变成 $O(m \log m + 2^k kn \alpha(n))$。

我赛时没用归并应该是因为比较难回溯。但其实根本不用回溯,从头归并的时间复杂度不会更高,瓶颈在 Kruskal。

:::details 归并解法(AI 生成,100 pts)
```cpp
#include <bits/stdc++.h>
using namespace std;

struct Edge {
    int u, v;
    long long w;
    bool operator<(const Edge& other) const {
        return w < other.w;
    }
};

struct DSU {
    vector<int> parent, rank;
    DSU(int n) : parent(n), rank(n, 0) {
        iota(parent.begin(), parent.end(), 0);
    }
    int find(int x) {
        while (parent[x] != x) {
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }
    bool unite(int x, int y) {
        x = find(x), y = find(y);
        if (x == y) return false;
        if (rank[x] < rank[y]) swap(x, y);
        parent[y] = x;
        if (rank[x] == rank[y]) rank[x]++;
        return true;
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m, k;
    cin >> n >> m >> k;

    vector<Edge> edges(m);
    for (int i = 0; i < m; i++) {
        cin >> edges[i].u >> edges[i].v >> edges[i].w;
        edges[i].u--; edges[i].v--;
    }

    // 原图 MST
    sort(edges.begin(), edges.end());
    DSU dsu0(n);
    vector<Edge> mstEdges;
    long long baseCost = 0;
    for (const auto& e : edges) {
        if (dsu0.unite(e.u, e.v)) {
            mstEdges.push_back(e);
            baseCost += e.w;
        }
    }

    vector<long long> c(k);
    vector<vector<Edge>> townEdges(k); // 每个乡镇到城市的边(已排序)
    for (int j = 0; j < k; j++) {
        cin >> c[j];
        for (int i = 0; i < n; i++) {
            long long cost;
            cin >> cost;
            townEdges[j].push_back({n + j, i, cost});
        }
        sort(townEdges[j].begin(), townEdges[j].end());
    }

    long long ans = baseCost;

    // 枚举乡镇选择
    for (int mask = 1; mask < (1 << k); mask++) {
        vector<int> selected;
        long long activateCost = 0;
        for (int j = 0; j < k; j++) {
            if (mask >> j & 1) {
                selected.push_back(j);
                activateCost += c[j];
            }
        }
        int totalNodes = n + selected.size();

        // 多路归并 Kruskal
        DSU dsu(totalNodes);
        // 每个列表的当前指针
        vector<int> ptr(selected.size() + 1, 0);
        // 堆: (weight, list_type, list_index)
        // list_type: 0 原MST边,1~selected.size() 乡镇边
        priority_queue<tuple<long long, int, int>, 
                       vector<tuple<long long, int, int>>, 
                       greater<tuple<long long, int, int>>> pq;

        // 初始加入原MST边的最小边
        if (!mstEdges.empty()) {
            pq.push({mstEdges[0].w, 0, 0});
        }
        // 加入每个乡镇的最小边
        for (int idx = 0; idx < selected.size(); idx++) {
            int j = selected[idx];
            if (!townEdges[j].empty()) {
                pq.push({townEdges[j][0].w, idx + 1, 0});
            }
        }

        long long mstCost = 0;
        int connected = 0;

        while (!pq.empty() && connected < totalNodes - 1) {
            auto [weight, type, pos] = pq.top();
            pq.pop();

            Edge e;
            if (type == 0) {
                // 原MST边
                e = mstEdges[pos];
                // 加入下一条原MST边
                if (pos + 1 < mstEdges.size()) {
                    pq.push({mstEdges[pos + 1].w, 0, pos + 1});
                }
            } else {
                // 乡镇边
                int j = selected[type - 1];
                e = townEdges[j][pos];
                // 加入下一条这个乡镇的边
                if (pos + 1 < townEdges[j].size()) {
                    pq.push({townEdges[j][pos + 1].w, type, pos + 1});
                }
            }

            // 调整节点编号:乡镇编号从 n 开始连续分配
            int u = e.u, v = e.v;
            if (u >= n) {
                // 映射乡镇编号
                int townIndex = -1;
                for (int idx = 0; idx < selected.size(); idx++) {
                    if (selected[idx] == u - n) {
                        townIndex = idx;
                        break;
                    }
                }
                u = n + townIndex;
            }
            if (v >= n) {
                int townIndex = -1;
                for (int idx = 0; idx < selected.size(); idx++) {
                    if (selected[idx] == v - n) {
                        townIndex = idx;
                        break;
                    }
                }
                v = n + townIndex;
            }

            if (dsu.unite(u, v)) {
                mstCost += weight;
                connected++;
            }
        }

        ans = min(ans, activateCost + mstCost);
    }

    cout << ans << '\n';
    return 0;
}
```
:::
::::

## T3

:::problem 题意
给定 $n$ 对字符串 $|s_{i,1}| = |s_{i,2}|$,有 $q$ 次询问,每次询问给出 $t_1, t_2$
:::

:::definition
若 $|s_1|=|s_2|$,定义 $(d_1, d_2, l, r) = \mathrm{diff}(s_1, s_2)$:
- $l$ 是 $s_1, s_2$ 的最长公共前缀;
- $r$ 是 $s_1, s_2$ 的最长公共后缀;
- $s_i = l + d_i + r$。
:::

不难发现可以替换等价于

- $d_{1s} = d_{1t}, d_{2s} = d_{2t}$
- $l_t \in \operatorname{suffix}(l_s)$
- $r_t \in \operatorname{prefix}(r_s)$

打了个四层的超大字典树,是 $O(q(n+\mathrm{ans}))$ 的,虽然大样例的 $\mathrm{ans}$ 都不大,但是可能被别有用心的构造卡掉。

然后发现查前缀的时候忘记找边界情况了。改了。

但是最后发现最后一个大样例错了。目前时间:$3 \mathrm{h}\ 30 \mathrm{min}$,于是赶紧做第四题去了。

AI 很傻,生成不出对应的代码,所以不贴了。

:::caution 赛时唐
实际上只需构造字符串 $l \# d_1 \# d_2 \# r$ 即可变成字符串匹配问题。

鉴定为字符串问题做少了。
:::

## T4

`next_permutation` 打了个暴力。最后剩 $15\ \mathrm{min}$。

## 后

!!城市套路深,我要回农村……!!

唱首歌罢。

> 你愿相信什么 就把世界 看成什么样
>
> 偶尔难题 加点重量 越要轻轻的旋转
>
> 所以无论如何 记得保管 小小的光环
>
> 笑就好 哭也好 今天就是明天最好的陪伴

晚上堵车。晕车。复活之后 8 点半吃了晚饭。