2024“钉耙编程”中国大学生算法设计超级联赛(1)§

循环位移§

由于查询的子串长度固定,所以可以用哈希线性跑,带上 set 也就只是多了个 log

#include <bits/stdc++.h>

using i64 = long long;

template <int seed, int mod, class T = std::string>
struct Hash {
    std::vector<i64> h, p;
    Hash(T a) {
        int n = a.size();
        h.assign(n + 1, 0);
        p.assign(n + 1, 1);
        for (int i = 0; i < n; ++i) {
            h[i + 1] = (h[i] * seed + a[i]) % mod;
            p[i + 1] = (p[i] * seed) % mod;
        }
    }
    i64 get(int l, int r) {
        return (h[r] + mod - h[l - 1] * p[r - l + 1] % mod) % mod;
    }
};

void solve() {
    std::string a, b;
    std::cin >> a >> b;

    Hash<1333331, 998244353> hash1(a + a + b);
    Hash<1145141, (int)(1e9 + 7)> hash2(a + a + b);

    std::set<std::pair<int, int>> set;

    for (int i = 1; i <= a.size(); i++) {
        set.insert(std::make_pair(hash1.get(i, i + a.size() - 1),
                                hash2.get(i, i + a.size() - 1)));
    }

    int ans = 0;

    for (int i = 1; i <= b.size() - a.size() + 1; i++) {
        int h1 = hash1.get(i + a.size() * 2, i + a.size() * 3 - 1),
            h2 = hash2.get(i + a.size() * 2, i + a.size() * 3 - 1);
        if (set.count(std::make_pair(h1, h2))) ans++;
    }

    std::cout << ans << '\n';
}

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

    int t;
    std::cin >> t;

    while (t--) {
        solve();
    }

    return 0;
}

星星§

常规动态规划题, O(n*k) 复杂度能过。

#include <bits/stdc++.h>

using i64 = long long;

void solve() {
    int n, k;
    std::cin >> n >> k;

    std::vector<std::vector<i64>> dp(n + 1, std::vector<i64>(k + 1, 1e18));

    dp[0][0] = 0;

    for (int i = 1; i <= n; i++) {
        std::vector<int> a(5, 0);

        for (int x = 1; x <= 4; x++) {
            std::cin >> a[x];
        }

        for (int x = 0; x <= 4; x++) {
            for (int j = x; j <= k; j++) {
                dp[i][j] = std::min(dp[i][j], dp[i - 1][j - x] + a[x]);
            }
        }
    }

    std::cout << dp[n][k] << '\n';
}

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

    int t;
    std::cin >> t;

    while (t--) {
        solve();
    }

    return 0;
}

§

难点不在于公式的推导,而是在于维护所需要的信息。

不难想到可以用权值树状数组来维护所需数据。

但需要合并多个子树的信息,同时还要计算子树之间的新的贡献。

这里可以借助树上启发式合并(dsu on tree)来解决这个问题。

假如我们遍历到某个节点 u ,我们需要计算以该点为根的子树对答案的贡献。

显然我们会先计算 u 节点子树的答案。

维护节点子树的信息很简单,但是如何计算子树之间的答案?

暴力的做法是每次清空或者再开一个树状数组,然后把子树节点一个个加进去算。

这样复杂度是 \(O(n^{2}log(n))\)

但树上启发式合并(dsu on tree)的思想却不想盲目的清空,它想着留下一些有用的信息。

这时我们就需要接触一个崭新的名词:重儿子(最大的那颗子树)。

我们完全可以让重儿子最后执行计算,并且不会清空信息。

这样计算 u 节点的答案时,就直接遍历重儿子以外的节点,每次计算后就把这个点加入到维护的信息中。

计算完成后,我们根据 u 节点是不是重儿子来决定是否清空现场(注意,我们并不会每次都构建一个树状数组,而是共用一个,当然你也可以在 dfs 过程中构造,每次只接收重儿子返回的树状数组,但这样可能会造成不必要的开销)。

时间复杂度为 \(O(nlog^{2}(n))\)

理解这个复杂度的由来可以参照树上合并子树的点集,每次都是将小的集合合并到大的集合里,也就是说只会遍历小的集合。

这个的时间复杂度一般为 \(O(nlog(n))\)

#include <bits/stdc++.h>

using u64 = unsigned long long;

template <typename T>
struct Fenwick {
    int n;
    std::vector<T> tr;
    Fenwick(int n) : n(n), tr(n + 1) {}

    inline int lowbit(int x) { return x & -x; }

    void add(int i, T val) {
        for (; i <= n; i += lowbit(i)) {
            tr[i] += val;
        }
    }

    T sum(int i) {
        T res = 0;
        for (; i > 0; i -= lowbit(i)) {
            res += tr[i];
        }
        return res;
    }

    T query(int l, int r) {
        if (l > r) return 0;
        return sum(r) - sum(l - 1);
    }

    T kth(int k) {
        int x = 0;
        T res = 0;
        for (int i = 1 << std::__lg(n); i; i /= 2) {
            if (x + i <= n && k >= tr[x + i]) {
                x += i;
                k -= tr[x];
                res += tr[x];
            }
        }
        return (k ? -1 : res);
    }
};

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

    int n;
    std::cin >> n;

    std::vector<std::vector<int>> eg(n + 1);

    for (int i = 0; i < n - 1; i++) {
        int u, v;
        std::cin >> u >> v;
        eg[u].push_back(v);
        eg[v].push_back(u);
    }

    std::vector<u64> a(n + 1);

    for (int i = 1; i <= n; i++) {
        std::cin >> a[i];
    }

    std::vector<int> dfn(n + 1), end(n + 1), name(n + 1), size(n + 1, 0),
        son(n + 1, 0);
    std::vector<u64> ans(n + 1, 0ULL);
    int timestamp = 0;

    auto dfs1 = [&](auto &dfs, int u, int fa) -> void {
        dfn[u] = ++timestamp;
        name[timestamp] = u;
        size[u] = 1;

        for (auto it : eg[u]) {
            if (it == fa) continue;

            dfs(dfs, it, u);
            size[u] += size[it];

            if (size[it] > size[son[u]]) {
                son[u] = it;
            }
        }
        end[u] = timestamp;
    };

    dfs1(dfs1, 1, 0);

    std::vector<Fenwick<u64>> fenwick(3, Fenwick<u64>(1e6));

    u64 now = 0ULL;

    auto add = [&](int u) {
        u64 v = a[u];
        now += v * (fenwick[0].sum(v - 1) * v - fenwick[1].sum(v - 1));
        now += fenwick[2].query(v + 1, 1e6) - v * fenwick[1].query(v + 1, 1e6);

        fenwick[0].add(v, 1ULL);
        fenwick[1].add(v, v);
        fenwick[2].add(v, v * v);
    };

    auto del = [&](int u) {
        u64 v = a[u];
        now -= v * (fenwick[0].sum(v - 1) * v - fenwick[1].sum(v - 1));
        now -= fenwick[2].query(v + 1, 1e6) - v * fenwick[1].query(v + 1, 1e6);

        fenwick[0].add(v, -1ULL);
        fenwick[1].add(v, -v);
        fenwick[2].add(v, -v * v);
    };

    auto dfs2 = [&](auto &dfs, int u, int fa, bool exist) -> void {
        for (auto it : eg[u]) {
            if (it == fa || it == son[u]) continue;
            dfs(dfs, it, u, 0);
        }

        if (son[u]) dfs(dfs, son[u], u, 1);

        for (auto it : eg[u]) {
            if (it == fa || it == son[u]) continue;
            for (int i = dfn[it]; i <= end[it]; i++) {
                add(name[i]);
            }
        }

        add(u);

        ans[u] = now;
        if (!exist) {
            for (int i = dfn[u]; i <= end[u]; i++) {
                del(name[i]);
            }
        }
    };

    dfs2(dfs2, 1, 0, 1);

    u64 res = 0;

    for (auto it : ans) {
        res ^= it * 2ULL;
    }

    std::cout << res << '\n';

    return 0;
}

传送§

该题的边是有时效性的,考虑线段树分治 + 可撤销并查集。

「线段树分治」模型:

  • 给定一些仅在 给定时间范围 内有效的操作。

  • 询问某个时间点所有操作的贡献。

「可撤销并查集」:

  • 使用栈维护操作。

  • 无路径压缩,并需要按秩合并(启发式合并),时间复杂度为 log 级别。

考虑线段树每个叶子节点代表时刻的贡献,设此时刻为 i ,则此时所有包含节点 1 的连通块内的所有节点的答案都应该加 i

在此过程中我们用并查集维护连通性,可以在 1 所在联通块的祖先上打上一个 +i 标记,代表这个连通块内的所有节点贡献 +i

具体计算贡献的过程中,在撤销时会把标记下放。

为了防止贡献重复累加,连边时孩子应减去父亲自带的标记。

时间复杂度为 \(O(nlog^{2}(n)+mlog(n))\)

#include <bits/stdc++.h>

using i64 = long long;

constexpr int N = 6e5 + 1;

int p[N], sz[N], top;
i64 tag[N];
std::array<int, 3> stack[N];
std::pair<int, int> eg[N];
std::vector<int> t[N << 2];

int find(int u) {
    while (u != p[u]) u = p[u];
    return u;
}

void merge(int u, int v) {
    int fu = find(u), fv = find(v);
    if (sz[fu] < sz[fv]) std::swap(fu, fv);
    stack[top++] = {fu, fv, sz[fv]};
    if (fu != fv) {
        p[fv] = fu, sz[fu] += sz[fv], sz[fv] = 0;
        tag[fv] -= tag[fu];
    }
}

void restore() {
    auto [fu, fv, size] = stack[--top];
    if (fu != fv) {
        p[fv] = fv, sz[fv] = size, sz[fu] -= size;
        tag[fv] += tag[fu];
    }
}

void modify(int u, int L, int R, int l, int r, int val) {
    if (L >= l && R <= r) {
        t[u].push_back(val);
        return;
    }

    int mid = (L + R) >> 1;
    if (l <= mid) modify(u << 1, L, mid, l, r, val);
    if (r >= mid + 1) modify(u << 1 | 1, mid + 1, R, l, r, val);
}

void query(int u, int L, int R) {
    for (auto it : t[u]) {
        merge(eg[it].first, eg[it].second);
    }
    if (L == R) {
        tag[find(1)] += L;
    } else {
        int mid = (L + R) >> 1;
        query(u << 1, L, mid), query(u << 1 | 1, mid + 1, R);
    }

    for (int i = 0; i < t[u].size(); i++) {
        restore();
    }
}

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

    int n, m;
    std::cin >> n >> m;

    for (int i = 0; i < m; i++) {
        std::cin >> eg[i].first >> eg[i].second;
        int l, r;
        std::cin >> l >> r;
        modify(1, 1, n, l, r, i);
    }

    for (int i = 1; i <= n; i++) {
        p[i] = i, sz[i] = 1;
    }

    query(1, 1, n);

    i64 ans = 0;

    for (int i = 1; i <= n; i++) {
        ans ^= tag[i];
    }

    std::cout << ans << '\n';

    return 0;
}

博弈§

由于是等概率选取字符,如果是轮流拿取的话,其实谁先谁后无所谓的。

在没有平局的情况下,两者获胜的概率是相等的。

什么情况下没有平局?

奇数个字符出现次数大于 1 时永远不会存在平局。

接下来只需要讨论 01 的情况。

在这之前我们需要先算出两种情况平局的概率。

需要计算给定一个集合,将集合中的元素两两凑一对的方案数(集合可能有奇数个元素)。

答案是:\(\frac {n!}{\left \lfloor \frac{n}{2} \right \rfloor !(n-\left \lfloor \frac{n}{2} \right \rfloor)!}(n-\left \lfloor \frac{n}{2} \right \rfloor)!=\frac {n!}{\left \lfloor \frac{n}{2} \right \rfloor !}\)

那么对于平局的情况,我们只需要统计合法方案数,和全部方案数,就能算出平局概率(并不需要在意每个元素对先选还是后选,这对结果没有影响,也就是说不用在于元素对的排列问题)。

  • 总方案数:\(\frac {sum!}{\left \lfloor \frac{sum}{2} \right \rfloor !}\)

  • 合法方案数:\(\prod \frac {c_{i}!}{\left \lfloor \frac{c_{i}}{2} \right \rfloor !}\)

  • 平局概率:\(\frac{\prod \frac {c_{i}!}{\left \lfloor \frac{c_{i}}{2} \right \rfloor !}}{\frac {sum!}{\left \lfloor \frac{sum}{2} \right \rfloor !}}\)

对于 0 情况,只需要减去平局概率然后均分即可。

对于 1 情况,平局概率实际上是第 \(\left \lfloor \frac{n}{2} \right \rfloor\) 轮后平局的概率,这其实意味着先手会获胜,因此答案是 \(\frac {1-p}{2}+p=\frac {1+p}{2}\)

#include <bits/stdc++.h>

using i64 = long long;

constexpr i64 mod = 998244353, N = 1e7 + 1;
class modint {
    i64 num;

public:
    modint(i64 num = 0) : num(num % mod) {}

    i64 val() const { return num; }

    modint pow(i64 other) {
        modint res(1), temp = *this;
        while (other) {
            if (other & 1) res = res * temp;
            temp = temp * temp;
            other >>= 1;
        }
        return res;
    }

    constexpr i64 norm(i64 num) const {
        if (num < 0) num += mod;
        if (num >= mod) num -= mod;
        return num;
    }

    modint inv() { return pow(mod - 2); }
    modint operator+(modint other) { return modint(num + other.num); }
    modint operator-() { return {-num}; }
    modint operator-(modint other) { return modint(-other + *this); }
    modint operator*(modint other) { return modint(num * other.num); }
    modint operator/(modint other) { return *this * other.inv(); }
    modint &operator*=(modint other) {
        num = num * other.num % mod;
        return *this;
    }
    modint &operator+=(modint other) {
        num = norm(num + other.num);
        return *this;
    }
    modint &operator-=(modint other) {
        num = norm(num - other.num);
        return *this;
    }
    modint &operator/=(modint other) { return *this *= other.inv(); }
    friend std::istream &operator>>(std::istream &is, modint &other) {
        is >> other.num;
        other.num %= mod;
        return is;
    }
    friend std::ostream &operator<<(std::ostream &os, modint other) {
        other.num = (other.num + mod) % mod;
        return os << other.num;
    }
};

modint fc[N], ifc[N];
void init() {
    int n = N - 1;
    fc[0] = 1;
    for (int i = 1; i <= n; i++) fc[i] = fc[i - 1] * i;
    ifc[n] = modint(1) / fc[n];
    for (int i = n - 1; i >= 0; i--) ifc[i] = ifc[i + 1] * (i + 1);
}

modint C(int n, int m) {
    if (n < m) return 0;
    if (m == 0) return 1;
    return fc[n] * ifc[n - m] * ifc[m];
}
void solve() {
    int n;
    std::cin >> n;

    std::vector<int> cnt(26, 0);

    int sum = 0, x = 0;

    while (n--) {
        char c;
        int h;
        std::cin >> c >> h;
        cnt[c - 'a'] = h;
        sum += h;
        x += h & 1;
    }

    if (x >= 2) {
        std::cout << modint(1) / modint(2) << '\n';
        return;
    }

    modint ans = 1;

    for (int i = 0; i < 26; i++) {
        ans *= fc[cnt[i]] * ifc[cnt[i] >> 1];
    }

    ans *= ifc[sum] * fc[sum >> 1];

    if (x == 0) {
        std::cout << (modint(1) - ans) / modint(2) << '\n';
    } else {
        std::cout << (modint(1) + ans) / modint(2) << '\n';
    }
}

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

    int t;
    std::cin >> t;

    init();

    while (t--) {
        solve();
    }

    return 0;
}

序列立方§

答案等价于任选三个子序列使它们相同的方案数。

\(\sum\sum\sum[a=b=c],(a\in S,b\in S,c\in S)\)

问题可以转化成 DP 问题 。

\(f_{i,j,k}\) 表示选择第一个序列结尾为 \(a_i\) ,第二个序列结尾为 \(a_j\) ,第三个序列结尾为 \(a_k\) 时的合法方案数。

则有:

\[f_{i,j,k}=\sum\sum\sum f_{i^{'},j^{'},k^{'}}(0\le i^{'}<i,0\le j^{'}<j,0\le k^{'}<k,a_i=a_j=a_k)\]

答案为 \(\sum f_{i,j,k}(a_i=a_j=a_k)\)

具体实现可以用三维前缀和来优化。

时间复杂度为 \(O(n^3)\)

注意,模数常量应加上 constexpr 前缀,否则会 TLE ,常量和变量性能差距非常大。

#include <bits/stdc++.h>

using i64 = long long;

constexpr i64 mod = 998244353;

int n, a[251];
i64 ans = 0, dp[251][251][251], g[251][251][251];

int main() {
    std::cin >> n;

    for (int i = 1; i <= n; i++) {
        std::cin >> a[i];
    }

    dp[0][0][0] = 1;
    for (int i = 0; i <= n; i++) {
        for (int j = 0; j <= n; j++) {
            g[i][j][0] = g[i][0][j] = g[0][i][j] = 1;
        }
    }

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            for (int k = 1; k <= n; k++) {
                if (a[i] == a[j] && a[j] == a[k]) {
                    dp[i][j][k] = g[i - 1][j - 1][k - 1];
                    ans += dp[i][j][k];
                    ans %= mod;
                }

                g[i][j][k] = dp[i][j][k];
                g[i][j][k] += g[i - 1][j][k] + g[i][j - 1][k] + g[i][j][k - 1];
                g[i][j][k] %= mod;
                g[i][j][k] -= g[i - 1][j - 1][k] + g[i - 1][j][k - 1] +
                            g[i][j - 1][k - 1];
                g[i][j][k] %= mod, g[i][j][k] += mod, g[i][j][k] %= mod;
                g[i][j][k] += g[i - 1][j - 1][k - 1];
                g[i][j][k] %= mod;
            }
        }
    }

    std::cout << ans << '\n';

    return 0;
}

位运算§

思路很简单,对于每一位分开考虑就行了,之后结果相乘。

注意要开 long long

#include <bits/stdc++.h>

using i64 = long long;

int get(int u) {
    int sum = 0;
    for (int i = 0; i < (1 << 4); i++) {
        sum += (((((i >> 3) & (i >> 2)) ^ (i >> 1)) | (i >> 0)) & 1) == u;
    }
    return sum;
}

void solve() {
    int n, k;
    std::cin >> n >> k;

    i64 ans = 1;

    for (int i = 0; i < k; i++) {
        ans *= get((n >> i) & 1);
    }

    std::cout << ans << '\n';
}

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

    int t;
    std::cin >> t;

    while (t--) {
        solve();
    }

    return 0;
}