训练总结 1

CF1899G

首先先求出 dfs 序,然后就相当于每次询问 \([l, r]\) 中是否存在 \(y \in [L_x, R_x]\)

常见套路:把询问离线,我们只需要求出 \([1, r]\)\([1, l - 1]\) 中 属于 \([L_x, R_x]\) 的点的个数,如果相等,那么说明 \([l, r]\) 中没有新增属于 \([L_x, R_x]\) 中的点,也就是不存在。

或者更直接的做法,把每个点的标号按照 dfs 序插到线段树中,然后固定询问的右端点,在线段树中查询 \([L_x, R_x]\) 中的最大值是否大于等于询问中的 \(l\)

void solve() {
    int n, q; std::cin >> n >> q;
    std::vector<std::vector<int>> G(n + 1);
    for (int i = 1; i < n; ++i) {
        int u, v; std::cin >> u >> v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    std::vector<int> p(n + 1);
    for (int i = 1; i <= n; ++i) {
        std::cin >> p[i];
    }
    std::vector<int> L(n + 1), R(n + 1);
    int dfc = 0;
    auto dfs = [&](auto &self, int u, int f) -> void {
        L[u] = ++dfc;
        for (auto v : G[u]) {
            if (v == f) {
                continue;
            }
            self(self, v, u);
        }
        R[u] = dfc;
    };
    dfs(dfs, 1, 0);
    std::vector<std::vector<std::array<int, 3>>> qa(n + 1);
    for (int i = 1; i <= q; ++i) {
        int l, r, x; std::cin >> l >> r >> x;
        qa[l - 1].push_back({x, i, 0});
        qa[r].push_back({x, i, 1});
    }
    Fenwick<int> fen(n + 1);
    std::vector<std::array<int, 2>> ans(q + 1);
    for (int i = 1; i <= n; ++i) {
        fen.add(L[p[i]], 1);
        for (auto [x, id, y] : qa[i]) {
            ans[id][y] = fen.rangeSum(L[x], R[x]);
        }
    }
    for (int i = 1; i <= q; ++i) {
        std::cout << (ans[i][0] == ans[i][1] ? "No\n" : "Yes\n");
    }
}

CF1902D

注意到对于翻转区间,是否翻转对终点的位置是没有影响的。进一步画图观察可以得到:设未翻转时第 \(i\) 步走到的点为 \((pre_{i, 0}, pre_{i, 1})\),对于一个要翻转的区间,如果翻转前经过了 \((x, y)\),那么翻转后一定会经过 \((pre_{l - 1, 0} + pre_{r, 0} - x, pre_{l - 1, 1} + pre_{r, 1} - y)\)

这里蕴含着一种对称性,对于网格问题有的时候可以从对称性入手思考,且网格路径的走法重新排列对终点是没有影响的。

CF1902E

正难则反,算贡献。

\(s_i\)\(s_j\) 的后缀和前缀的最长匹配长度为 \(l\),那么\(|C(s_i, s_j)| = |s_i| + |s_j| - l\)

问题的关键在于求 \(l\) 对答案的贡献。固定 \(s_i\),将 \(s_i\) 翻转,那么对于翻转后的 \(s_i\) 的每一个前缀,相当于我们要计算出有多少个 \(s_j\) 的前缀能匹配,减掉他们(贡献类似与一层一层叠上去)。这个可以用字典树解决。

CF1905D

注意到题目要求前缀 \(\text{mex}\) 的和,那么正着肯定不好处理。注意到题目给出的是一个排列,所以我们倒着考虑,发现题目转化为求一个排列循环移位后 后缀 \(\min\) 的和 的最大值。

注意到后缀 \(\min\) 一定是单调不减的,同时是分段的,即某一段的 \(\min\) 都是一个值,可以合并计算。那么我们正着维护一个单调不减的单调栈,破坏单调性要 pop 的时候减去之前的贡献,push 一个新元素的时候加上新贡献,那么把原数组倍长用单调栈维护取答案的 \(\max\) 就行了。

void solve() {
    int n; std::cin >> n;
    std::vector<int> p(n * 2 + 2, -1);
    for (int i = 1; i <= n; ++i) {
        std::cin >> p[i];
        p[i + n] = p[i];
    }
    std::vector<int> stk;
    stk.push_back(0);
    i64 res = 0, ans = 0;
    for (int i = 1; i <= n * 2; ++i) {
        while (stk.size() && p[stk.back()] > p[i]) {
            int u = stk.back(); stk.pop_back();
            res -= 1ll * (u - stk.back()) * p[u];
        }
        res += 1ll * (i - stk.back()) * p[i];
        stk.push_back(i);
        if (i > n) {
            ans = std::max(ans, res + 1ll * n);
        }
    }
    std::cout << ans << "\n";
}

CF2072F

实际上要求的就是 \(\binom{n - 1}{i} \mod 2\) 的值。

直接的做法就是预处理 \(1 - N\) 中每个数有多少个 2,然后做一个前缀和。直接判断 2 的个数是否等于 0 即可。

更简单的做法:组合数模 2 的性质

ABC391F

\(f(i, j, k)=A_iB_j + B_jC_k + C_kA_i\),把 a, b, c 从小到大排序,那么有 \(f(i, j, k) \geq f(i - 1, j, k)\)\(f(i, j, k) \geq f(i, j - 1, k)\)\(f(i, j, k) \geq f(i, j, k - 1)\)

所以可以一步一步地推出第 k 大。具体来说,小于当前值 \(f(i, j, k)\) 的较大值在上述三个值中取到。那么维护一个优先队列,每次弹出最大值,并将对应的可能的较大值插到队列中,做 k 次就能得到第 k 大值。

对于某些求前 k 大值的问题,有可能是能够递推求解的。

ABC359F

直觉上的结论:\(n\) 个节点的树的度数之和为 \(2n - 2\)

问题转化为一个经典的贪心:使当前点 \(u\) 的度数 \(i\) 变为 \(i+1\) 对答案的贡献为 \((2i+1) \times a_u\),那么把贡献当作关键字插到 pq 里,每次挑出贡献最大的加入答案。

每个点的度数至少为 1,所以先对每个点做一次,然后再把剩余的 \(n-2\) 度数分配。

ABC390F

算贡献。

2022 年上海市大学生程序设计竞赛

posted @ 2025-03-16 17:12  Nylch  阅读(33)  评论(0)    收藏  举报