题解 | 练习赛 Round 149


A.调色题Ⅰ

假设我们将原始颜色编号 从小到大排序,得到序列 ,其中

设最终变换后的颜色序列为 。为了使总代价 最小,且满足 互不相同及 的约束,最优策略应保证 的相对顺序与 一致,即

对于序列中的每一个位置 ,其最终颜色 必须满足两个限制条件:

  1. 对于 ,必须满足

综上,为了使 尽可能小,我们可以得到递推关系:

最终的最小操作总次数即为:

时间复杂度:

void solve() {
    int n;
    cin >> n;
    i64 ans = 0;
    vector<int> a(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    sort(a.begin() + 1, a.end());
    for (int i = 1; i <= n; i++) {
        ans += max(a[i - 1] + 1 - a[i], 0);
        a[i] = max(a[i - 1] + 1, a[i]);
    }
    cout << ans << "\n";
    return;
}

B.调色题Ⅰ

定义打完一轮后的总能量净变化为:

定义 为在第一轮中,打出编号为 的牌之前的能量值:

  • 对于
  1. 首先判断 的情况,必须满足两个限制条件:

    1. 能够成功打出前 张牌,即对于所有 ,均满足
    2. 一轮后的能量净变化
  2. 对于编号为 的牌,其能够被成功打出的最大轮数 满足:

    这表示该编号的牌在最后一次可以被打出的是第 轮,所以答案小于 。此时,它对总出牌数的贡献上限为

    最终的答案即

时间复杂度:

void solve() {
    int n;
    i64 x;
    cin >> n >> x;
    i64 res = x;
    vector<int> a(n + 1), b(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    for (int i = 1; i <= n; i++) {
        cin >> b[i];
    }
    for (int i = 1; i <= n; i++) {
        res -= a[i];
        if (res >= 0) {
            res += b[i];
        } else {
            cout << i - 1 << "\n";
            return;
        }
    }
    i64 ch = x - res;
    if (ch <= 0) {
        cout << "Infinity\n";
        return;
    }
    i64 ans = 1e18;
    for (int i = 1; i <= n; i++) {
        i64 sum = (x - a[i]) / ch;
        ans = min(ans, (sum + 1) * n + i - 1);
        x += b[i] - a[i];
    }
    cout << ans << "\n";
    return;
}

C.调色题Ⅱ

考虑对问题进行建图求解。我们将每一个数值视作图中的一个节点。对于初始数组中的每一个 ,它可以经过一次调色操作变为 ,这等价于在图上存在一条从节点 连向节点 的有向边,且边权为

题目要求所有方格最终的颜色编号完全相同,这等价于我们要寻找一个全局目标节点 ,使得所有的初始起点 都能到达 ,且使得总代价 达到最小。

考虑到任何数通过 操作产生的状态必然是该数自身的约数,所以真正有用的节点仅限于所有初始颜色 的约数集合。设值域上限为 ,单个数的最大约数个数记为 ,则总的有效节点数最多为 个。我们只需要在这些节点构成的空间上运行 BFS(广度优先搜索)求解最短路即可。

时间复杂度:

constexpr int N = 5e5;
void solve() {
    int n, m;
    cin >> n >> m;
    vector<int> a(n + 1), b(m + 1);
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    for (int i = 1; i <= m; i++) {
        cin >> b[i];
    }
    vector<int> dis(N + 1, 1e9);
    vector<int> sum(N + 1), cnt(N + 1);
    for (int i = 1; i <= n; i++) {
        vector<int> vec;
        queue<int> q;
        q.push(a[i]);
        dis[a[i]] = 0;
        cnt[a[i]]++;
        while (!q.empty()) {
            int x = q.front();
            q.pop();
            vec.push_back(x);
            for (int j = 1; j <= m; j++) {
                int gd = gcd(x, b[j]);
                if (dis[gd] == 1e9) {
                    dis[gd] = dis[x] + 1;
                    sum[gd] += dis[gd];
                    cnt[gd]++;
                    q.push(gd);
                }
            }
        }
        for (auto x : vec) {
            dis[x] = 1e9;
        }
    }
    int ans = 1e9;
    for (int i = 1; i <= N; i++) {
        if (cnt[i] == n) {
            ans = min(ans, sum[i]);
        }
    }
    cout << (ans == 1e9 ? -1 : ans) << "\n";
    return;
}

D.鼠鼠题

只要当前攻击力 足以击败区间左侧或右侧的 Jerry,贪心地扩张区间显然是最优策略。

通过 ST 表结合二分查找,在每一轮中快速找到向左和向右满足 的最远可达边界。

这种扩张过程最多只会持续 轮, 为值域。若当前攻击力 因为小于左侧或右侧某个位置的体力值 而停止扩张(即 ),那么当新的攻击力 时,新的攻击力 满足:

由于 ,且 ,可以得出:

这意味着,每一次二分查找都会使攻击力至少翻倍,最多只会二分查找 次。

时间复杂度:

void solve() {
    int n, q;
    cin >> n >> q;
    vector<int> a(n + 1);
    vector<i64> pre(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        pre[i] = pre[i - 1] + a[i];
    }
    ST<int> st(a);
    while (q--) {
        i64 pos, x;
        cin >> pos >> x;
        if (a[pos] > x) {
            cout << 0 << "\n";
            continue;
        }
        x += a[pos];
        int L = pos, R = pos;
        bool flag = 1;
        while (flag) {
            flag = 0;
            if (L != 1) {
                int l = 1, r = L - 1;
                while (l < r) {
                    int mid = l + r >> 1;
                    if (st.range(mid, L - 1) > x) {
                        l = mid + 1;
                    } else {
                        r = mid;
                    }
                }
                if (st.range(l, L - 1) <= x) {
                    x += pre[L - 1] - pre[l - 1];
                    L = l;
                    flag = 1;
                }
            }
            if (R != n) {
                int l = R + 1, r = n;
                while (l < r) {
                    int mid = l + r + 1 >> 1;
                    if (st.range(R + 1, mid) > x) {
                        r = mid - 1;
                    } else {
                        l = mid;
                    }
                }
                if (st.range(R + 1, l) <= x) {
                    x += pre[l] - pre[R];
                    R = l;
                    flag = 1;
                }
            }
        }
        cout << R - L + 1 << "\n";
    }
    return;
}

E.代码题

考虑转化成求期望

表示当前能触发更新的集合为 时,后续期望的更新次数。

枚举第一次更新的元素 ,由于每个元素被选中的概率均为 ,转移方程如下:

注意到只有当 时,状态才会有有效的后续转移。因此,我们只需要计算 范围内的 值。当 时,

对于转移方程的这部分求和 ,有 ,使用一个变量维护即可。

时间复杂度:

void solve() {
    int n;
    cin >> n;
    i64 ans = 1;
    for (int i = 1; i < n; i++) {
        int m = (n - 1) / i;
        vector<i64> dp(m + 2);
        i64 suf = 0;
        for (int j = m; j >= 1; j--) {
            if (i * j + 1 <= m) {
                suf = (suf + dp[i * j + 1]) % mod;
            } else if (i * j + 1 <= n) {
                suf = (suf + 1) % mod;
            }
            dp[j] = (1 + comb.inv(n - j + 1) * suf) % mod;
        }
        ans = (ans + dp[1]) % mod;
    }
    ans = ans * comb.fac(n) % mod;
    cout << ans << "\n";
    return;
}

F.数数题

可以观察到,每次操作后的结果只与当前操作数 的奇偶性有关。记序列 中奇数有 个,偶数有 个。

为某个特定序列 操作后结果为奇数的操作序列数量, 为操作后结果为偶数的操作序列数量。

定义 个奇数和 个偶数的所有排列情况的 之和;

定义 个奇数和 个偶数的所有排列情况的 之和。

由于同奇偶性的数字之间可以任意互换位置,最终答案为

思路一

考虑先求出结果为奇数的方案数,再用总方案数进行容斥。

考虑最后一次操作和最后一个数 的奇偶性:

  • 如果最后一个数 是奇数:无论前 个数计算出的结果 是奇是偶,我们总有且仅有一种操作(加法或乘法)能让最终结果变为奇数。因此,只要确定最后一个数是奇数,前面的排列和操作方式可以任意选择。这部分的方案数为
  • 如果最后一个数 是偶数:要让最终结果为奇数,最后一次操作必须使用加法,且前面的计算结果 必须是奇数。这等价于在少了一个偶数的序列中求奇数结果的方案数,即

所以有递推式:,不断展开递推即可。

思路二

对于边界情况 或者 有:

对于 ,考虑状态转移,有如下递推:

为了解出上述递推,设以下辅助变量:

将原递推式代入可得 的递推关系:

又因为和差关系:

展开代入 中,可以推导出:

对于 ,进而解得

时间复杂度:

void solve() {
    int n;
    cin >> n;
    int odd = 0, even = 0;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        if (x & 1) {
            odd++;
        } else {
            even++;
        }
    }
    if (odd == 0) {
        cout << qpow(2, even - 1) * comb.fac(even) % mod << "\n";
        return;
    }
    i64 ans = (odd == 1 ? mod - inv(2) : 0);
    for (int i = 0; i <= even; i++) {
        ans = (ans + qpow(2, odd + i - 2) * comb(odd + i, i)) % mod;
    }
    ans = ans * comb.fac(odd) % mod * comb.fac(even) % mod;
    cout << ans << "\n";
    return;
}

G.猜猜题

任意两个区间要么相离,要么存在包含关系,我们可以将每个区间 挂载在其所包含的最小区间 下,从而构建出森林,考虑进行树上dp。

表示在以 为根的子树中,共选择了 个区间,且区间 的确定状态为

  • :区间 内所有变量均已知,选择 个区间的方案数。
  • :区间 内恰好有一个变量未知,选择 个区间的方案数。

对于节点 的所有子节点 ,转移如下:

然后考虑是否选择节点

  1. 此时选择 不会提供额外信息,而不选 仍能保持状态:
  1. 此时必须选择

为恰好在选择 个区间时完全确定数组的方案数。由于每次抽取是等概率的,总的抽取方案数为

为在 次询问内完成确定的概率。则 Alice 恰好在第 次询问后停止的概率为:

最终期望为:

注意判断无解的情况。

时间复杂度:

vector<i64> operator*(const vector<i64> &a, const vector<i64> &b) {
    vector<i64> c(a.size() + b.size() - 1, 0);
    for (int i = 0; i < a.size(); ++i) {
        for (int j = 0; j < b.size(); ++j) {
            c[i + j] = (c[i + j] + a[i] * b[j]) % mod;
        }
    }
    return c;
}
vector<i64> operator+(const vector<i64> &a, const vector<i64> &b) {
    vector<i64> c(max(a.size(), b.size()), 0);
    for (int i = 0; i < a.size(); ++i) {
        c[i] = (c[i] + a[i]) % mod;
    }
    for (int i = 0; i < b.size(); ++i) {
        c[i] = (c[i] + b[i]) % mod;
    }
    return c;
}
void solve() {
    int n, m;
    cin >> n >> m;
    vector<pair<int, int>> a(m);
    for (auto &[l, r] : a) {
        cin >> l >> r;
    }
    ranges::sort(a, [](auto x, auto y) {
        x.second = -x.second;
        y.second = -y.second;
        return x < y;
    });
    vector<vector<int>> tree(m);
    vector<int> vis(m);
    stack<int> stk;
    for (int i = 0; i < m; ++i) {
        while (!stk.empty() && a[stk.top()].second < a[i].first) {
            stk.pop();
        }
        if (!stk.empty()) {
            tree[stk.top()].push_back(i);
        }
        stk.push(i);
    }
    bool flag = 0;
    auto dfs = [&](auto dfs, int u) -> pair<vector<i64>, vector<i64>> {
        vis[u] = 1;
        auto [l, r] = a[u];
        int len = r - l + 1, sum = 0;
        for (auto v : tree[u]) {
            auto [l, r] = a[v];
            sum += r - l + 1;
        }
        vector<i64> f0{1}, f1{0};
        for (auto v : tree[u]) {
            auto [g0, g1] = dfs(dfs, v);
            f1 = f1 * g0 + f0 * g1;
            f0 = f0 * g0;
        }
        if (len == sum) {
            f0 = f0 * vector<i64>{1, 1} + f1 * vector<i64>{0, 1};
        } else if (len == sum + 1) {
            f1 = f0;
            f0 = f0 * vector<i64>{0, 1};
        } else {
            flag = 1;
        }
        return make_pair(f0, f1);
    };
    vector<i64> res{1};
    i64 sum = 0;
    for (int i = 0; i < m; i++) {
        if (vis[i]) {
            continue;
        }
        auto [l, r] = a[i];
        sum += r - l + 1;
        auto [f0, f1] = dfs(dfs, i);
        res = res * f0;
    }
    if (sum != n || flag) {
        cout << -1 << "\n";
        return;
    }
    i64 ans = 0;
    for (int i = m; i >= 1; i--) {
        res[i] = res[i] * inv(comb(m, i)) % mod;
    }
    for (int i = m; i >= 1; i--) {
        res[i] = (res[i] - res[i - 1] + mod) % mod;
    }
    for (int i = 0; i <= m; i++) {
        ans = (ans + i * res[i]) % mod;
    }
    cout << ans << "\n";
    return;
}

组合数

constexpr i64 mod = 998244353;

i64 qpow(i64 a, i64 b) {
    i64 res = 1;
    while (b) {
        if (b & 1) {
            res = res * a % mod;
        }
        a = a * a % mod;
        b >>= 1;
    }
    return res;
}
i64 inv(i64 a) { return qpow(a, mod - 2); }
struct Comb {
    vector<i64> _fac, _invfac;
    int n;
    Comb() : n(0), _fac(1, 1), _invfac(1, 1) {}
    Comb(int n) : Comb() { init(n); }
    void init(int m) {
        if (m <= n) return;
        _fac.resize(m + 1);
        _invfac.resize(m + 1);
        for (int i = n + 1; i <= m; i++) {
            _fac[i] = _fac[i - 1] * i % mod;
        }
        _invfac[m] = ::inv(_fac[m]);
        for (int i = m; i > n; i--) {
            _invfac[i - 1] = _invfac[i] * i % mod;
        }
        n = m;
    }
    i64 fac(int m) {
        if (m > n) init(2 * m);
        return _fac[m];
    }
    i64 invfac(int m) {
        if (m > n) init(2 * m);
        return _invfac[m];
    }
    i64 inv(int m) {
        return invfac(m) * fac(m - 1) % mod;
    }
    i64 operator()(int n, int m) {
        if (m < 0 || n < m) {
            return 0;
        }
        return fac(n) * invfac(m) % mod * invfac(n - m) % mod;
    }
    i64 Lucas(i64 n, i64 m) {
        if (m == 0) {
            return 1;
        }
        return (*this)(n % mod, m % mod) * Lucas(n / mod, m / mod) % mod;
    }
} comb;

ST表

template <typename T, typename Func = function<T(const T &, const T &)>>
struct ST {
    vector<vector<T>> st;
    Func func;

    ST() = default;
    ST(const vector<T> &v, Func func = [](const T &a, const T &b) {
        return max(a, b);
    }) : func(move(func)) {
        int k = bit_width<unsigned>(v.size());
        st.resize(k + 1, vector<T>(v.size()));
        st[0] = v;
        for (int i = 0; i < k; i++) {
            for (int j = 0; j + (1 << (i + 1)) - 1 < v.size(); ++j) {
                st[i + 1][j] = this->func(st[i][j], st[i][j + (1 << i)]);
            }
        }
    }
    T range(int l, int r) {
        int t = __lg(r - l + 1);
        return func(st[t][l], st[t][r + 1 - (1 << t)]);
    }
};