Canadian Computing Olympiad (CCO) 2017 ~ 2025 做题记录

| 场次 | \(\quad\qquad\text{A}\qquad\quad\) | 完成情况 | \(\quad\qquad\text{B}\qquad\quad\) | 完成情况 | \(\quad\qquad\text{C}\qquad\quad\) | 完成情况 |f
| :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| \(\text{CCO 2017 Day 1}\) | \(\text{Vera and Trail Building}\) | \(\color{green}\checkmark\) | \(\text{Cartesian Conquest}\) | \(\color{green}\checkmark\) | \(\text{Vera and Modern Art}\) | \(\color{green}\checkmark\) |
| \(\text{CCO 2017 Day 2}\) | \(\text{Rainfall Capture}\) | \(\color{green}\checkmark\) | \(\text{Professional Network}\) | \(\color{green}\checkmark\) | \(\text{Shifty Grid}\) | \(\color{green}\checkmark\) |
| \(\text{CCO 2018 Day 1}\) | \(\text{Geese vs. Hawks}\) | \(\color{green}\checkmark\) | \(\text{Wrong Answer}\) | \(\color{green}\checkmark\) | \(\text{Fun Palace}\) | \(\color{green}\checkmark\) |
| \(\text{CCO 2018 Day 2}\) | \(\text{Gradient Descent}\) | \(\color{green}\checkmark\) | \(\text{Boring Lectures}\) | \(\color{green}\checkmark\) | \(\text{Flop Sorting}\) | \(\color{green}\checkmark\) |
| \(\text{CCO 2019 Day 1}\) | \(\text{Human Error}\) | \(\color{green}\checkmark\) | \(\text{Sirtet}\) | \(\color{green}\checkmark\) | \(\text{Winter Driving}\) | \(\color{green}\checkmark\) |
| \(\text{CCO 2019 Day 2}\) | \(\text{Card Scoring}\) | \(\color{green}\checkmark\) | \(\text{Marshmallow Molecules}\) | \(\color{green}\checkmark\) | \(\text{Bad Codes}\) | \(\color{green}\checkmark\) |
| \(\text{CCO 2020 Day 1}\) | \(\text{A Game with Grundy}\) | \(\color{green}\checkmark\) | \(\text{Exercise Deadlines}\) | \(\color{green}\checkmark\) | \(\text{Mountains and Valleys}\) | \(\color{green}\checkmark\) |
| \(\text{CCO 2020 Day 2}\) | \(\text{Travelling Salesperson}\) | \(\color{green}\checkmark\) | \(\text{Interval Collection}\) | \(\color{green}\checkmark\) | \(\text{Shopping Plans}\) | \(\color{green}\checkmark\) |

\(\color{blue}\mathbf{CCO\;2017\;Day\;1}\)

\(\text{Vera and Trail Building}\)

给定整数 \(K\),构造一个 \(V\) 个顶点 \(E\) 条边的连通无向图(\(1\le V,E\le5\times10^3\)),使得恰好有 \(K\) 个点对 \((a,b)\)\(1 \le a < b \le V\)),满足存在一条从 \(a\)\(b\) 再回到 \(a\) 的路径,且每条边最多经过一次。

\(\texttt{Data Range:}\;K\le10^7\texttt{; Time Limit: 1000ms; Memory Limit: 2048 MiB}\)

点对 \((a,b)\) 的个数等于图中每个边双连通分量的大小 \(s_i\) 对应的 \(\dbinom{s_i}{2}\) 之和。

每次贪心的选取出最大的 \(s_i\) 满足 \(\dbinom{s_i}{2}\le K(s_i\ge 2)\),并令 \(K\to K-\dbinom{s_i}{2}\),那么我们可以得到一种可能的 \(s_i\) 组成的序列。

为了从 \(s_i\) 反推出原图,我们构造若干个环,环的大小分别为 \(s_1,s_2,\cdots\),并在每个环之间任选一个点连至下一个环即可,时间复杂度为 \(\mathcal{O}(\sqrt K)\)

点数和边数数量为什么是符合限制的 对于每个 $K$,每次选取的 $s_i$ 的量级是 $\mathcal{O}(\sqrt{K})$ 的,$K$ 减去 $\dbinom{s_i}{2}$ 后的量级也是 $\mathcal{O}(\sqrt{K})$ 的。

所以 \(V\)\(E\) 的量级均为 \(\mathcal{O}(\sqrt{K}+\sqrt[4]{K}+\cdots)=\mathcal{O}(\sqrt{K})\)。更精确的,常数约为 \(\sqrt{2}\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

signed main() {
    int n, s = 0; scanf("%d", &n);
    vector<int> z;
    for (int i = 5000; i >= 2; --i) {
        while (n >= i * (i - 1) / 2) z.push_back(i), n -= i * (i - 1) / 2, s += i;
    }
    printf("%d %d\n", s, (int)z.size() - 1 + s);
    s = 0;
    for (auto x : z) {
        if (s) printf("%d %d\n", s, s + 1);
        for (int i = 1; i <= x; ++i) printf("%d %d\n", s + i, s + i % x + 1);
        s += x;
    }
    return 0;
}

\(\text{Cartesian Conquest}\)

有一个 \(N \times M\) 的矩形,你需要用若干个长宽比为 \(2:1\)\(1:2\) 且长宽均为整数的矩形合并成这个 \(N \times M\) 的矩形。每次合并操作为将一个矩形水平合并到当前矩形上(初始时为空),使得合并后仍然为矩形。求所需矩形数量的最小值和最大值,数据保证至少存在一种方案。

\(\texttt{Data Range:}\;N,M \le 10^8\texttt{; Time Limit: 2000ms; Memory Limit: 2048 MiB}\)

考虑分别计算最小值和最大值的答案,将合并的过程倒为划分的过程,记 \(f_{N,M}\) 表示 \(N\times M\) 的矩形的最小 / 最大划分次数,不妨假设 \(N\ge M\)

\(N<2M\) 时,我们暴力枚举该矩形长边的方向并进行转移;否则,我们根据当前求的是最小值还是最大值进行分类讨论。

  • 最小值:当 \(2\not\mid M\) 时,我们只能一直沿着短边放置 \(2M\times M\) 的矩形;当 \(2\mid M\) 的时,由于 \(4\)\(\dfrac{M}{2}\times M\) 的矩形可以用一个\(\dfrac{M}{2}\times M\) 的矩形来代替,所以只有至多两种情况,分别求解即可;
  • 最大值:当 \(2\not\mid M\) 时,我们只能一直沿着短边放置 \(2M\times M\) 的矩形;当 \(2\mid M\) 的时,我们一直沿着短边放置 \(\dfrac{M}{2}\times M\) 的矩形,因为 \(2M\times M\) 的矩形可以用 \(4\)\(\dfrac{M}{2}\times M\) 的矩形来代替。

加上记忆化后即可通过。

有关时间复杂度 $\texttt{DM::OJ}$ 上在该题的讨论区中有人声称本题时间复杂度上界为 $\mathcal{O}(\min(N,M)^{0.915})$,同一讨论的作者声称经程序测试证明复杂度大致是 $\mathcal{O}(\min(N,M)^{0.8})$ 的。
点击查看代码
#include <bits/stdc++.h>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/hash_policy.hpp>

using namespace std;
using namespace __gnu_pbds;

const int inf = 1e9;

template <>

struct tr1::hash< pair<int, int> > {
    size_t operator () (pair<int, int> x) const {
        return 998244353ll * x.first + 142857ll * x.second;
    }
};

gp_hash_table< pair<int, int>, int> f, g;

int dfs1(int n, int m) {
    if (!n || !m) return 0;
    if (n < m) swap(n, m);
    if (f.find({n, m}) != f.end()) return f[{n, m}];
    if (n >= 2 * m) {
        int ans = dfs1(n % (2 * m), m) + n / (2 * m);
        if (!(m & 1)) {
            ans = min(ans, dfs1(n % (m / 2) + m / 2 * 3, m) + n / (2 * m) + n % (2 * m) / (m / 2));
        }
        return f[{n, m}] = ans;
    }
    int ans = inf;
    if (!(n & 1)) ans = min(ans, dfs1(n, m - n / 2) + 1);
    if (!(m & 1)) ans = min(ans, dfs1(m, n - m / 2) + 1);
    return f[{n, m}] = ans;
}

int dfs2(int n, int m) {
    if (!n || !m) return 0;
    if (n < m) swap(n, m);
    if (g.find({n, m}) != g.end()) return g[{n, m}];
    if (n >= 2 * m) {
        if (m & 1) return dfs2(n % (2 * m), m) + n / (2 * m);
        int t = n / (m / 2) - 3;
        return g[{n, m}] = dfs2(n - (m / 2) * t, m) + t;
    }
    int ans = -inf;
    if (!(n & 1)) ans = max(ans, dfs2(n, m - n / 2) + 1);
    if (!(m & 1)) ans = max(ans, dfs2(m, n - m / 2) + 1);
    return g[{n, m}] = ans;
}

signed main() {
    int n, m; scanf("%d%d", &n, &m);
    printf("%d %d\n", dfs1(n, m), dfs2(n, m));
    return 0;
}

\(\text{Vera and Modern Art}\)

在平面直角坐标系上进行 \(N\) 次修改 \((x_i,y_i,v_i)\),记不大于 \(x_i\) 的最大的 \(2\) 的幂次为 \(a_i\),不大于 \(y_i\) 的最大的 \(2\) 的幂次为 \(b_i\),则会将所有 \((x_i+p\cdot a_i,y_i+q\cdot b_i)\)\(p,q\) 为非负整数)位置上的数加上 \(v_i\)。在修改结束后询问 \(Q\)\((r_i,c_i)\) 位置上的值。初始时所有位置上的值全为 \(0\)

\(\texttt{Data Range:}\;N,Q\le2\times10^5,1\le x_i,y_i,r_i,c_i\le V=10^{18}\texttt{; Time Limit: 4000ms; Memory Limit: 2048 MiB}\)

将平面直角坐标系的大小限制为 \(2^{60}\),则从高到低反转二进制位后,修改转化为矩形加,修改仍为单点修改,离散化后使用扫描线线段树维护即可,时间复杂度 \(\mathcal{O}((N+Q)\log V)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

signed main() {
    int n, q; scanf("%d%d", &n, &q);
    auto tr = [&](long long x) {
        long long y = 0;
        for (int i = 0; i < 60; ++i) {
            if ((x >> i) & 1) y |= 1ll << (59 - i);
        }
        return y;
    };
    vector<long long> disk;
    vector< array<long long, 3> > mdf, qry;
    for (int i = 1; i <= n; ++i) {
        long long x, y, v; scanf("%lld%lld%lld", &x, &y, &v);
        long long A = 63 - __builtin_clzll(x), B = 63 - __builtin_clzll(y);
        long long a = tr(x ^ (1ll << A)) + 1, b = tr(x ^ (1ll << A)) + (1ll << (60 - A)),
                  c = tr(y ^ (1ll << B)) + 1, d = tr(y ^ (1ll << B)) + (1ll << (60 - B));
        mdf.push_back({a, c, v}), mdf.push_back({a, d, -v});
        mdf.push_back({b, c, -v}), mdf.push_back({b, d, v});
        disk.push_back(c), disk.push_back(d);
    }
    sort(disk.begin(), disk.end()), disk.erase(unique(disk.begin(), disk.end()), disk.end());
    auto getId = [&](long long x) {
        return upper_bound(disk.begin(), disk.end(), x) - disk.begin();
    };
    vector<long long> res(q), s(disk.size() + 1);
    auto add = [&](int x, long long y) {
        for (; x <= (int)disk.size(); x += x & -x) s[x] += y;
    };
    auto query = [&](int x) {
        long long S = 0;
        for (; x; x -= x & -x) S += s[x];
        return S;
    };
    for (int i = 0; i < q; ++i) {
        long long x, y;
        scanf("%lld%lld", &x, &y);
        qry.push_back({tr(x), tr(y), i});
    }
    sort(mdf.begin(), mdf.end()), sort(qry.begin(), qry.end());
    auto it = mdf.begin();
    for (auto [a, b, c] : qry) {
        while (it != mdf.end() && it -> at(0) <= a) {
            add(getId(it -> at(1)), it -> at(2)), ++it;
        }
        res[c] = query(getId(b));
    }
    for (int i = 0; i < q; ++i) printf("%lld\n", res[i]);
    return 0;
}

\(\color{blue}\mathbf{CCO\;2017\;Day\;2}\)

\(\text{Rainfall Capture}\)

\(N\) 根柱子,其中第 \(i\) 根柱子长度为 \(h_i\),你可以任意排列这 \(N\) 根柱子,求出所有可能的盛水量(即 \(\sum\limits_{i=1}^{n}\left(\min(\max\limits_{j=1}^{i}h_j,\max\limits_{j=i}^{n}h_j)-h_i\right)\))。

\(\texttt{Data Range:}\;N\le500,h_i\le50\texttt{; Time Limit: 1000ms; Memory Limit: 2048 MiB}\)

不妨假设 \(h\) 单调不降。在最高的柱子的位置确定之后,考虑其两侧的柱子,那么我们只关心前后缀最大值以及它们的位置,因此我们可以将左侧的柱子和右侧的柱子 “归并” 到右侧。

那么第 \(i\) 根柱子上方的储水量只取决于它右侧数字的最大值,因此它上方的储水量只可能为 \(h_j-h_i(i\le j<N)\)。可以证明可能的储水量即为高度前 \(N-2\) 小的柱子对应的所有可能储水量的和,必要性显然。

充分性的构造性证明

假如 \(h_i\) 的贡献为 \(h_j-h_i\),若 \(i\ne j\),那么就在数轴上画一条左端点为 \(i\),右端点为 \(j\) 的线段。我们可以通过合并线段使得任意端点不同时作为线段的左端点和右端点。我们将所有不作为左端点的 \(h\) 按照降序排列,再将每条线段的左端点插入右端点以及上一个位置之间即可。

使用 bitset 优化背包 dp 即可,总时间复杂度 \(\mathcal{O}\left(\dfrac{N^2(\max a_i)^2}{w}\right)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int N = 505, M = 55;

int h[N];
bitset<N * M> f, tmp;

signed main() {
    int n; scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%d", &h[i]);
    sort(h + 1, h + n + 1);
    f[0] = 1;
    for (int i = 1; i <= n - 2; ++i) {
        tmp = f;
        for (int j = i + 1; j < n; ++j) {
            if (h[j] != h[j - 1]) tmp |= f << (h[j] - h[i]);
        }
        f = tmp;
    }
    for (int i = 0; i < N * M; i = f._Find_next(i)) {
        printf("%d%c", i, " \n"[f._Find_next(i) == N * M]);
    }
    return 0;
}

\(\text{Professional Network}\)

\(N\) 个物品,每个物品有参数 \(A_i\) 以及价格 \(B_i\),表示你可以花费 \(B_i\) 购买或是在已经有至少 \(A_i\) 个物品时免费获得这个物品,求购买全部物品的最少花费。

\(\texttt{Data Range:}\;N\le2\times10^5\texttt{; Time Limit: 2000ms; Memory Limit: 2048 MiB}\)

先假设购买了全部物品,再去掉免费获取的物品的价值和。

对于一个物品集合 \(S\),其可以作为最终的免费获取物品集合当且仅当对于任意 \(0\le i\le N\)\(S\) 中大于等于 \(N-i\) 的数的个数不超过 \(i\)

因此,我们可以将其转化为对于每一个 \(0\le i<N\),在全部 \(A_j\le i\) 的物品中选择一个变为免费(也可以不选),使用 Hall 定理可以证明这是充要的。

因此,我们从小到大枚举每个 \(i\),并维护一个价格的大根堆。每次将所有 \(A_j=i\) 的物品的价格加入堆中,如果堆非空,那么再弹出最贵的物品,表示免费获取这个物品。总时间复杂度 \(\mathcal{O}(N\log N)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

signed main() {
    int n, s = 0; scanf("%d", &n);
    vector< pair<int, int> > z(n);
    for (auto &[u, v] : z) {
        scanf("%d%d", &u, &v);
        s += v;
    }
    sort(z.begin(), z.end());
    auto it = z.begin();
    priority_queue<int> pq;
    for (int i = 0; i < n; ++i) {
        while (it != z.end() && (*it).first == i) {
            pq.push((*it++).second);
        }
        if (!pq.empty()) {
            s -= pq.top();
            pq.pop();
        }
    }
    printf("%d\n", s);
    return 0;
}

\(\text{Shifty Grid}\)

给定一个 \(N\times M\) 的矩阵,矩阵中的数构成了一个 \(0\sim NM-1\) 的排列。一次操作可以循环位移一行或一列。你需要在 \(10^5\) 次操作内使得矩阵上的数字按照从上到下、从左到右的顺序读取后依次为 \(0,1,\cdots,NM-1\)

\(\texttt{Data Range:}\;N,M\le100,2\mid N,2\mid M,\texttt{; Time Limit: 2000ms; Memory Limit: 2048 MiB}\)

行列编号从 \(0\) 开始。

考虑从左上到右下依次调整每个数的位置,假设现在要将 \((c,d)\) 上的数字移动到 \((a,b)\) 上,且不改变已经确定的数字。当 \(a\ne N-1\) 时,我们先通过适当调整使得 \(a\ne c\)\(b\ne d\),再进行如下操作:

  • 将第 \(b\) 列循环右移 \(c-a\) 位;
  • 将第 \(c\) 行循环右移 \(b-d\) 位;
  • 将第 \(b\) 列循环右移 \(a-c\) 位。

当矩阵只剩下最后一行未调整好时,假设我们要将 \((N-1,b)\) 上的数字移动到 \((N-1,a)\)\(b>a\)),且不改变已经确定的数字,任取偏移量 \(D\) 不为 \(N\) 的倍数,进行如下操作:

  • 将第 \(N-1\) 行循环右移 \(M-1-b\) 位;
  • 将第 \(M-1\) 列循环右移 \(D\) 位;
  • 将第 \(N-1\) 行循环右移 \(b-a\) 位;
  • 将第 \(M-1\) 列循环右移 \(-D\) 位;
  • 将第 \(N-1\) 行循环右移 \(a-b\) 位;
  • 将第 \(M-1\) 列循环右移 \(D\) 位;
  • 将第 \(N-1\) 行循环右移 \(b+1\) 位。

\(D\)\(\{-1,1\}\) 之间来回取值即可保证约 \(\dfrac{1}{2}\) 的正确率。因此只需要在所有操作开始前随机打乱矩阵,则期望 \(2\) 次即可出解。朴素实现的时间复杂度是 \(\mathcal{O}\left((NM)^2\right)\) 的。

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int N = 105;

int n, m, a[N][N], b[N];
vector< array<int, 3> > res;

void opt(int dir, int x, int y) {
    if (dir == 1) {
        y = (y % m + m) % m;
        res.push_back({dir, x, y});
        for (int i = 0; i < m; ++i) b[(i + y) % m] = a[x][i];
        for (int i = 0; i < m; ++i) a[x][i] = b[i];
    } else {
        y = (y % n + n) % n;
        res.push_back({dir, x, y});
        for (int i = 0; i < n; ++i) b[(i + y) % n] = a[i][x];
        for (int i = 0; i < n; ++i) a[i][x] = b[i];
    }
}

int solve() {
    for (int i = 0; i + 1 < n; ++i) {
        for (int j = 0; j < m; ++j) {
            if (a[i][j] == i * m + j) continue;
            for (int k = j + 1; k < m; ++k) {
                if (a[i][k] == i * m + j) opt(2, k, 1), opt(1, i + 1, (k == j + 1 ? 1 : -1)), opt(2, k, -1);
            }
            for (int k = i + 1; k < n; ++k) {
                if (a[k][j] == i * m + j) opt(1, k, 1);
            }
            for (int k = i + 1; k < n; ++k) {
                for (int l = 0; l < m; ++l) {
                    if (a[k][l] != i * m + j) continue;
                    opt(2, j, k - i), opt(1, k, j - l), opt(2, j, i - k);
                    break;
                }
            }
        }
    }
    int D = 1;
    for (int i = 0; i < m; ++i) {
        if (a[n - 1][i] == (n - 1) * m + i) continue;
        for (int j = i + 1; j < m; ++j) {
            if (a[n - 1][j] != (n - 1) * m + i) continue;
            opt(1, n - 1, m - 1 - j), opt(2, m - 1, D), opt(1, n - 1, j - i), opt(2, m - 1, -D);
            opt(1, n - 1, i - j), opt(2, m - 1, D), opt(1, n - 1, j + 1);
            D = -D;
            break;
        }
    }
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            if (a[i][j] != i * m + j) return 0;
        }
    }
    printf("%d\n", (int)res.size());
    for (auto [x, y, z] : res) printf("%d %d %d\n", x, y + 1, z);
    return 1;
}

int back[N][N];

signed main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) scanf("%d", &back[i][j]);
    }
    mt19937 rnd;
    while (1) {
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < m; ++j) a[i][j] = back[i][j];
        }
        res.clear();
        for (int i = 0; i < n; ++i) opt(1, i, rnd() % m);
        for (int i = 0; i < m; ++i) opt(2, i, rnd() % n);
        if (solve()) return 0;
    }
    return 0;
}

\(\color{blue}\mathbf{CCO\;2018\;Day\;1}\)

\(\text{Geese vs. Hawks}\)

给定两支球队 \(N\) 场比赛的得分以及胜负情况,按照时间顺序给出且无平局,但不一定是两支球队间的。求两支球队之间的比赛双方得分总和之和的最大值。

\(\texttt{Data Range:}\;N\le1000\texttt{; Time Limit: 1000ms; Memory Limit: 2048 MiB}\)

\(f_{i,j}\) 表示当前第一支球队考虑到了第 \(i\) 场比赛,第二支球队考虑到了第 \(j\) 场比赛的最大比赛得分和,转移可以从 \(f_{i-1,j}\)\(f_{i,j-1}\) 而来。当第一支球队的第 \(i\) 场比赛以及第二支球队的第 \(j\) 场比赛得分与胜负情况符合时还可以从 \(f_{i-1,j-1}+a_i+b_j\) 转移而来。总时间复杂度 \(\mathcal{O}(N^2)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int N = 1005;

int a[N], b[N], f[N][N];

signed main() {
    int n; scanf("%d", &n);
    string S, T;
    cin >> S, S = ' ' + S;
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
    cin >> T, T = ' ' + T;
    for (int i = 1; i <= n; ++i) scanf("%d", &b[i]);
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            f[i][j] = max(f[i - 1][j], f[i][j - 1]);
            if (S[i] != T[j] && a[i] != b[j] && (S[i] == 'W') == (a[i] > b[j])) {
                f[i][j] = max(f[i][j], f[i - 1][j - 1] + a[i] + b[j]);
            }
        }
    }
    printf("%d\n", f[n][n]);
    return 0;
}

\(\text{Wrong Answer}\)

构造一张 \(N(N\le100)\) 个点的完全图,边有 \([1,100]\) 以内的整数边权,A 和 B 初始时位于节点 \(1\),每个人每次操作可以走到任意编号比它大的节点,代价为这条边的边权。记使得除节点 \(1\) 以外每个节点都被经过恰好 \(1\) 次的最小代价为 \(Y\)

考虑如下贪心策略:从 \(2\)\(N\) 枚举 \(i\),如果 A 走这条边后 A 的代价和小于 B 走这条边后 B 的代价和则让 A 走到 \(i\),否则让 B 走到 \(i\)。记该种贪心策略得到的答案为 \(X\)

你需要构造一种方案使得 \(\dfrac{X}{Y}>96\)
\(\texttt{Time Limit: 1000ms; Memory Limit: 2048 MiB}\)

考虑让最优策略走的边全为 \(1\),且可以直观感受到边权更为 “极端” 的边更容易增大 \(\dfrac{X}{Y}\),也即我们需要用一些边权为 \(100\) 的边来 “引诱” 错解的平均分配策略得到极端劣解。

经观察,可以得到如下构造:取 \(N=100\),初始时令每条边的边权为 \(1\) 并将所有 \(j=i+2\)\(j=i+1,2\not\mid i\) 的边 \((i,j)(2\le i<j\le N)\) 的边权设置为 \(100\),即可让劣解几乎每次都取到边权为 \(100\) 的边,可以算得 \(X=9702,Y=99,\dfrac{X}{Y}=98>96\),可以通过。

总时间复杂度为 \(\mathcal{O}(N^2)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

signed main() {
    puts("100");
    for (int i = 1; i <= 100; ++i) {
        for (int j = i + 1; j <= 100; ++j) {
            printf("%d%c", (i != 1 && j - i <= 2 && (j - i == 2 || (i & 1)) ? 100 : 1), " \n"[j == 100]);
        }
    }
    return 0;
}

\(\text{Fun Palace}\)

\(N\) 个房间,其中第 \(i\) 个房间和第 \(i+1\) 个房间之间有门(\(1\le i<N\)),门打开当且仅当第 \(i\) 个房间中心有 \(a_i\) 个人或第 \(i+1\) 个房间中心有 \(b_i\) 个人,此时人可以在两个房间之间移动(注意的是,当人准备去往另一个房间的时候,它会离开房间的中心)。求总人数的最大值满足无法使第 \(1\) 个房间的人数 \(\ge e\)

\(\texttt{Data Range:}\;N\le1000,a_i,b_i,e\le 10^4,\texttt{; Time Limit: 1000ms; Memory Limit: 2048 MiB}\)

\(f_{i,j}\) 表示若同时考虑 \(N\) 间房间,则第 \(i\) 个房间最终至多有 \(j\) 个人,当前前 \(i\) 个房间总共至多有多少人,则初始有 \(f_{1,j}=j(j<e)\),答案即为最大的 \(f_{N,j}\)

考虑转移,观察人员在第 \(i\) 间房间以及第 \(i+1\) 间房间的门间的流通情况:

  • 如果 \(j\ge a_i+b_i\),那么相当于无限制流通,故有 \(f_{i,j}\to f_{i+1,j}\)
  • 否则,如果 \(j\ge a_i\),则此时可以从 \(i\) 单向流通到 \(i+1\) 至多 \(j-a_i\) 人,故有 \(f_{i,j}\to f_{i+1,j-a_i}\)
  • 否则,当另一侧的人数至少为 \(b_i\) 时,同理有 \(f_{i,j}+b_i\to f_{i+1,j+b_i}\);当另一侧的人数小于 \(b_i\) 时,有 \(f_{i,j}+k\to f_{i+1,k}(k<b_i)\)

总时间复杂度可以做到 \(\mathcal{O}\left(N\max(a_i,b_i,e)\right)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int N = 1005;

int f[N][20005], a[N], b[N];

signed main() {
    int n, m; scanf("%d%d", &n, &m);
    for (int i = 1; i < n; ++i) {
        scanf("%d%d", &a[i], &b[i]);
    }
    memset(f, -0x3f, sizeof(f));
    for (int i = 0; i < m; ++i) f[1][i] = i;
    for (int i = 1; i < n; ++i) {
        int mx = -2e9;
        for (int j = 0; j <= 20000; ++j) {
            if (f[i][j] < 0) continue;
            if (j < a[i]) {
                mx = max(mx, f[i][j]);
                f[i + 1][j + b[i]] = max(f[i + 1][j + b[i]], f[i][j] + b[i]);
            } else if (j < a[i] + b[i]) {
                f[i + 1][j - a[i]] = max(f[i + 1][j - a[i]], f[i][j]);
            } else {
                f[i + 1][j] = max(f[i + 1][j], f[i][j]);
            }
        }
        for (int j = 0; j < b[i]; ++j) {
            f[i + 1][j] = max(f[i + 1][j], mx + j);
        }
    }
    printf("%d\n", *max_element(f[n], f[n] + 20000 + 1));
    return 0;
}

\(\color{blue}\mathbf{CCO\;2018\;Day\;2}\)

\(\text{Gradient Descent}\)

有一个 \(R\)\(C\) 列的网格和 \(N\) 个棋子,每个棋子均摆在网格的一个位置,位置可能重叠。定义一个格子的分数为其到 \(N\) 个棋子的哈密顿距离之和,单次询问可以询问任意格子的分数。你需要在 \(75\) 次操作内询问得出所有格子的分数的最小值。

\(\texttt{Data Range:}\;R,C\le10^7,N\le100\texttt{; Time Limit: 1000ms; Memory Limit: 2048 MiB}\)

显然 \(x\) 轴分量和 \(y\) 轴分量独立,且每一维单谷(且除谷处相邻位置不等),因此可以使用二分解决。

具体的,对于一个维度,我们在二分时询问相邻的两个位置并根据大小关系来调整二分区间,询问次数约为 \(2\log R+2\log C\)

进一步的,我们可以同时在两个维度上二分,将原本的 \(4\) 次询问优化为 \(3\) 次,此时询问次数约为 \(3\log R\)(视 \(R,C\) 同阶),时间复杂度与询问次数相同。

点击查看代码
#include <bits/stdc++.h>

using namespace std;

int r, c;

int query(int x, int y) {
    if (x > r || y > c) return 0;
    printf("? %d %d\n", x, y);
    fflush(stdout);
    scanf("%d", &x);
    return x;
}

signed main() {
    scanf("%d%d%*d", &r, &c);
    int l1 = 2, r1 = r, l2 = 2, r2 = c, p1 = 1, p2 = 1;
    while (l1 <= r1 || l2 <= r2) {
        int m1 = (l1 <= r1 ? (l1 + r1) / 2 : p1), m2 = (l2 <= r2 ? (l2 + r2) / 2 : p2);
        int v1 = query(m1, m2);
        if (v1 < query(m1 + 1, m2)) p1 = m1, r1 = m1 - 1;
        else l1 = m1 + 1;
        if (v1 < query(m1, m2 + 1)) p2 = m2, r2 = m2 - 1;
        else l2 = m2 + 1;
    }
    printf("! %d\n", query(p1, p2));
    return 0;
}

\(\text{Boring Lectures}\)

给定长度为 \(N\) 的数列 \(a\) 以及 \(K\),接下来会进行 \(Q\) 次单点修改,在所有修改前以及每次修改后你需要求出在所有连续 \(K\) 个元素中最大值与次大值的和的最大值。
\(\texttt{Data Range:}\;N\le10^6,Q\le10^5\texttt{; Time Limit: 8000ms; Memory Limit: 2048 MiB}\)

显然求的即为最大的 \(a_x+a_y\) 满足 \(x\ne y\)\(|x-y|\le K\)

将数字 \(K\) 个分一块,则选取的两个数要么在同一块,要么在相邻块。可以证明必然存在一种选取方案使得其选取了至少一个某一段的最大值(若有多个最大值,则取最右侧的)。

证明

使用反证法。当两个数位于同一块时显然。

否则考虑相邻的两个块以及分别选取的位置 \(x,y(x<y)\),假设对应块中的最右侧最大值位置分别为 \(p,q\),那么必然有 \(a_p\ge a_x,a_q\ge a_y\)

由于 \((x,p)\) 的选择以及 \((y,q)\) 的选择均不优于 \((x,y)\),因此有 \(a_p\le a_y,a_q\le a_x\),所以 \(a_x=a_y=a_p=a_q\),此时可以改为选取数对 \((x,p)\)

因此使用线段树维护区间最大值即可,并且用 multiset 维护每一块的最大值对应的答案,每次修改只有本身所处块和相邻块对应值可能变化,总时间复杂度 \(\mathcal{O}((N+Q)\log N)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int N = 1e6 + 5;

int n, m, a[N];
pair<int, int> f[1 << 21];

void update(int k, int l, int r, int x) {
    if (l == r) {
        f[k] = {a[x], x};
        return;
    }
    int mid = (l + r) >> 1;
    if (x <= mid) update(k << 1, l, mid, x);
    else update(k << 1 | 1, mid + 1, r, x);
    f[k] = max(f[k << 1], f[k << 1 | 1]);
}

pair<int, int> query(int k, int l, int r, int x, int y) {
    if (l > y || r < x) return make_pair(-1, 0);
    if (l >= x && r <= y) return f[k];
    int mid = (l + r) >> 1;
    return max(query(k << 1, l, mid, x, y), query(k << 1 | 1, mid + 1, r, x, y));
}

int v1[N];

int gv(int x) {
    return a[x] + max(query(1, 1, n, max(x - m + 1, 1), x - 1), query(1, 1, n, x + 1, min(x + m - 1, n))).first;
}

signed main() {
    int q; scanf("%d%d%d", &n, &m, &q);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        update(1, 1, n, i);
    }
    multiset<int> S;
    for (int i = 1; (i - 1) * m < n; ++i) {
        int L = (i - 1) * m + 1, R = min(i * m, n);
        v1[i] = gv(query(1, 1, n, L, R).second);
        S.insert(v1[i]);
    }
    printf("%d\n", *--S.end());
    while (q--) {
        int x, y;
        scanf("%d%d", &x, &y);
        a[x] = y;
        update(1, 1, n, x);
        int t = (x - 1) / m + 1;
        for (int i = t - 1; i <= t + 1; ++i) {
            if (i >= 1 && (i - 1) * m < n) {
                S.erase(S.find(v1[i]));
                v1[i] = gv(query(1, 1, n, (i - 1) * m + 1, min(i * m, n)).second);
                S.insert(v1[i]);
            }
        }
        printf("%d\n", *--S.end());
    }
    return 0;
}

\(\text{Flop Sorting}\)

给定长度为 \(N\) 的排列 \(a\),每次操作可以交换一个区间内的最小值与最大值,在 \(3\times10^5\) 次操作内将其变为数列 \(b\)

\(\texttt{Data Range:}\;N\le4096\texttt{; Time Limit: 2000ms; Memory Limit: 2048 MiB}\)

由于操作可逆,所以考虑将 \(a\) 转化为排好序的中间态后再同理转化为 \(b\)

为了将 \(a\) 排序,我们考虑归并排序,此时考虑如何合并两个有序序列 \(p\)\(q\)

我们取出 \(p\)\(q\) 合并后的中位数,记其为 \(x\),考虑将 \(\le x\)\(>x\) 分离。发现错位的只有 \(p\) 的一个后缀和 \(q\) 的一个前缀,可以将这两段翻转(操作次数是 \(\mathcal{O}(\mathit{len})\) 的)后再将整体翻转即可。

此时问题变为了两个子问题,左右两侧都是由至多两个有序序列拼接而成的序列,递归即可,总时间复杂度和操作次数均为 \(\mathcal{O}(N\log^2N)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

vector< pair<int, int> > divide(vector<int> x) {
    if (is_sorted(x.begin(), x.end())) return {};
    if (x.size() == 2) return {{1, 2}};
    vector<int> y = x;
    nth_element(y.begin(), y.begin() + y.size() / 2 - 1, y.end());
    int m = y[y.size() / 2 - 1];
    int L = find_if(x.begin(), x.end(), [&](int z) {
        return z > m;
    }) - x.begin(),
        R = find_if(x.rbegin(), x.rend(), [&](int z) {
        return z <= m;
    }).base() - x.begin();
    vector< pair<int, int> > p;
    if (L < R) {
        int mid = find_if(x.begin() + L, x.end(), [&](int z) {
            return z <= m;
        }) - x.begin();
        auto rev = [&](int le, int ri) {
            vector< pair<int, int> > ans;
            for (int i = le, j = ri - 1; i < j; ++i, --j) ans.push_back({i + 1, j + 1});
            reverse(x.begin() + le, x.begin() + ri);
            return ans;
        };
        vector< pair<int, int> > a1 = rev(L, mid), a2 = rev(mid, R), a3 = rev(L, R);
        p.insert(p.end(), a1.begin(), a1.end());
        p.insert(p.end(), a2.begin(), a2.end());
        p.insert(p.end(), a3.begin(), a3.end());
    }
    auto it = stable_partition(x.begin(), x.end(), [&](int z) {
        return z <= m;
    });
    vector< pair<int, int> > p1 = divide(vector<int>(x.begin(), it)),
                             p2 = divide(vector<int>(it, x.end()));
    p.insert(p.end(), p1.begin(), p1.end());
    for (auto [a, b] : p2) p.push_back({a + (it - x.begin()), b + (it - x.begin())});
    return p;
}

vector< pair<int, int> > solve(vector<int> x) {
    if (is_sorted(x.begin(), x.end())) return {};
    if (x.size() == 2) return {{1, 2}};
    int mid = x.size() >> 1;
    vector< pair<int, int> > p1 = solve(vector<int>(x.begin(), x.begin() + mid)),
                             p2 = solve(vector<int>(x.begin() + mid, x.end()));
    sort(x.begin(), x.begin() + mid), sort(x.begin() + mid, x.end());
    vector< pair<int, int> > p3 = divide(x);
    for (auto [a, b] : p2) p1.push_back({a + mid, b + mid});
    p1.insert(p1.end(), p3.begin(), p3.end());
    return p1;
}

signed main() {
    int n; scanf("%d", &n);
    vector<int> S(n), T(n);
    for (auto &x : S) scanf("%d", &x);
    for (auto &x : T) scanf("%d", &x);
    vector< pair<int, int> > p1 = solve(S), p2 = solve(T);
    p1.insert(p1.end(), p2.rbegin(), p2.rend());
    printf("%d\n", (int)p1.size());
    for (auto [x, y] : p1) printf("%d %d\n", x, y);
    return 0;
}

\(\color{blue}\mathbf{CCO\;2019\;Day\;1}\)

\(\text{Human Error}\)

小 A 和小 B 玩游戏,游戏是在一个 \(R\times C\) 的网格中进行的。棋盘上每个位置要么为空,要么为小 A 的棋子,要么为小 B 的棋子。双方轮流操作,小 A 先行,并且每位玩家有给定误差常数 \(J\)(小 A)和 \(D\)(小 B)。

在一次操作中,行动方需要选择自己的一枚棋子并吃掉相邻的一枚棋子(可以是自己的),如果可行的操作数量不超过其误差常数则会在所有可能的行动中选取一种;否则行动方需要选择误差常数种操作并随机执行其一。

双方都想让自己的胜率最大,请求出小 A 的胜率,保留三位小数。

\(\texttt{Data Range:}\;R\times C\le13,J,D\le13\texttt{; Time Limit: 1000ms; Memory Limit: 2048 MiB}\)

考虑记忆化搜索,记 \(f_{S,w}\) 表示当前棋盘状态为 \(S\),当前行动方为 \(w\)\(w\)\(\texttt{A}\)\(\texttt{B}\)),在双方最优策略下当前行动方的最大胜率。

转移时只需枚举所有可能的操作,记 \(c\) 为误差常数与操作种数的较小值,在 \(c\) 个最优操作中随机选取一个执行即可,时间复杂度的上界为 \(\mathcal{O}(3^{RC}RC\log(RC))\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int dx[] = {-1, 1, 0, 0},
          dy[] = {0, 0, -1, 1};

int R, C, J, D;
map<string, double> M;

double query(string s, int w) {
    if (M.count(s)) return M[s];
    basic_string<double> z;
    for (int i = 0; i < R; ++i) {
        for (int j = 0; j < C; ++j) {
            if (s[i * C + j] != "JD"[w]) {
                continue;
            }
            for (int k = 0; k < 4; ++k) {
                int x = i + dx[k], y = j + dy[k];
                if (x >= 0 && x < R && y >= 0 && y < C && s[x * C + y] != '.') {
                    char t = s[x * C + y];
                    s[i * C + j] = '.';
                    s[x * C + y] = "JD"[w];
                    z += (1 - query(s, !w));
                    s[i * C + j] = "JD"[w];
                    s[x * C + y] = t;
                }
            }
        }
    }
    if (z.empty()) return M[s] = 0;
    sort(z.begin(), z.end(), greater<double>{});
    z = z.substr(0, (!w ? J : D));
    return M[s] = accumulate(z.begin(), z.end(), (double)0.0) / z.size();
}

signed main() {
    scanf("%d%d", &R, &C);
    string s;
    for (int i = 0; i < R; ++i) {
        string t;
        cin >> t;
        s += t;
    }
    scanf("%d%d", &J, &D);
    printf("%.3lf\n", query(s, 0));
    return 0;
}

\(\text{Sirtet}\)

给定一个 \(N\times M\) 的网格,格子有黑白两色,所有四连通的黑格子构成一个块。现在,你需要求出,如果这是一个俄罗斯方块游戏的当前局面,在受向下的重力的影响后,最后的网格状态。

\(\texttt{Data Range:}\;N\times M\le10^6\texttt{; Time Limit: 2000ms; Memory Limit: 2048 MiB}\)

首先先通过 dfs 搜索得出每一个块,考虑对于每一块记录下 \(f_i\) 表示当前块最终下落距离,则 \(f_i\) 不超过该连通块最下方格子到达底线的距离。

对于同处于第 \(i\) 列的两个不同的黑格子 \((j,i)\)\((k,i)\),不妨设 \(j<k\),假设它们分别位于块 \(a,b\),那么必然有 \(f_a<f_b+k-j\)

可以证明,当 \(f\) 符合条件的时候,每个 \(f_i\) 都存在确定的可能的最大值,且此时的 \(f_i\) 即为最后第 \(i\) 个块下落的高度。

并且注意到对于每一列,我们只需要考虑去除白格后所有相邻的黑格对,所以限制的总数是 \(\mathcal{O}(NM)\) 的,使用差分约束算法求解即可做到 \(\mathcal{O}(NM\log(NM))\)。进一步的,由于边权总和的级别为 \(\mathcal{O}(NM)\),因此可以使用 bfs 做到 \(\mathcal{O}(NM)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int N = 1e6 + 5;
const int dx[] = {-1, 1, 0, 0},
          dy[] = {0, 0, -1, 1};

vector<int> a[N], vis[N], b[N], res[N];
vector< pair<int, int> > wh[N];
int n, m, col;

void dfs(int x, int y) {
    vis[x][y] = col;
    wh[col].push_back(make_pair(x, y));
    for (int i = 0; i < 4; ++i) {
        int xx = x + dx[i], yy = y + dy[i];
        if (xx < 1 || xx > n || yy < 1 || yy > m || !a[xx][yy] || vis[xx][yy]) {
            continue;
        }
        dfs(xx, yy);
    }
}

vector< pair<int, int> > G[N];
int dis[N];

signed main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) {
        a[i].resize(m + 1);
        vis[i].resize(m + 1);
        b[i].resize(m + 1);
        res[i].resize(m + 1);
        string str; cin >> str;
        for (int j = 1; j <= m; ++j) {
            a[i][j] = str[j - 1] == '#';
        }
    }
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            if (a[i][j] && !vis[i][j]) {
                ++col;
                dfs(i, j);
            }
        }
    }
    for (int j = 1; j <= m; ++j) {
        for (int i = n - 1; i >= 1; --i) {
            if (a[i + 1][j]) {
                b[i][j] = i + 1;
            } else {
                b[i][j] = b[i + 1][j];
            }
        }
    }
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            if (!a[i][j]) {
                continue;
            }
            int k = b[i][j], id = k == 0 ? 0 : vis[k][j];
            G[id].push_back(make_pair(vis[i][j], (k == 0 ? n + 1 : k) - i - 1));
        }
    }
    memset(dis, 0x3f, sizeof(dis));
    priority_queue< pair<int, int>, vector< pair<int, int> >, greater< pair<int, int> > > pq;
    dis[0] = 0;
    pq.push(make_pair(0, 0));
    while (!pq.empty()) {
        int x = pq.top().second;
        if (dis[x] != pq.top().first) {
            pq.pop();
            continue;
        }
        pq.pop();
        for (int i = 0; i < G[x].size(); ++i) {
            int v = G[x][i].first;
            if (dis[x] + G[x][i].second < dis[v]) {
                dis[v] = dis[x] + G[x][i].second;
                pq.push(make_pair(dis[v], v));
            }
        }
    }
    for (int i = 1; i <= col; ++i) {
        for (int j = 0; j < wh[i].size(); ++j) {
            res[wh[i][j].first + dis[i]][wh[i][j].second] = 1;
        }
    }
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            putchar(".#"[res[i][j]]);
        }
        puts("");
    }
    return 0;
}

\(\text{Winter Driving}\)

给定一棵 \(N\) 个点的树,其中第 \(i\) 个点与第 \(P_i(P_i<i)\) 个点之间有连边(\(i\ge2\)),第 \(i\) 个点的点权为 \(A_i\),记 \(D\) 为每个点的度数的最大值。

现在,你要给这棵树的每一条边定向,假设 \(f(i,j)\) 表示节点 \(i\)\((1)\)\((0)\) 可以仅通过这些有向边到达节点 \(j\),你需要输出 \(\sum\limits_{i=1}^{N}\sum\limits_{j=1}^{N}f(i,j)A_iA_j\) 的最大值。

\(\texttt{Data Range:}\;N\le2\times10^5,D\le36\texttt{; Time Limit: 1000ms; Memory Limit: 2048 MiB}\)

假设树的任意一个带权重心为 \(\mathit{rt}\),可以使用调整法证明,对于 \(\mathit{rt}\) 的每一个子树,其中的所有边要么均为根向边,要么均为叶向边。

具体证明

考虑一条简单路径 \(a\to b\to c\to d(a\ne b\ne c\ne d)\)(允许经过反向边),如果 \(a\to b,c\to d\) 所走的边均为正向边且 \(b\to c\) 所走的边均为反向边,那么可以通过简单调整得到更优解。

因此,一定不存在这样的路径 \(a\to b\to c\to d\),故必然存在一个点 \(x\),使得对于 \(x\) 的每一个子树,其中的所有边要么均为根向边,要么均为叶向边。

\(x\) 不为 \(\mathit{rt}\) 时,显然有一边的子树的 \(A\) 之和会超过所有点点权和的一半,可以通过简单调整证明其一定不优。

因此,我们求出其每一个子树的大小,并尝试将其分为总和尽量平均的两组,则答案即为两组元素总和之积以及每个子树的贡献的和之和。

由于每个点的度数均不超过 \(D\),所以可以使用 Meet in Middle 解决,总时间复杂度为 \(O\left(N+D2^{\frac{D}{2}}\right)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int N = 2e5 + 5;

int a[N], p[N], s[N], sz[N], fl[N];

signed main() {
    int n; scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]), s[i] = a[i];
    for (int i = 2; i <= n; ++i) scanf("%d", &p[i]);
    for (int i = n; i >= 2; --i) s[p[i]] += s[i];
    for (int i = 1; i <= n; ++i) fl[i] = (s[1] - s[i] <= s[1] / 2);
    for (int i = 2; i <= n; ++i) fl[p[i]] &= (s[i] <= s[1] / 2);
    int o = find(fl + 1, fl + n + 1, 1) - fl;
    for (int i = 1; i <= n; ++i) sz[i] = s[i];
    for (int i = o, j = 0; i; i = p[i]) sz[i] = s[1] - j, j = s[i];
    vector<long long> t;
    if (p[o]) t.push_back(sz[p[o]]);
    for (int i = 1; i <= n; ++i) {
        if (p[i] == o) t.push_back(sz[i]);
    }
    long long res = -s[1];
    for (int i = 1; i <= n; ++i) res += 1ll * a[i] * sz[i];
    vector<long long> s1, s2;
    auto dfs = [&](auto &&self, int x, int y, long long S, vector<long long> &dt) {
        if (x == y) {
            dt.push_back(S);
            return;
        }
        self(self, x + 1, y, S, dt), self(self, x + 1, y, S + t[x], dt);
    };
    dfs(dfs, 0, t.size() / 2, 0, s1), dfs(dfs, t.size() / 2, t.size(), 0, s2);
    s1.push_back(-1e18), s2.push_back(1e18);
    sort(s1.begin(), s1.end());
    long long S = accumulate(t.begin(), t.end(), 0ll), mx = 0;
    auto upd = [&](long long v) {
        if (v >= 0 && v <= S) mx = max(mx, v * (S - v));
    };
    for (auto x : s2) {
        upd(x + *--lower_bound(s1.begin(), s1.end(), S / 2 - x));
        upd(x + *lower_bound(s1.begin(), s1.end(), S / 2 - x));
    }
    printf("%lld\n", res + mx);
    return 0;
}

\(\color{blue}\mathbf{CCO\;2019\;Day\;2}\)

\(\text{Card Scoring}\)

\(n\) 张卡牌和整数 \(k\),其中第 \(i\) 张牌上面写着一个不超过 \(n\) 的数字。现在从左往右考虑每张卡牌,可以取走或是不取,需要保证手上手牌上的数字相同。任意时刻可以进行结算,获得 \(x^{\frac{1}{2}k}\) 的得分并清除手牌,其中 \(x\) 为手牌数量。求出最大的可能得分,要求与标准答案的相对误差不超过 \(10^{-6}\)

\(\texttt{Data Range:}\;n\le10^6,2\le k\le 4\texttt{; Time Limit: 6000ms; Memory Limit: 2048 MiB}\)

记第 \(i\) 张卡牌的数字为 \(a_i\)

\(f_i\) 表示仅考虑前 \(i\) 张卡牌的最大得分,显然可以强制让第 \(i\) 张卡牌被选取,此时我们枚举 \(j(j\le i)\) 并用 \(f_{j-1}+x^{\frac{1}{2}k}\) 更新 \(f_i\),其中 \(x\)\(j\sim i\) 的卡牌数字为 \(a_i\) 的数量。

不妨强制让 \(a_j=a_i\),并记 \(c_i\) 表示 \(1\sim i\) 的卡牌中所写数字为 \(a_i\) 的卡牌的数量,则有转移式

\[f_i=\max\limits_{a_j=a_i,j\le i}\left(f_{j-1}+(c_i-c_j+1)^{\frac{1}{2}k}\right) \]

考虑对于每种相同的数字分别转移,由于 \(x^{\frac{1}{2}k}\) 关于 \(x\) 具有凸性,所以转移具有决策单调性,维护单调栈即可,时间复杂度 \(\mathcal{O}(n\log n)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int N = 1e6 + 5;

int n, a[N], s[N];
double m, f[N];
vector<int> val[N];
vector< pair<int, int> > S[N];

int calc(int x, int y) {
    int l = s[max(x, y)], r = (int)val[a[x]].size() - 1, res = 0;
    while (l <= r) {
        int mid = (l + r) >> 1;
        if (pow(s[val[a[x]][mid]] - s[x] + 1, m) + f[x - 1] > pow(s[val[a[x]][mid]] - s[y] + 1, m) + f[y - 1]) res = val[a[x]][mid], l = mid + 1;
        else r = mid - 1;
    }
    return res;
}

signed main() {
    scanf("%lf%d", &m, &n), m /= 2;
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]), s[i] = val[a[i]].size(), val[a[i]].push_back(i);
    for (int i = 1; i <= n; ++i) {
        while (S[a[i]].size() >= 2 && S[a[i]].back().second <= calc(i, S[a[i]].back().first)) S[a[i]].pop_back();
        S[a[i]].push_back({i, (S[a[i]].empty() ? n : calc(i, S[a[i]].back().first))});
        int l = 0, r = S[a[i]].size() - 1, res;
        while (l <= r) {
            int mid = (l + r) >> 1;
            if (i <= S[a[i]][mid].second) res = S[a[i]][mid].first, l = mid + 1;
            else r = mid - 1;
        }
        f[i] = pow(s[i] - s[res] + 1, m) + f[res - 1];
    }
    printf("%.10lf\n", f[n]);
    return 0;
}

\(\text{Marshmallow Molecules}\)

有一张 \(N\) 个点的无向简单图,初始时有 \(M\) 条边,不断进行 “如果 \(a,b\) 之间有边且 \(a,c\) 之间有边,并且 \(b,c\) 之间没有连边,那么在 \(b,c\) 之间连一条边(\(a<b<c\))”,问最终图的边数。

\(\texttt{Data Range}\;N,M\le10^5\texttt{; Time Limit: 4000ms; Memory Limit: 2048 MiB}\)

将无向图视作有向图,每条边由编号较小的点连向编号较大的点。

首先 \(1\) 号点不会有新的连边,因此我们可以在 \(1\) 号点的所有相邻点之间连一个团并删去 \(1\) 号点。

因此,我们可以按照从小到大枚举每一个节点 \(i\),将答案加上 \(i\) 的出边条数并将 \(i\) 的所有出边所到的点之间两两连边。

注意到,我们并不需要在所有出边所到的点之间均连边,我们可以取出其中编号最小的节点并将它与其它节点连边,这样在访问到这个节点的时候会将这些边 “传下去”。

我们使用 set 等数据结构维护每个点的出边,使用启发式合并维护边集,总时间复杂度为 \(\mathcal{O}(N\log^2N)\)(视 \(N,M\) 同阶,下同),若使用哈希表等数据结构即可做到 \(\mathcal{O}(N\log N)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 5;

set<int> S[N];

signed main() {
    int n, m; scanf("%d%d", &n, &m);
    for (int i = 1, x, y; i <= m; ++i) {
        scanf("%d%d", &x, &y), S[x].insert(y);
    }
    long long res = 0;
    for (int i = 1; i <= n; ++i) {
        if (S[i].empty()) continue;
        res += S[i].size();
        int x = *S[i].begin();
        S[i].erase(x);
        if (S[i].size() > S[x].size()) S[x].swap(S[i]);
        for (auto v : S[i]) S[x].insert(v);
    }
    printf("%lld\n", res);
    return 0;
}

\(\text{Bad Codes}\)

给定 \(N\) 个不同的 \(\texttt{01}\) 串,每一个串的长度均不超过 \(M\)。求出最短的字符串使得其有两同不同的方式划分为这些串的拼接(每个串可以使用任意非负整数次),输出字符串长度或无解。

\(\texttt{Data Range:}\;N,M\le50\texttt{; Time Limit: 1000ms; Memory Limit: 2048 MiB}\)

考虑两个串 \(S\)\(T\),初始时均为空,每次选取较短的一个串后面拼接上一个给定的串,所求即为让 \(S\)\(T\) 相等但拼接方式不同的所有方案中最小的 \(|S|\)

为了让拼接方式不同,可以初始枚举两个不同的串并钦定它们为初始的 \(S\)\(T\)。在拼接串的过程中,可以假设 \(S\)\(T\) 的前缀,而我们只关心 \(T\)\(|S|+1\) 位开始的部分,而这必然是某个串的后缀。

此时可以枚举 \(S\) 后拼接的字符串,其必然与 \(T\) 的对应后缀拥有前缀关系,此时可以将公共部分消去。将原状态以及新状态之间连一条有向边,长度为消去前的的 \(\min(|S|,|T|)\)

使用堆优化 Dijkstra 算法求解最短路即可做到 \(\mathcal{O}\left((NM)^2+NM(\log N+\log M)\right)\) 的时间复杂度。

点击查看代码
#include <bits/stdc++.h>

using namespace std;

int n;
string s[55];
map<string, int> V;
vector< tuple<string, string, int> > E;

void dfs(string x) {
    if (V[x]++ || x == "") return;
    int m = x.size();
    for (int i = 1; i <= n; ++i) {
        if ((int)s[i].size() >= m) {
            if (s[i].substr(0, m) == x) {
                E.push_back({x, s[i].substr(m), m});
                dfs(s[i].substr(m));
            }
        } else if (x.substr(0, s[i].size()) == s[i]) {
            E.push_back({x, x.substr(s[i].size()), s[i].size()});
            dfs(x.substr(s[i].size()));
        }
    }
}

vector< pair<int, int> > G[3005];
int dis[3005];

signed main() {
    scanf("%d%*d", &n);
    for (int i = 1; i <= n; ++i) cin >> s[i];
    sort(s + 1, s + n + 1, [&](string x, string y) {
        return x.size() < y.size();
    });
    V["S"] = 1;
    for (int i = 1; i <= n; ++i) {
        for (int j = i + 1; j <= n; ++j) {
            if (s[j].substr(0, s[i].size()) == s[i]) {
                E.push_back({"S", s[j].substr(s[i].size()), s[i].size()});
                dfs(s[j].substr(s[i].size()));
            }
        }
    }
    int cnt = 0;
    for (auto &[x, y] : V) y = ++cnt;
    for (auto [x, y, z] : E) G[V[x]].push_back({V[y], z});
    memset(dis, 0x3f, sizeof(dis));
    priority_queue< pair<int, int> > pq;
    pq.push({0, V["S"]});
    while (!pq.empty()) {
        auto [D, x] = pq.top(); pq.pop();
        if (dis[x] < -D) continue;
        dis[x] = -D;
        for (auto [v, w] : G[x]) pq.push({D - w, v});
    }
    if (dis[V[""]] > 1e8) puts("-1");
    else printf("%d\n", dis[V[""]]);
    return 0;
}

\(\color{blue}\mathbf{CCO\;2020\;Day\;1}\)

\(\text{A Game with Grundy}\)

平面直角坐标系的 \(x\) 轴上有 \(N\) 个人,其中第 \(i\) 个人站在 \((x_i,0)\),其视角为经过该点的斜率为 \(\pm\dfrac{v_i}{h_i}\) 的两条直线的中间部分。给定 \(L,R,Y\),对于每个 \(0\le i\le N\),求出整数 \(a\in[L,R]\) 的个数使得 \((a,Y)\) 被至多 \(i\) 个人看到。

\(\texttt{Data Range:}\;N\le10^5\texttt{; Time Limit: 1000ms; Memory Limit: 2048 MiB}\)

显然每个人看到的整点位置在直线 \(y=Y\) 上构成了一个区间,因此可以离散化后使用前缀和计算出每一段整点被看到的次数。

之后,我们只需再使用一次前缀和即可求出被至多 \(i\) 个人看到的整点数量,总时间复杂度 \(\mathcal{O}(N\log N)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 5;

map<int, int> M;
int s[N];

signed main() {
    int n, l, r, e; scanf("%d%d%d%d", &n, &l, &r, &e);
    for (int i = 1; i <= n; ++i) {
        int x, p, q;
        scanf("%d%d%d", &x, &p, &q);
        long double L = x - (long double)e * q / p, R = x + (long double)e * q / p;
        int tl = ceil(L + 1e-10), tr = floor(R - 1e-10);
        tl = max(tl, l), tr = min(tr, r);
        if (tl <= tr) {
            ++M[tl];
            --M[tr + 1];
        }
    }
    M[r + 1] += 0;
    int lst = l, z = 0;
    for (auto [x, y] : M) {
        s[z] += x - lst;
        lst = x;
        z += y;
    }
    for (int i = 0; i <= n; ++i) {
        if (i) s[i] += s[i - 1];
        printf("%d\n", s[i]);
    }
    return 0;
}

\(\text{Exercise Deadlines}\)

给定一个长度为 \(N\) 的数列 \(d(1\le d_i\le N)\),一次操作可以交换相邻的两个数,求最少次数使得 \(\forall 1\le i\le N,d_i\ge i\)

\(\texttt{Data Range:}\;N\le2\times10^5\texttt{; Time Limit: 1000ms; Memory Limit: 2048 MiB}\)

我们按照编号从大到小考虑每一个 \(d_i\),依次确定每一个 \(d_i\) 最后的位置 \(a_i\),则操作次数即为 \(a\) 的逆序对个数。

显然,可以将 \(a_i\) 设置为目前所有尚未用过且不超过 \(d_i\) 的位置中最大的一个,证明可以使用调整法。

使用 \(\texttt{set}\) 模拟该过程并用树状数组统计逆序对,总时间复杂度 \(\mathcal{O}(N\log N)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int N = 2e5 + 5;

int a[N], b[N], s[N];

signed main() {
    int n; scanf("%d", &n);
    set<int> S;
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]), S.insert(i);
    for (int i = n; i >= 1; --i) {
        if (*S.begin() > a[i]) return 0 & puts("-1");
        b[i] = *--S.upper_bound(a[i]), S.erase(b[i]);
    }
    long long res = 0;
    for (int i = n; i >= 1; --i) {
        for (int j = b[i]; j; j -= j & -j) res += s[j];
        for (int j = b[i]; j <= n; j += j & -j) ++s[j];
    }
    printf("%lld\n", res);
    return 0;
}

\(\text{Mountains and Valleys}\)

给定一棵 \(N(N\ge 4)\) 个点 \(M\) 条边的无向简单图,保证边权为 \(1\) 的边恰有 \(N-1\) 条且构成了一棵树,其余边的边权均 \(\ge\left\lceil\dfrac{N}{3}\right\rceil\),求出该图的最短哈密顿路径。

\(\texttt{Data Range:}\;N\le5\times10^5,M\le2\times10^6\texttt{; Time Limit: 7000ms; Memory Limit: 2048 MiB}\)

如果不经过任何一条边权大于 \(1\) 的边,则答案显然为 \(2(N-1)-D\),其中 \(D\) 为直径长度。

如果经过了任意一条边权大于 \(1\) 的边,不妨钦定这条边的两个端点之间的距离大于这条边的边权,此时必然有 \(D>\left\lceil\dfrac{N}{3}\right\rceil\),故答案不超过 \(2(N-1)-\left\lceil\dfrac{N}{3}\right\rceil-1\)。如果选择了两条这样的边,则答案至少为 \(2\times\left\lceil\dfrac{N}{3}\right\rceil+N-3\),一定不优。因此我们只会选择恰好一条这样的边,考虑枚举每条这样的边并计算此时的答案。

假设起点为 \(x\),终点为 \(y\),这条边的始终点为 \((a,b)\),可以证明此时最小的额外代价即为 \(2(N-1)-|x\rightsquigarrow y|-|a\rightsquigarrow b|+2\max(w-1,0)\),其中 \(w\) 为路径 \(x\rightsquigarrow y\) 和路径 \(a\rightsquigarrow b\) 的路径的边集的交集的大小。

假设树的直径为 \((S,T)\),不妨让这棵树以 \(S\) 为根,分别求出 \(a,b\)\(T\)\(\operatorname{LCA}\),记为 \(p\)\(q\),不妨假设 \(\operatorname{dep}(p)\le\operatorname{dep}(q)\),如果 \(\operatorname{dep}(q)\le\operatorname{dep}(p)+1\) 则取 \((x,y)=(S,T)\) 即可。

否则,如果 \((x,y)\) 同时经过 \(p\)\(q\),则 \((x,y)=(S,T)\);如果 \((x,y)\) 不经过 \(p\)\(q\),若 \((x,y)\) 不在 \((p,q)\) 之间可以分别以预处理出 \(S,T\) 为根求出每个子树内的直径长度,若 \((x,y)\)\((p,q)\) 之间则可以使用线段树维护。

如果 \((x,y)\) 只经过 \(p\)\(q\) 中其一,由于对称性不妨设为 \(p\),如果 \(p\rightsquigarrow q\) 的第一条边没有被 \(x\rightsquigarrow y\) 经过,则该贡献只与 \(a\) 有关,可以提前树形 dp 预处理。

如果经过了第一条边,可以将 \((a,b)\) 修改为 \((a,T)\),容易看出这不影响这种情况下的答案(因为 \(T\) 为深度最大的点),而该贡献只与 \(p\) 有关(因为 \(S\) 是与 \(p\) 最远的点,所以一定会选取 \(S\) 而不选取已经被经过一次的边)。

综上所述,可以在 \(\mathcal{O}(N)\) 树形 dp 预处理出若干信息后每次询问可以在 \(\mathcal{O}(\log N)\) 的复杂度内计算单条树边对应的最短哈密顿路径长度,总时间复杂度 \(\mathcal{O}(N+M\log N)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

constexpr int N = 5e5 + 5;

int n, S, T;
vector<int> G[N];
int dep[N];

namespace tree_lca {
    int dfn[N], dfn_c, pos[20][N];

    int get(int x, int y) {
        return dfn[x] < dfn[y] ? x : y;
    }

    void init() {
        auto dfs = [&](auto &&self, int x, int fa = 0) -> void {
            dfn[x] = ++dfn_c;
            pos[0][dfn_c] = fa;
            for (auto v : G[x]) {
                if (v != fa) self(self, v, x);
            }
        };
        dfs(dfs, S);
        for (int i = 1; (1 << i) <= n; ++i) {
            for (int j = 1; j + (1 << i) - 1 <= n; ++j) {
                pos[i][j] = get(pos[i - 1][j], pos[i - 1][j + (1 << (i - 1))]);
            }
        }
    }

    int LCA(int x, int y) {
        if (x == y) return x;
        x = dfn[x], y = dfn[y];
        if (x > y) swap(x, y);
        int k = 31 - __builtin_clz(y - x++);
        return get(pos[k][x], pos[k][y - (1 << k) + 1]);
    }
}

int LCA(int x, int y) {
    return tree_lca::LCA(x, y);
}

vector<int> chain;
int pre[N], nxt[N];

struct Info {
    int f[N], g[N], h[N], F[N], diam[N];

    void init(int rt) {
        auto dfs1 = [&](auto &&self, int x, int fa = 0) -> void {
            for (auto v : G[x]) {
                if (v == fa) continue;
                self(self, v, x);
                if (v != pre[x] && v != nxt[x]) {
                    f[x] = max(f[x], f[v] + 1);
                }
            }
        };
        dfs1(dfs1, rt);
        auto dfs2 = [&](auto &&self, int x, int fa = 0, int D = 1) -> void {
            h[x] = f[x] - D + 1;
            for (auto v : G[x]) {
                if (v != fa && (v == pre[x] || v == nxt[x])) {
                    g[v] = max(g[x], D + f[v]);
                    self(self, v, x, D + 1);
                    h[x] = max(h[x], h[v]);
                }
            }
        };
        dfs2(dfs2, rt);
        auto dfs3 = [&](auto &&self, int x, int fa = 0, int D = 0, int ty = 0, int now = 0) -> void {
            F[x] = max(now, D + f[x]);
            int mx = 0, sec = 0, ind = 0;
            for (auto v : G[x]) {
                if (v == fa || v == pre[x] || v == nxt[x]) continue;
                if (f[v] + 1 > mx) {
                    sec = mx, mx = f[v] + 1, ind = v;
                } else {
                    sec = max(sec, f[v] + 1);
                }
            }
            diam[x] = mx + sec;
            for (auto v : G[x]) {
                if (v == fa) continue;
                int nty = ty + (v != pre[x] && v != nxt[x]), tnow = now;
                if (v != pre[x] && v != nxt[x]) {
                    tnow = max(tnow, D + (v == ind ? sec : mx));
                }
                self(self, v, x, D + (nty > 1 ? -1 : 1), nty, tnow);
                if (v != pre[x] && v != nxt[x]) {
                    diam[x] = max(diam[x], diam[v]);
                }
            }
        };
        dfs3(dfs3, rt);
    }
} p[2];

struct Data {
    int mxL, mxR, mx;

    Data operator + (Data y) {
        Data res;
        res.mxL = max(mxL, y.mxL);
        res.mxR = max(mxR, y.mxR);
        res.mx = max({mx, y.mx, mxL + y.mxR});
        return res;
    }
} dt[1 << 20];

void build(int k, int l, int r, vector<Data> &info) {
    if (l == r) {
        dt[k] = info[l - 1];
        return;
    }
    int mid = (l + r) >> 1;
    build(k << 1, l, mid, info);
    build(k << 1 | 1, mid + 1, r, info);
    dt[k] = dt[k << 1] + dt[k << 1 | 1];
}

Data query(int k, int l, int r, int x, int y) {
    if (l >= x && r <= y) return dt[k];
    int mid = (l + r) >> 1;
    if (y <= mid) return query(k << 1, l, mid, x, y);
    if (x > mid) return query(k << 1 | 1, mid + 1, r, x, y);
    return query(k << 1, l, mid, x, y) + query(k << 1 | 1, mid + 1, r, x, y);
}

void init() {
    auto getFar = [&](int st) {
        int mxDep = 0, wh = 0;
        dep[st] = 1;
        auto dfsFar = [&](auto &&self, int x, int fa = 0) -> void {
            for (auto v : G[x]) {
                if (v != fa) dep[v] = dep[x] + 1, self(self, v, x);
            }
            if (dep[x] > mxDep) {
                mxDep = dep[x], wh = x;
            }
        };
        dfsFar(dfsFar, st);
        return wh;
    };
    S = getFar(1), T = getFar(S);
    auto dfsChain = [&](auto &&self, int x) {
        chain.push_back(x);
        if (x == S) return;
        for (auto v : G[x]) {
            if (dep[v] < dep[x]) self(self, v);
        }
    };
    dfsChain(dfsChain, T), reverse(chain.begin(), chain.end());
    for (int i = 1; i < (int)chain.size(); ++i) {
        pre[chain[i]] = chain[i - 1];
        nxt[chain[i - 1]] = chain[i];
    }
    tree_lca::init();
    p[0].init(S), p[1].init(T);
    vector<Data> info;
    for (int i = 0; i < (int)chain.size(); ++i) {
        Data x;
        x.mxL = p[0].f[chain[i]] + i + 2;
        x.mxR = p[0].f[chain[i]] - i;
        x.mx = p[0].diam[chain[i]];
        info.push_back(x);
    }
    build(1, 1, dep[T], info);
}

int dis(int x, int y) {
    return dep[x] + dep[y] - 2 * dep[LCA(x, y)];
}

int query(int x, int y) {
    int a = LCA(x, T), b = LCA(y, T);
    if (dep[a] > dep[b]) swap(x, y), swap(a, b);
    if (dep[a] == dep[b] || dep[a] + 1 == dep[b]) {
        return dep[T] - dep[S];
    }
    int res = dep[T] - dep[S] - (dep[b] - dep[a] - 1) * 2;
    if (a != S) res = max(res, p[0].g[pre[a]]);
    if (b != T) res = max(res, p[1].g[nxt[b]]);
    res = max(res, p[0].h[nxt[a]] + 2 * dep[a]);
    res = max(res, p[1].h[pre[b]] + 2 * (dep[S] + dep[T] - dep[b]));
    res = max({res, p[0].F[x], p[1].F[y]});
    res = max(res, query(1, 1, dep[T], dep[a] + 1, dep[b] - 1).mx);
    return res;
}

signed main() {
    int m; scanf("%d%d", &n, &m);
    vector< array<int, 3> > apd;
    for (int i = 1, x, y, w; i <= m; ++i) {
        scanf("%d%d%d", &x, &y, &w), ++x, ++y;
        if (w == 1) G[x].push_back(y), G[y].push_back(x);
        else apd.push_back({x, y, w});
    }
    init();
    int res = 2 * (n - 1) - dis(S, T);
    for (auto [x, y, w] : apd) {
        if (w * 2 <= n) {
            int D = dis(x, y);
            if (D > w) {
                res = min(res, 2 * (n - 1) - D + w - query(x, y));
            }
        }
    }
    printf("%d\n", res);
    return 0;
}

\(\color{blue}\mathbf{CCO\;2020\;Day\;2}\)

\(\text{Travelling Salesperson}\)

有一张 \(N\) 个点的无向完全图,边有红蓝两色,求出最短的哈密顿路径使得边的颜色只会交替一次。

\(\texttt{Data Range:}\;N\le2000\texttt{; Time Limit: 7000ms; Memory Limit: 2048 MiB}\)

答案显然有下界 \(N-1\),接下来我们归纳给出一个构造。

初始时,将路径设置为 \(\{1,2\}\),接下来按照顺序枚举全部编号 \(\ge 3\) 的节点 \(i\)

如果此时路径只有一种颜色,可以将 \(i\) 插入在路径末尾。否则,一定存在一个分界点 \(x\),使得 \(x\) 之前的边是一种颜色,\(x\) 之后的边是一种颜色。

如果 \((i,x)\) 之间的边的颜色与 \(x\) 和上一个点之间边的的颜色相同,我们将 \(i\) 插入在 \(x\) 的后面,否则插入在 \(x\) 的前面,则操作后的路径仍合法。

循环 \(N-2\) 轮即可完成构造,总时间复杂度 \(\mathcal{O}(N^2)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int N = 2005;

int G[N][N];

signed main() {
    int n; scanf("%d", &n);
    for (int i = 2; i <= n; ++i) {
        string S; cin >> S, S = ' ' + S;
        for (int j = 1; j < i; ++j) G[i][j] = G[j][i] = (S[j] == 'R');
    }
    for (int i = 1; i <= n; ++i) {
        vector<int> v1 = {i}, v2;
        for (int j = 1; j <= n; ++j) {
            if (j == i) continue;
            if (v2.empty()) {
                if (v1.size() == 1 || G[v1[0]][v1[1]] == G[v1.back()][j]) v1.push_back(j);
                else v2.push_back(j);
            } else {
                if (G[v1.back()][j] == G[v1[0]][v1[1]]) {
                    if (G[v2.back()][j] == !G[v1[0]][v1[1]]) v1.push_back(j);
                    else v1.push_back(j), v1.push_back(v2.back()), v2.pop_back();
                } else {
                    if (G[v1.end()[-2]][j] == G[v1[0]][v1[1]]) v2.push_back(v1.back()), v1.back() = j;
                    else v2.push_back(v1.back()), v2.push_back(j), v1.pop_back();
                    if (v1.size() == 1) v1.insert(v1.end(), v2.rbegin(), v2.rend()), v2.clear();
                }
            }
        }
        printf("%d\n", n);
        v1.insert(v1.end(), v2.rbegin(), v2.rend());
        for (int j = 0; j < n; ++j) printf("%d%c", v1[j], " \n"[j + 1 == n]);
    }
    return 0;
}

\(\text{Interval Collection}\)

有一个区间可重集合,\(Q\) 次操作,每次加入或者删除一个区间,保证区间的左右端点在 \([1,V]\) 内(对于所有区间 \([l,r]\) 都有 \(l<r\)),在每次操作结束后,你需要回答如下问题:

  • 选出至少一个区间,在最小化选出的区间的交的长度的条件下,最小化最短的能够包含所有选出的区间的区间长度,你只需要输出后者。

\(\texttt{Data Range:}\;Q\le5\times10^5,V=10^6\texttt{; Time Limit: 3500ms; Memory Limit: 2048 MiB}\)

考虑对于一个给定的区间集合如何计算答案,显然选出的区间的交的最小值为全部区间交的最小值。

如果全部区间的交不为空,记为 \([l,r]\),设左端点为 \(l\) 的区间的右端点最小值为 \(R\),右端点为 \(r\) 的区间的左端点最大值为 \(L\),显然答案为 \(R-L\)

否则,问题相当于选择出两个不相交的区间 \([a,b],[c,d](b\le c)\),最小化 \(d-a\),这可以对于值域使用线段树简单维护,总时间复杂度 \(\mathcal{O}(Q\log V)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int N = 1e6 + 5;

multiset<int> L, R;
multiset<int> tl[N], tr[N];

struct Info {
    int v1, v2, mn;
} p[1 << 21];

void build(int k, int l, int r) {
    p[k].v1 = 1e9, p[k].v2 = -1e9, p[k].mn = 1e9;
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(k << 1, l, mid), build(k << 1 | 1, mid + 1, r);
}

void update(int k, int l, int r, int x) {
    if (l == r) {
        p[k].v1 = (tl[l].empty() ? 1e9 : *tl[l].begin()), p[k].v2 = (tr[r].empty() ? -1e9 : *tr[r].rbegin()), p[k].mn = p[k].v1 - p[k].v2;
        return;
    }
    int mid = (l + r) >> 1;
    if (x <= mid) update(k << 1, l, mid, x);
    else update(k << 1 | 1, mid + 1, r, x);
    p[k].v1 = min(p[k << 1].v1, p[k << 1 | 1].v1);
    p[k].v2 = max(p[k << 1].v2, p[k << 1 | 1].v2);
    p[k].mn = min({p[k << 1].mn, p[k << 1 | 1].mn, p[k << 1 | 1].v1 - p[k << 1].v2});
}

signed main() {
    int q; scanf("%d", &q);
    build(1, 1, 1000000);
    while (q--) {
        string op; cin >> op;
        int c = (op == "A" ? 1 : -1), l, r; scanf("%d%d", &l, &r);
        if (c == 1) L.insert(l), R.insert(r), tl[l].insert(r), tr[r].insert(l);
        else L.erase(L.find(l)), R.erase(R.find(r)), tl[l].erase(tl[l].find(r)), tr[r].erase(tr[r].find(l));
        update(1, 1, 1000000, l), update(1, 1, 1000000, r);
        if (*L.rbegin() >= *R.begin()) {
            printf("%d\n", p[1].mn);
        } else {
            printf("%d\n", *tl[*L.rbegin()].begin() - *tr[*R.begin()].rbegin());
        }
    }
    return 0;
}

\(\text{Shopping Plans}\)

\(N\) 个商品,共有 \(M\) 种,其中第 \(i\) 个商品的种类为 \(a_i\),价格为 \(c_i\)。你需要购买若干个物品,其中第 \(i\) 种物品购买的个数要在 \([x_i,y_i]\) 之间,请求出所有购买方案中前 \(K\) 少的花费(如有多种方案算入多次),若不存在输出 \(-1\)

\(\texttt{Data Range:}\;N,M,K\le2\times10^5\texttt{; Time Limit: 2000ms; Memory Limit: 2048 MiB}\)

考虑该问题的子问题:只有 \(1\) 种物品,选出 \([L,R]\) 个元素,求前 \(K\) 小花费。

将所有方案列出来,考虑将它们组织成一个森林结构,要求父节点的花费小于等于子节点。可以使用优先队列维护当前所有待考虑方案,初始时只包含所有树根,进行 \(K\) 次操作,每次选出花费最小的方案弹出并将其所有子节点对应的方案加入,则第 \(i\) 次操作弹出的方案花费即为第 \(i\) 小的方案的花费和。

为了保证时间复杂度,构造的时候需要保证每个节点的子结点个数为常数,并且每个状态可以用常数个变量表示。

考虑构造以下树形结构:先将物品按照价格从小到大排序,令根节点为所有选取前 \(i\in[L,R]\) 个物品的方案。考虑按照从右到左的顺序调整选取的物品,记录四元组 \((S,\mathit{cur},\mathit{lim},\mathit{pos})\) 表示当前方案价值和、在考虑的物品序号、能移动到的最右端、以及当前位置,初始时 \(\mathit{cur}=\mathit{pos}=i,\mathit{lim}=\infty\)。此时前 \(\mathit{cur}-1\) 个物品还没有进行移动而第 \(\mathit{cur}\) 个物品之后的选取已经确定。注意两种方案对应的四元组相同不能说明方案相同。

其子节点有至多 \(2\) 个:

  • \(\mathit{pos}\ne\mathit{lim}\),则可将第 \(\mathit{cur}\) 个物品的选择往右移一位;
  • \(\mathit{cur}\ne1\),则可将 \(\mathit{cur}\) 减去 \(1\) 并立即将第 \(\mathit{cur}-1\) 个物品的选择往右移一位。

容易发现每次操作的 \(\Delta S\) 均非负且每种方案对应的节点都恰有一个父节点。

考虑该问题的另一个子问题:有 \(M\) 种物品,每种物品只能选恰好 \(1\) 个,求前 \(K\) 小花费。

先将每个物品的内部的花费按照升序排序,再对于物品之间按照 \(f(x,2)-f(x,1)\) 升序排序,其中 \(f(i,j)\) 表示第 \(i\) 类物品中第 \(j\) 便宜的价格,若不存在则为 \(\infty\)

初始时,根节点为每种物品都选 \(f(x,1)\) 的方案,其子节点只有一个,为将 \(f(1,1)\) 替换为 \(f(2,1)\) 的方案。考虑按照编号从小到大的顺序调整选区的物品,记录三元组 \((S,\mathit{cur},\mathit{pos})\) 表示当前方案价值和、在考虑的种类编号、以及 \(\mathit{cur}\) 的当前位置,初始时 \(\mathit{cur}=1,\mathit{pos}=1\)。此时前 \(\mathit{cur}-1\) 种物品已经考虑完毕而第 \(\mathit{cur}\) 种物品之后的选取还未更改。类似的,两种方案对应的三元组相同不能说明方案相同。

其子节点有至多 \(3\) 个:

  • 将第 \(\mathit{cur}\) 种物品选取的 \(f(\mathit{cur},\mathit{pos})\) 改为 \(f(\mathit{cur},\mathit{pos}+1)\)
  • 如果 \(\mathit{cur}\ne M\),则可将 \(\mathit{cur}\) 加上 \(1\) 并立即将第 \(\mathit{cur}+1\) 类物品的选择改为 \(f(\mathit{cur}+1,2)\)
  • 如果 \(\mathit{cur}\ne M\)\(\mathit{pos}=2\),则可将 \(f(\mathit{cur},2)\) 改回 \(f(\mathit{cur},1)\),并将 \(\mathit{cur}\) 加上 \(1\) 并立即将第 \(\mathit{cur}+1\) 类物品的选择改为 \(f(\mathit{cur}+1,2)\)

同样的可以得到每次操作的 \(\Delta S\) 均非负且每种方案对应的节点都恰有一个父节点。

为了解决原问题,我们将上述两个子问题的算法进行拼接,将第一个子问题视作黑盒并执行第二个子问题的算法,每次需要计算 \(f\) 时调用黑盒即可,总时间复杂度 \(\mathcal{O}(N\log N)\)(视 \(N,M,K\) 同阶)。

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int N = 2e5 + 5;
const long long inf = 1e18;

struct Items {
    vector<int> a;
    int L, R;

    struct Info {
        long long S;
        int cur, lim, pos;

        Info(long long _S, int _cur, int _lim, int _pos) {
            S = _S, cur = _cur, lim = _lim, pos = _pos;
        }

        bool operator < (const Info &y) const {
            return S > y.S;
        }
    };

    priority_queue<Info> pq;

    void init() {
        sort(a.begin(), a.end());
        vector<long long> sum = {0};
        for (auto x : a) sum.push_back(x);
        partial_sum(sum.begin(), sum.end(), sum.begin());
        for (int i = L; i <= min(R, (int)a.size()); ++i) {
            pq.push(Info(sum[i], i, a.size(), i));
        }
    }

    vector<long long> ans;

    void extend() {
        if (pq.empty()) {
            ans.push_back(inf);
            return;
        }
        Info x = pq.top(); pq.pop();
        ans.push_back(x.S);
        if (x.cur == 0) return;
        if (x.pos != x.lim) {
            pq.push(Info(x.S + a[x.pos] - a[x.pos - 1], x.cur, x.lim, x.pos + 1));
        }
        if (x.cur != 1 && x.pos != x.cur) {
            pq.push(Info(x.S + a[x.cur - 1] - a[x.cur - 2], x.cur - 1, x.pos - 1, x.cur));
        }
    }

    long long operator [](int pos) {
        while ((int)ans.size() < pos) extend();
        return ans[pos - 1];
    }
} p[N];

struct Info {
    long long S;
    int cur, pos;

    Info(long long _S, int _cur, int _pos) {
        S = _S,  cur = _cur, pos = _pos;
    }

    bool operator < (const Info &y) const {
        return S > y.S;
    }
};

priority_queue<Info> pq;

signed main() {
    int n, m, k; scanf("%d%d%d", &n, &m, &k);
    for (int i = 1, x, y; i <= n; ++i) {
        scanf("%d%d", &x, &y);
        p[x].a.push_back(y);
    }
    for (int i = 1; i <= m; ++i) {
        scanf("%d%d", &p[i].L, &p[i].R);
        p[i].init();
    }
    vector< pair<long long, int> > tord;
    long long ini = 0;
    for (int i = 1; i <= m; ++i) {
        ini = min(ini + p[i][1], inf);
        tord.push_back({p[i][2] - p[i][1], i});
    }
    sort(tord.begin(), tord.end());
    vector<int> ord;
    for (int i = 0; i < m; ++i) ord.push_back(tord[i].second);
    pq.push(Info(ini, 0, 1));
    for (int i = 1; i <= k; ++i) {
        Info now = pq.top(); pq.pop();
        if (now.S >= inf) {
            for (int j = i; j <= k; ++j) puts("-1");
            return 0;
        }
        printf("%lld\n", now.S);
        if (now.cur == 0 && now.pos == 1) {
            pq.push(Info(now.S + p[ord[now.cur]][2] - p[ord[now.cur]][1], now.cur, now.pos + 1));
        } else {
            pq.push(Info(now.S + p[ord[now.cur]][now.pos + 1] - p[ord[now.cur]][now.pos], now.cur, now.pos + 1));
            if (now.cur != m - 1) {
                pq.push(Info(now.S + p[ord[now.cur + 1]][2] - p[ord[now.cur + 1]][1], now.cur + 1, 2));
                if (now.pos == 2) {
                    pq.push(Info(now.S + (p[ord[now.cur + 1]][2] - p[ord[now.cur + 1]][1]) - (p[ord[now.cur]][2] - p[ord[now.cur]][1]), now.cur + 1, 2));
                }
            }
        }
    }
    return 0;
}

\(\color{blue}\mathbf{CCO\;2021\;Day\;1}\)

\(\text{Swap Swap Sort}\)

给定一个长度为 \(N\) 的数列 \(a\),保证 \(a_i\)\([1,K]\) 之间的整数。有排列 \(P\),初始时为单位排列,进行 \(Q\) 次操作,每次操作给定 \(j\),表示交换 \(P_j\) 以及 \(P_{j+1}\),在每次操作后,你需要求出以下问题的答案:

  • 至少需要进行多少次交换 \(a\) 中相邻两个数的操作,才能使得对于任意 \(1\le i<j\le K\),均有 \(P_i\)\(a\) 中的每次出现均在任意 \(P_{j}\) 之前。

\(\texttt{Data Range:}\;K\le N\le10^5,Q\le10^6\texttt{; Time Limit: 3000ms; Memory Limit: 2048 MiB}\)

原问题的答案显然为将排列 \(P\) 视作数字的偏序顺序后数列 \(a\) 的逆序对个数,初始时的答案即为 \(a\) 的逆序对数量,而为了计算每次答案的增量,我们只需要回答 \(Q\)\(\sum\limits_{i=1}^{N}\sum\limits_{k=i+1}^{N}[a_i=P_j][a_k=P_{j+1}]\) 即可。

记数字 \(x\)\(a\) 中的出现次数为 \(c_x\),每次回答询问显然可以通过枚举其中一个数的位置再在另一个数的出现位置中二分做到 \(\mathcal{O}(\min(c_{P_j},c_{P_{j+1}})\log N)\) 的时间复杂度。

将询问记忆化,由于 \(\sum\limits_{i=1}^{K}c_i=N\),因此可以分析出此时的时间复杂度为 \(\mathcal{O}(N\sqrt{Q}\log N)\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

signed main() {
    int n, m, q; scanf("%d%d%d", &n, &m, &q);
    vector<int> a(n), s(m + 1), cnt(m);
    vector< vector<int> > pos(m);
    long long res = 0;
    int id = 0;
    for (auto &x : a) {
        scanf("%d", &x), ++cnt[--x], pos[x].push_back(id++);
        for (int i = x + 2; i <= m; i += i & -i) res += s[i];
        for (int i = x + 1; i; i -= i & -i) ++s[i];
    }
    vector<int> per(m);
    iota(per.begin(), per.end(), 0);
    map< pair<int, int>, long long> M;
    auto query = [&](int x, int y) {
        if (!cnt[x] || !cnt[y]) return 0ll;
        int ty = (cnt[x] > cnt[y]);
        if (cnt[x] > cnt[y]) swap(x, y);
        long long S = 0;
        if (M.count({x, y})) S = M[{x, y}];
        else {
            for (auto v : pos[x]) S += lower_bound(pos[y].begin(), pos[y].end(), v) - pos[y].begin();
            M[{x, y}] = S;
        }
        if (ty) S = 1ll * cnt[x] * cnt[y] - S;
        return S;
    };
    while (q--) {
        int x; scanf("%d", &x);
        res += 1ll * cnt[per[x - 1]] * cnt[per[x]] - 2 * query(per[x - 1], per[x]);
        swap(per[x - 1], per[x]);
        printf("%lld\n", res);
    }
    return 0;
}

\(\text{Weird Numeral System}\)

给定集合 \(a\),保证 \(a\) 中元素绝对值均不超过 \(M\)。给定 \(K\) 以及 \(Q\) 个绝对值在 \(10^{18}\) 以内的整数 \(n\),需要找到任意数列 \(d_0,d_1,\cdots,d_{m-1}\) 满足 \(d_i\in a\)\(\sum\limits_{i=0}^{m-1}d_iK^i=n\) 或输出无解。

\(\texttt{Data Range:}\;K\le10^6,M\le2500,Q\le5\texttt{; Time Limit: 1500ms; Memory Limit: 2048 MiB}\)

显然这等价于进行若干次操作,每次选取 \(x\in a\)\(K|(n-x)\) 并将 \(n\to\dfrac{n-x}{K}\),将每次操作选取的 \(x\) 按顺序拼接即可得到所求的数列 \(d\)。使用记忆化后可以通过。

点击查看代码
#include <bits/stdc++.h>

using namespace std;

signed main() {
    int k, q, D, M; scanf("%d%d%d%d", &k, &q, &D, &M);
    vector<int> usd(M * 2 + 1);
    for (int i = 1, x; i <= D; ++i) scanf("%d", &x), usd[x + M] = 1;
    unordered_map<long long, int> vis;
    auto dfs = [&](auto &&self, long long x) -> vector<int> {
        if (abs(x) <= M && usd[x + M]) return {(int)x};
        if (vis.count(x) && vis[x] == 1) return {};
        vis[x] = 1;
        for (int i = -M; i <= M; ++i) {
            if (!usd[i + M] || (x - i) % k || (x - i) / k == x) continue;
            vector<int> ans = self(self, (x - i) / k);
            if (!ans.empty()) {
                ans.push_back(i), vis[x] = 0;
                return ans;
            }
        }
        return {};
    };
    while (q--) {
        long long x; scanf("%lld", &x);
        vector<int> ans = dfs(dfs, x);
        if (ans.empty()) puts("IMPOSSIBLE");
        else {
            for (int i = 0; i < (int)ans.size(); ++i) printf("%d%c", ans[i], " \n"[i + 1 == (int)ans.size()]);
        }
    }
    return 0;
}
posted @ 2025-06-30 21:56  hhoppitree  阅读(279)  评论(2)    收藏  举报