Codeforces Round 926 (Div. 2) 题解

比赛链接:https://codeforces.com/contest/1929

官解链接:https://codeforces.com/blog/entry/125943

出的很差的一场。

UPD1:加入了 E 题使用或卷积的另解。

推歌

CF1929A. Sasha and the Beautiful Array

任意排列数组 \(a_{1..n}\),求 \(\sum_{i=2}^n (a_i - a_{i-1})\) 的最大值。

题解

见过最显然的 A 题,奠定了本场题目简单的基调。

众所周知 \(\sum_{i=2}^n (a_i - a_{i-1}) = a_n - a_1\),又因为可以任意排列,答案就是 \(\max(a) - \min(a)\)

代码实现

void solve() {
    int n;
    cin >> n;
    vector<int> a(n);
    for (int &x: a) cin >> x;
    cout << ranges::max(a) - ranges::min(a) << '\n';
}

CF1929B. Sasha and the Drawing

在一个 \(n \times n\) 的正方形网格上,最少要给多少个格子涂色,才能使至少有 \(k\) 条对角线上至少有一个格子被染色?

题解

垃圾场特有的猜猜题,但我一开始画图画错了,导致猜错了……

在一个最优方案中,涂色一个格子只会使染色对角线的数目增加 \(1\)\(2\)。且我们永远可以将染色两条对角线的操作提前(因为他不会和任何之前的操作产生冲突!)。那么问题就是如何连续进行尽可能多的染色两条对角线的操作。

我们有一种染色方案,能使得共 \(2n\) 次操作中,只有最后两次操作只染色一条对角线,前 \(2n-2\) 次操作如下图:

image

最后两次操作分别染色右上角和右下角的两个小方格。

要证明这是最优方案,只要说明不可能使所有操作都染色两条对角线。很简单:左下角和右上角的两个小方格,它们所在的两条对角线均只能通过这一条小方格染色,也就是这两次涂色必然进行;但它们又在同一条反对角线上,因此两次涂色操作必定至少有一次只染色一条对角线。

若所有操作均染色两条对角线,需要的操作次数为 \(\lceil \dfrac k 2\rceil\)。当 \(\lceil \dfrac k 2\rceil \le 2n-2\) 时,这就是答案;否则答案为 \(2n-2 + k - 2 * (2n-2) = k + 2 - 2n\)

代码实现

void solve() {
    int n, k;
    cin >> n >> k;
    if (k <= 4 * n - 4) {
        cout << (k + 1) / 2 << '\n';
    } else {
        cout << k + 2 - 2 * n << '\n';
    }
}

CF1929F. Sasha and the Wedding Binary Search Tree

给定一棵 BST,其中部分点的权值不定 \(\in [1, C]\)。求有多少填充方案。

题解

非常简单的题,甚至完全可以放到 C 题之前……分数虚高是因为被放在最后,而很多人被 D 挡住了,根本没来得及看后两题。

BST 等价于中序遍历是非降序列。对每段连续的 \(-1\),它可以取前后两个数(可以在首尾插入两个哨兵)范围内的任意非降序列。\(n\) 个数的,取值范围长度为 \(m\) 的非降序列的个数个数即为在 \(n\) 个数内插入 \(m-1\) 个隔板的方案数 \(\binom{n + m-1}{n}\)。不同段的方案是无关的,根据乘法原理将方案数相乘即可。

使用 \(\binom {n}{m} = \dfrac {n(n-1)\cdots (n-m+1)}{m!}\),可以在 \(O(m)\) 时间内暴力求组合数。由于连续 \(-1\) 段长度的总和不超过 \(n\),时间复杂度是正确的。

代码实现

void solve() {
    int n, m;
    cin >> n >> m;
    vector<int> l(n + 1), r(n + 1), val(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> l[i] >> r[i] >> val[i];
    }
    vector<int> a;
    a.push_back(1);
    function<void(int)> dfs = [&](int u) {
        if (l[u] != -1) dfs(l[u]);
        a.push_back(val[u]);
        if (r[u] != -1) dfs(r[u]);
    };
    dfs(1);
    a.push_back(m);
    Z ans = 1;
    auto comb = [&](int n, int m) -> Z {
        Z res = 1;
        for (int i = 1; i <= m; i++) {
            res = res * (n + 1 - i) / i;
        }
        return res;
    };
    for (int i = 1, j; i <= n; i = j) {
        if (a[i] != -1) {
            j = i + 1;
        } else {
            for (j = i; a[j] == -1; j++);
            int len = j - i;
            int num = a[j] - a[i - 1] + 1;
            ans *= comb(len + num - 1, len);
        }
    }
    cout << ans << '\n';
}

CF1929C. Sasha and the Casino

赔率为 \(k\) 的赌场,保证至多连续输 \(x\) 次。求 \(a\) 元本金能否保证在有限时间内赢得任意金额,即对任意符合条件的输赢情况,\(\lim\limits_{t \to \infty} money= \infty\)

题解

这场比赛最有趣的一题。大家在学概率论时,也许听说过这么一个问题:

若赔率为 \(2\),且赌徒采用如下策略:

  • \(i\) 次下注 \(2^{i-1}\) 元(以保证一旦赢钱,总计能赚 \(1\) 元钱),且一旦第一次赢钱就停手。

是否代表赌徒一定稳赚不赔?事实上当胜率 \(p < \dfrac 1 2\) 时,由于总下注的期望 \(\sum_{i=1}^{\infty} (2^i-1) q^{i-1}p = +\infty\),而成本是有限的,在赢得这一元之前,总会把本金赔光。

回到问题本身来。加入了至多连续输 \(x\) 次的限制后,这个策略是否是最优的?有以下观察:

  • 每次赢钱后的局面没有本质区别。因此,策略只与当前连续输的局数有关。

  • 策略需要确保无论输多少(\(\le x\))次之后赢,至少能赚一元钱。

  • 由于本金是有限的,在这个前提下,花的本金越少越好。

因此这个策略确实是最优的。花的本金的最大值为连续输了 \(x\) 次时,共 \(x+1\) 次下注的总和,判断是否有这么多钱即可。每次下注的本金是 \(\lceil \dfrac {s + 1} {k-1}\rceil = \lfloor \dfrac s {k-1} \rfloor + 1\),其中 \(s\) 是已经输掉的金额。

由于金额是指数增长的,时间复杂度为 \(O(\min(x, \log a))\)

代码实现

void solve() {
    int k, x, a;
    cin >> k >> x >> a;
    int need = 1;
    for (int round = 0; round < x && need <= a; round++) {
        need += need / (k - 1) + 1;
    }
    cout << (need <= a ? "YES" : "NO") << '\n';
}

CF1929D. Sasha and a Walk in the City

有一棵树,求满足以下条件点集的数量:

  • 树上任意一条简单路径至多包含点集中的两个点。

题解

分析满足条件点集的性质,发现其等价于存在一个点,以它为根时,任意节点之间不存在父子关系。在任意定根的树上,它等价于上面两种情况之一:

  • 任意两点之间不存在父子关系;

  • 或存在一个点是其它所有节点的父亲,但不是它们的 LCA(即是它们 LCA 的祖先)。

考虑使用树形 DP 如何计算。记 \(dp_u\)\(u\) 的子树中,任意两点之间不存在父子关系的点集的数量(包含空集),由于 \(dp_u\) 一定是 \(dp_{fa_u}\) 的一部分,这部分贡献可以汇总到树根计入答案。对第二种情况,我们该点直接将贡献计入答案即可。具体流程如下:

  • \(dp_u = 1 + \prod\limits_{v} dp_v\)

  • \(ans \leftarrow ans + \sum\limits_v (dp_v - 1)\)

对第一种情况,若选择 \(u\),则不能选择子树中任意其它节点,这部分方案数为 \(1\)。而不选择 \(u\),则所有子树内的方案互不干扰,根据乘法原理将它们乘起来。

对第二种情况,我们必须选择 \(u\),而为了防止成为 LCA,只能选择一个子树中的节点,且注意排除空集。

代码实现

void solve() {
    int n;
    cin >> n;
    vector<vector<int>> adj(n);
    for (int e = 1; e < n; e++) {
        int u, v;
        cin >> u >> v;
        u--, v--;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    vector<Z> dp(n);
    Z ans = 0;
    function<void(int, int)> dfs = [&](int u, int fa) {
        dp[u] = 1;
        for (int v : adj[u]) {
            if (v == fa) continue;
            dfs(v, u);
            dp[u] *= dp[v];
            ans += dp[v] - 1;
        }
        dp[u] += 1;
    };
    dfs(0, 0);
    cout << ans + dp[0] << '\n';
}

CF1929E. Sasha and the Happy Tree Cutting

有一棵树,树上有 \(k \le 20\) 条路径。求最少染色多少条边,可以使每条路径上至少有一条边被染色。

题解

考虑将一条边涂色能影响哪些路径,可以发现不同影响的总数是 \(O(k)\) 的。证明可以从虚树考虑:建出这 \(2k\) 个点的虚树,则虚树中相邻两点连线上所有边的影响相同;而我们知道虚树的边数是 \(O(2k) = O(k)\) 的。

我们也可以做更细致的分析:对路径 \((u, v)\),记他们的 LCA 为 \(w\),则对该路径造成影响的边是 \((w, v)\)\((w, u)\) 两条自上而下的链上的边。因此边的影响集合向上只在经过一个是某条路径 LCA 的点时才会减小;又因为只要影响集合不减小,将涂色边向上移就不会使解更劣,考虑边的范围可以缩减到所有 \((w, v)\)\((w, u)\) 路径上的第一条边。

之后即可朴素地状压 DP。可以看出这是一个 DAG 上最短路的模型,使用类似最短路的写法。总时间复杂度 \(O(n + k 2^k)\)

代码实现

constexpr int INF = 1e9 + 7;
void solve() {
    int n;
    cin >> n;
    vector<vector<int>> adj(n);
    for (int e = 1; e < n; e++) {
        int u, v;
        cin >> u >> v;
        u--, v--;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    int k;
    cin >> k;
    vector<int> emsk(n);
    for (int p = 0; p < k; p++) {
        int u, v;
        cin >> u >> v;
        u--, v--;
        emsk[u] ^= 1 << p;
        emsk[v] ^= 1 << p;
    }
    vector<int> buc;
    function<void(int, int)> dfs = [&](int u, int fa) {
        for (int v : adj[u]) {
            if (v == fa) continue;
            dfs(v, u);
            emsk[u] ^= emsk[v];
        }
        for (int v : adj[u]) {
            if (v != fa && (~emsk[u] & emsk[v])) {
                buc.push_back(emsk[v]);
            }
        }
    };
    dfs(0, 0);
    vector<int> dp(1 << k, INF);
    dp[0] = 0;
    for (int msk = 0; msk < (1 << k) - 1; msk++) {
        for (int trans : buc) {
            dp[msk | trans] = min(dp[msk | trans], dp[msk] + 1);
        }
    }
    cout << dp[(1 << k) - 1] << '\n';
}

UPD:另解

事实上没有边的种类为 \(O(k)\) 的性质,这题也可以使用一种暴力至极的方法去做。

考虑令 \(g(S) = \sum_{e \in E} [f(e) = S]\),则它与自身 OR 卷积 \(t\) 次的结果 \(g^{(t)}(S)\) 代表“选出 \(t\) 条边,它们影响路径的并集是 \(S\) 的方案数(可以重复,考虑顺序)”。注意到答案若存在,显然不超过 \(k\)(每条选中的边都一定有作用),做卷积一直到全集 \(U\) 的方案数不为 \(0\) 时,卷积的次数就是答案。时间复杂度 \(O(k^2 2^k)\)

注意对一般的问题,中间结果可能会溢出。但由于我们只关心方案数是否为 \(0\),每步计算结束后将不为 \(0\) 的数全部置为 \(1\),即可保证所有数始终在 int32 范围内。

代码实现

using Z = int;
vector<Z> fwtOr(const vector<Z> &f, int n, Z mul = 1) {
    vector<Z> res = f;
    for (int k2 = 2, k = 1; k2 <= n; k2 <<= 1, k <<= 1) {
        for (int i = 0; i < n; i += k2) {
            for (int j = 0; j < k; j++) {
                res[i + j + k] += res[i + j] * mul;
            }
        }
    }
    return res;
}

void solve() {
    // use the same emsk
    vector<Z> g(1 << k, 0);
    for (int x : emsk) {
        g[x] = 1;
    }
    vector<Z> conv(g);
    int ans = 1;
    g = fwtOr(g, 1 << k);
    while (!conv[(1 << k) - 1]) {
        conv = fwtOr(conv, 1 << k);
        for (int i = 0; i < (1 << k); i++) {
            conv[i] *= g[i];
        }
        conv = fwtOr(conv, 1 << k, -1);
        for (int &x : conv) {
            if (x) {
                x = 1;
            }
        }
        ans++;
    }
    cout << ans << '\n';
}
posted @ 2024-02-16 13:09  cccpchenpi  阅读(552)  评论(1编辑  收藏  举报