2025春训第二十六场

StarStar
6 min read

题面可以看这里.

A. 数列计数

最害怕的数学题。

\(\prod_{i=1}^{n}\binom{a_i}{b_i} \) 等价于对于每一个 i,都有 \(\binom{a_i}{b_i}\) 为奇数。

对于 \(\binom{n}{k}\),根据(ChatGPT指出的)卢卡斯定理,取 p = 2 有

\[\binom{n}{k} \equiv \binom{n\ \text{mod}\ 2}{k\ \text{mod}\ 2} \binom{\lfloor\frac{n}{2}\rfloor}{1}\ (\text{mod}\ 2)\]

右边这一项同理可以不断展开,最终等价于**取出了 n 和 k 的所有二进制位算组合数再乘积。**显然要想保证是奇数,只能 k 的每个二进制位都不比 n 大。

对于每一项做一次数位 dp,统计每一位都比 \(a_i\) 小的数的个数即可,时间复杂度是 \(\Theta(n\log 10^9)\)。

#include <iostream>

using namespace std;

const int N = 100010;
const int MOD = 998244353;
int a[N];
long long f[31][2]; // 第i位,是否顶上界

int dp(int a, int l) {
    f[30][1] = 1;
    for (int i = 29; i >= 0; --i) {
        if (a >> i & 1) { // 可选 1 和 0
            if (l >> i & 1) {
                f[i][1] = f[i + 1][1];
                f[i][0] = (f[i + 1][0] * 2 + f[i + 1][1]) % MOD;
            }
            else {
                f[i][1] = f[i + 1][1];
                f[i][0] = f[i + 1][0] * 2 % MOD;
            }
        }
        else { // 只能选 0 
            if (l >> i & 1) {
                f[i][1] = 0;
                f[i][0] = (f[i + 1][0] + f[i + 1][1]) % MOD;
            }
            else {
                f[i][1] = f[i + 1][1];
                f[i][0] = f[i + 1][0];
            }
        }
    }
    return (f[0][0] + f[0][1]) % MOD;
}

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        int n;
        scanf("%d", &n);
        for (int i = 1; i <= n; ++i) {
            scanf("%d", &a[i]);
        }
        long long res = 1;
        for (int i = 1; i <= n; ++i) {
            int l;
            scanf("%d", &l);            
            res = (res * dp(a[i], min(l, a[i]))) % MOD;
        }
        printf("%lld\n", res);
    }
    return 0;
}

D. 弯曲筷子

这道题需要发现一个小结论,显然我当时没发现……

先对 c 数组排序一下,排序后一根筷子只可能和他相邻的或者隔一个的筷子配对。所以问题就转化成了一个线性dp 的问题。对于每一个 i 只用考虑 i - 1 和 i - 2. dp时主要需要考虑兜底,即如何强制选上必须选的,状态转移方程很简单,可以参考下面的代码。

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 300010;

pair<int, bool> c[N];
long long f[N][2];

long long p2(long long a) {
    return a * a;
}

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        int n, m;
        scanf("%d%d", &n, &m);
        memset(f, 0x3f, sizeof(long long) * (n * 2 + 1));
        for (int i = 1; i <= n; ++i) {
            scanf("%d", &c[i].first);
            c[i].second = false;
        }
        for (int i = 1; i <= m; ++i) {
            int t;
            scanf("%d", &t);
            c[t].second = true;
        }
        f[0][1] = 0;
        sort(c + 1, c + n + 1);
        int pre = 0;
        for (int i = 1; i <= n; ++i) {
            if (c[i - 1].second) {
                f[i][0] = f[i - 1][1]; // 一定要配对
                f[i][1] = f[i - 1][0] + p2(c[i].first - c[i - 1].first);
            }
            else {
                f[i][0] = min(f[i - 1][0], f[i - 1][1]); // 随意
                if (i >= 2) f[i][1] = min(f[i - 1][0] + p2(c[i].first - c[i - 1].first), f[i - 2][0] + p2(c[i].first - c[i - 2].first));
            }
        }
        if (c[n].second) printf("%lld\n", f[n][1]);
        else printf("%lld\n", min(f[n][0], f[n][1]));
    }
    return 0;
}

E. 修复公路

我感觉这应该是最水的一道题,我们需要用最小的代价使不连通的图连通,显然就是把本来不连通的连通块作为节点建一棵树,答案是连通块数量 - 1.

需要主意别把初始化写挂😭

#include <iostream>
#include <vector>
#include <queue>
#include <cstring>

using namespace std;

const int N = 300010;

vector<int> ed[N];
bool vis[N];

void dfs(int x) {
    if (vis[x]) return;
    vis[x] = true;
    for (int y : ed[x]) {
        dfs(y);
    }
}

void solve() {
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        vis[i] = false;
        ed[i].clear();
    }
    for (int i = 1; i <= n; ++i) {
        int t;
        scanf("%d", &t);
        if (i - t > 0) ed[i].push_back(i - t), ed[i - t].push_back(i);
        if (i + t <= n) ed[i].push_back(i + t), ed[i + t].push_back(i);
    }
    int cnt = 0;
    for (int i = 1; i <= n; ++i) {
        if (!vis[i]) cnt++, dfs(i);
    }
    printf("%d\n", cnt - 1);
}

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        solve();
    }
    return 0;
}

G. 宝石商店

这是一道纯数据结构题,不难发现

\[(a_i \lor x) \oplus (a_i \land x) = a_i \oplus x\]

问题转化成在区间 [l, r] 内找一个 i,使得 \(a_i \oplus x\) 最大。于是可持久化字典树或者主席树套字典树都行。建议别写树套树,一错一个不吱声。

我不作死,我写的是字典树。

#include <iostream>
#include <cstring>

using namespace std;

const int N = 200010;

int trie[N * 60][2], rt[N], tot;

void add(int x, int cur, int pre) {
    for (int i = 30; i >= 0; --i) {
        bool t = x >> i & 1;
        trie[cur][t] = ++tot;
        trie[cur][t ^ 1] = trie[pre][t ^ 1];
        cur = trie[cur][t], pre = trie[pre][t];
    }
}

int query(int x, int l, int r) {
    int res = 0;
    for (int i = 30; i >= 0; --i) {
        bool t = x >> i & 1;
        if (trie[r][t ^ 1] && trie[r][t ^ 1] != trie[l][t ^ 1]) {
            res += 1 << i;
            l = trie[l][t ^ 1], r = trie[r][t ^ 1];
        }
        else {
            l = trie[l][t], r = trie[r][t];
        }
    }
    return res;
}

int main() {
    int T;
    scanf("%d", &T);
    while (T--){ 
        int n, m;
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; ++i) {
            rt[i] = ++tot;
            int t;
            scanf("%d", &t);
            add(t, rt[i], rt[i - 1]);
        }
        while (m--) {
            int l, r, x;
            scanf("%d%d%d", &l, &r, &x);
            printf("%d\n", query(x, rt[l - 1], rt[r]));
        }
        memset(trie, 0, (tot + 1) * sizeof(trie[0]));
        memset(rt, 0, (n + 1) * sizeof(int));
        tot = 0;
    }
    return 0;
}

I. 部落冲突

我这里魔改了一下并查集,这道题用并查集处理唯一的困难就是操作2(野蛮人 a 移动到部落 b 中),直接改野蛮人 a 的父亲可能会波及到他的子树,所以需要一种办法让每个野蛮人始终是叶子。于是我开了 n 个虚拟节点分别初始化成每个野蛮人的父亲,用来合并和交换,进行合并和交换的时候动的永远都是虚拟节点,这样随便改父亲就不会出问题了。

#include <iostream>
#include <vector>

using namespace std;

const int N = 1000010;

int rep[N], rnk[N * 2], fa[N * 2]; // 前 n 个表示野蛮人,中间表示初始虚拟节点
int getfa(int x) {
    return x == fa[x] ? x : fa[x] = getfa(fa[x]);
}

void solve() {
    int n, q;
    scanf("%d%d", &n, &q);

    int tot = 2 * n;
    for (int i = 1; i <= n; ++i) {
        fa[i] = i + n;
        fa[i + n] = i + n;
        rep[i] = i + n; // 代表点暂时选中间的初始节点,不能动前 n 个
        rnk[i + n] = i;
    }

    while (q--) {
        int op;
        scanf("%d", &op);
        if (op == 4) {
            int a;
            scanf("%d", &a);
            printf("%d\n", rnk[getfa(a)]);
        }
        else {
            int a, b;
            scanf("%d%d", &a, &b);
            if (op == 1) {
                int r1 = rep[a], r2 = rep[b];
                fa[r2] = r1; // 合并
                rep[b] = r1;
            }
            else if (op == 2) {
                fa[a] = rep[b]; // 野蛮人只能是叶子
            }
            else {
                swap(rnk[rep[a]], rnk[rep[b]]);
                swap(rep[a], rep[b]);
            }
        }
    }
}

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        solve();
    }
    return 0;
}

J. 选择配送

这道题应该是第二水的题,尝试分类讨论分离两个点的参数,尝试去绝对值

\[|p_1 - p_2| + |q_1 - q_2| = \max\{|(p_1 + q_1) - (q_1 + q_2|), |(p_1 - q_1) - (p_2 + q_2)|\}\]

只需要记录四个极端值 \(\max\{x_i + y_i\}, \min\{x_i + y_i\},\max\{x_i - y_i\},\min\{x_i - y_i\}\)就能找到对于每个候选配送站的最大距离。

#include <iostream>
#include <climits>

using namespace std;

const int N = 1000010;

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        int n, m;
        long long mn_s = LLONG_MAX, mx_s = LLONG_MIN, mn_m = LLONG_MAX, mx_m = LLONG_MIN;
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; ++i) {
            long long x, y;
            scanf("%lld%lld", &x, &y);
            mx_s = max(mx_s, x + y);
            mn_s = min(mn_s, x + y);
            mx_m = max(mx_m, x - y);
            mn_m = min(mn_m, x - y);
        }
        long long res = LLONG_MAX;
        for (int i = 1; i <= m; ++i) {
            long long x, y;
            scanf("%lld%lld", &x, &y);
            long long su = x + y, me = x - y;
            res = min(res, max(max(abs(su - mx_s), abs(su - mn_s)), max(abs(me - mx_m), abs(me - mn_m))));
        }
        printf("%lld\n", res);
    }
    return 0;
}
0
Subscribe to my newsletter

Read articles from Star directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Star
Star

A Chinese OIer and a tosser.