周赛 Round 138 题解

由于牛客的渲染问题,你可以点此链接进入我的博客查看

A 小苯的转盘游戏

知识点:线性不定方程,同余 / 模运算,构造性证明

设进行了 ,则最终有:

也就是:

因为 互质,且我们可以让 足够大来保证 也非负。

更具体地说:

  • 先选一个 ,满足

  • 再让 足够大,使得

这样的 总能找到,所以任意 都可以互相到达。

时间复杂度

void solve() {
    cout << "YES" << endl;
}

B 小苯的最大异或

知识点:位运算,枚举,优化暴力

做一次操作,相当于:

也就是二进制右移一位:

所以,连续操作若干次后:

  • 可能变成
  • 可能变成

其中 都是非负整数。

因为 的操作互不影响,最终只和“各自被操作了多少次”有关,和操作顺序无关。

所以最终所有可能状态一定是:

由于数据范围保证 ,而 。也就是说,最多右移 次左右就会变成 ,再往后也还是 。因此枚举 已经完全覆盖所有情况。

我们只要把所有可能的 都试一遍,取最大异或值即可。

时间复杂度

void solve() {
    int x, y;
    cin >> x >> y;
 
    int ans = 0;
    for (int i = 0; i <= 31; ++i) {
        int a = x >> i;
        for (int j = 0; j <= 31; ++j) {
            int b = y >> j;
            ans = max(ans, a ^ b);
        }
    }
 
    cout << ans << endl;
}

C 小苯的数位排序

知识点:贪心,单调性,模拟,构造 / 可行性判断

每次操作把某个数变成它的各位数字和:

这个操作有两个性质:

  • 不会变大,只会变小或不变;
  • 对于 ,一定有

所以一个数不断做下去,最终会变成一个一位数,并且之后再怎么做都不会变了。

我们要让整个数组非递减,也就是:

从右往左处理最合适,因为:

  • 右边一旦确定,就不应该再动它;
  • 左边怎么变,只会影响自己和更左边,不会影响已经处理好的右边。

所以我们从 依次处理:

  • 如果 ,不用管;
  • 如果 ,就只能不断对 ,直到:
    • 它变得 ,或者
    • 它已经不再变化了,但还是大于 ,这时无解。

时间复杂度

void solve() {
    int n;
    cin >> n;
    auto calc = [&](ll x) {
        string s = to_string(x);
        ll sum = 0;
        for (auto v : s) sum += v - '0';
        return sum;
    };
    vector<ll> a(n);
    for (int i = 0; i < n; ++i) cin >> a[i];
    int ans = 0;
    for (int i = n - 2; i >= 0; --i) {
        while (a[i] > a[i + 1]) {
            ll nxt = calc(a[i]);
            if (nxt == a[i]) {
                cout << -1 << endl;
                return;
            }
            a[i] = nxt;
            ans++;
        }
    }
    cout << ans << endl;
}

D 小苯的路径计数

知识点:树形 DP,DFS 遍历,祖先到后代路径计数,按路径终点统计贡献

对于每个节点 ,只需要知道,以 为终点的、满足条件的同色路径有多少条。

设这个数量为 。设 的父亲是

  • ,那么没有任何同色路径能从 延伸到 ,所以

  • ,则:

    • 本身是一条合法路径;
    • 所有以 为终点的同色路径,都可以再接上 继续保持同色。

​ 所以:

时间复杂度

void solve() {
    int n;
    cin >> n;
    vector<int> c(n + 1);
    for (int i = 1; i <= n; i++) cin >> c[i];
    vector<vector<int>> g(n + 1);
    for (int i = 1, u, v; i < n; i++) {
        cin >> u >> v;
        g[u].pb(v), g[v].pb(u);
    }
    ll ans = 0;
    vector<ll> dp(n + 1);

    auto dfs = [&](auto &&self, int u, int fa) -> void {
        for (auto v : g[u]) {
            if (v == fa) continue;
            dp[v] = (c[v] == c[u] ? dp[u] + 1 : 0);
            ans += dp[v];
            self(self, v, u);
        }
    };

    dfs(dfs, 1, 0);
    cout << ans << endl;
}

E 小苯的数字染色

知识点:前缀 DP,状态压缩,按奇偶分类维护最优值

所以整个过程等价于:

  • 按顺序选择若干个互不重叠的区间
  • 每个区间只看两端点,且两端点数值奇偶性相同
  • 目标最大化所有区间端点和

如果我们从左到右扫描数组:

  • 当前点可以作为某个区间的左端点
  • 也可以作为某个之前左端点的右端点
  • 由于区间不能重叠,所以一旦某个区间被选中,它对后面的影响只体现在前缀最优值里

而且这里只和数值的奇偶性有关,所以只需要维护奇数端点和偶数端点两个状态即可。

设:

  • :扫描到当前位置之前,已经处理完的前缀能得到的最大分数

  • :表示“当前有一个还没闭合的左端点”时,能得到的最大值,即:

其中 是某个奇偶性为 的左端点。

扫描到 ,设它的奇偶性为

  1. 不把 作为右端点:

  2. 作为某个同奇偶左端点的右端点:

    因为 里已经包含了“左端点 + 左边所有已经完成的区间”的最优值。

  3. 作为新的左端点,以后可能和后面的同奇偶数配对:

    这里必须用当前的 ,因为左端点前面的部分已经确定最优了。

时间复杂度

constexpr ll INF = 2e18;

void solve() {
    int n;
    cin >> n;

    ll dp = 0;
    ll m[2] = {-INF, -INF};

    for (int i = 0; i < n; ++i) {
        ll a;
        cin >> a;
        int p = abs(a) % 2;

        ll ndp = dp;
        if (m[p] != -INF) ndp = max(ndp, a + m[p]);

        m[p] = max(m[p], dp + a);

        dp = ndp;
    }

    cout << dp << endl;
}

F 小苯的对称序列

知识点:区间嵌套 DP,按和分类,树状数组,前缀最值优化

对于一个对称子序列,设它首尾配对的和都等于 。那么它的每一对端点都可以看成一个合法配对:

  • 普通配对 ,要求 ,贡献长度
  • 中心单点 ,要求 ,贡献长度

并且这些配对一定是嵌套的:

  • 外层配对的左端点更小
  • 外层配对的右端点更大

也就是说,合法答案本质上是: 在所有满足 的区间中,找一条左端点递增、右端点递减的最长链。

这就变成了一个经典的二维偏序最长链 / 加权 LIS 问题。

对每个可能的和 ,收集所有满足: 的配对

对于某个固定的 ,我们处理这些配对:

  • 如果
  • 如果

设当前配对是 ,我们希望找到之前已经处理过的、能套在它里面的最好方案:

  • 之前配对的左端点更小
  • 右端点更大

也就是找满足: 的最大 DP 值。

于是:

其中

把右端点 反过来编号

这样“右端点更大”就变成了 更小,可以用树状数组维护前缀最大值。处理顺序按左端点 从小到大。每个配对

  • 查询 中所有 的最大值
  • 加上当前权值
  • 再把结果更新到 对应的位置

时间复杂度

template <class Int = ll>
struct BIT {
    vector<Int> a;
    int n;

    BIT() {}
    BIT(int n) { init(n); }

    void init(int n) {
        this->n = n;
        a.resize(n + 1);
    }

    void update(int x, Int k) {
        for (; x <= n; x += x & -x) {
            a[x] = max(a[x], k);
        }
    }

    Int ask(int x) {
        Int ans = 0;
        for (; x; x -= x & -x) {
            ans = max(ans, a[x]);
        }
        return ans;
    }

    void clear() { fill(a.begin(), a.end(), 0); }
};

void solve() {
    int n, mx = 0;
    cin >> n;
    vector<int> a(n);
    for (int i = 0; i < n; i++) cin >> a[i], mx = max(mx, a[i]);

    vector<vector<PII>> pairs(mx * 2 + 1);

    for (int i = 0; i < n; i++) {
        for (int j = i; j < n; j++) {
            int sum = a[i] + a[j];
            pairs[sum].pb({i, j});
        }
    }

    int ans = 0;
    BIT tr(n);

    for (int S = 2; S <= mx * 2; S++) {
        if (pairs[S].empty()) continue;

        tr.clear();
        int mxl = 0;

        for (auto [u, v] : pairs[S]) {
            int wt = (u == v) ? 1 : 2;

            int val = tr.ask(n - v - 1) + wt;

            mxl = max(mxl, val);

            tr.update(n - v, val);
        }
        ans = max(ans, mxl);
    }

    cout << ans << endl;
}

当然也可以不用树状数组,直接枚举区间内部状态,时间复杂度

void solve() {
    int n, mx = 0;
    cin >> n;
    vector<int> a(n);
    vector<vector<int>> pos(1005);
    for (int i = 0; i < n; i++) cin >> a[i], mx = max(mx, a[i]), pos[a[i]].pb(i);
 
    vector<bool> flag(2 * mx + 1);
    for (int i = 0; i < n; i++) {
        for (int j = i; j < n; j++) {
            flag[a[i] + a[j]] = true;
        }
    }
 
    int ans = 0;
    vector<int> dp(n);
    vector<PII> upd;
 
    for (int S = 2; S <= mx * 2; S++) {
        if (!flag[S]) continue;
 
        fill(all(dp), 0);
        int cur = 0;
 
        for (int i = n - 1; i >= 0; i--) {
            int tar = S - a[i];
            if (tar < 0 || tar > 1000 || pos[tar].empty()) continue;
 
            upd.clear();
            for (auto j : pos[tar]) {
                if (j < i) continue;
 
                int in = 0;
                for (int k = i + 1; k < j; k++) {
                    if (dp[k] > in) {
                        in = dp[k];
                    }
                }
 
                int clen = (i == j) ? 1 : 2 + in;
                upd.pb({j, clen});
 
                if (clen > cur) {
                    cur = clen;
                }
            }
 
            for (auto [u, v] : upd) {
                if (v > dp[u]) {
                    dp[u] = v;
                }
            }
        }
 
        if (cur > ans) {
            ans = cur;
        }
    }
 
    cout << ans << endl;
}

乱搞 (疑似将要被击杀):

知识点:枚举,区间 DP,滚动数组优化

设选出的对称子序列为

题目要求:

也就是说,这个子序列一定存在一个固定的公共和 ,满足首尾配对都等于

所以问题即为,枚举公共和 ,求在原数组中能选出的最长首尾配对和都等于 的子序列长度。

因为 ,所以 的范围只需要枚举到

定义 表示在区间 中,能选出的、公共和为 的最长对称子序列长度。

那么转移为:

  • 不选左端点 ,答案是
  • 不选右端点 ,答案是
  • 如果 ,可以把它们作为一对首尾,再加上中间部分

于是:

如果 ,单个数只能作为中心点:

但是 ,最坏情况下肯定会 ,所以我们考虑剪枝,先算一个“理论上界” 。对于固定的

  • ,那么值为 的元素最多只能配成

  • (即 为偶数且 ),这些元素可以全部用上,贡献

这就能计算出一个不考虑位置顺序的最大可能长度上界

如果 ,说明这个 ​ 再怎么做也不可能超过当前答案,直接跳过。

时间复杂度

void solve() {
    int n;
    cin >> n;
    vector<int> a(n + 1), freq(1005);
    int mx = 0;
 
    for (int i = 1; i <= n; i++) cin >> a[i], freq[a[i]]++, mx = max(mx, a[i]);
 
    int ans = 0, mxs = mx * 2;
 
    for (int S = 2; S <= mxs; S++) {
        int tr = 0;
        for (int x = 1; x <= S / 2; x++) {
            if (S - x > 1000) continue;
            if (x * 2 == S) {
                tr += freq[x];
            } else {
                tr += 2 * min(freq[x], freq[S - x]);
            }
        }
        if (tr <= ans) continue;
 
        vector<int> dp(n + 1, 0);
        for (int i = n; i >= 1; i--) {
            int prev = 0;
            for (int j = i; j <= n; j++) {
                int temp = dp[j];
 
                if (i == j) {
                    dp[j] = (a[i] * 2 == S) ? 1 : 0;
                } else {
                    if (a[i] + a[j] == S) {
                        dp[j] = 2 + prev;
                    } else {
                        dp[j] = max(dp[j], dp[j - 1]);
                    }
                }
                prev = temp;
            }
        }
        ans = max(ans, dp[n]);
    }
 
    cout << ans << endl;
}

乱搞 ​ :

知识点 优化,优化暴力

固定公共和 以后,把原数组倒过来,记为 ,然后把 里的每个数 变成 ,得到数组 。那么原数组中最长的、公共和为 的对称子序列等价于 的最长公共子序列长度。

所以整题就是:

  1. 枚举
  2. 对每个 求一次
  3. 取最大值。

时间复杂度

static constexpr int MAXB = 8;

struct Bits {
    ull b[MAXB]{};

    Bits operator|(const Bits& o) const {
        Bits r;
#pragma GCC unroll 8
        for (int i = 0; i < MAXB; ++i) r.b[i] = b[i] | o.b[i];
        return r;
    }

    Bits and_not(const Bits& o) const {
        Bits r;
#pragma GCC unroll 8
        for (int i = 0; i < MAXB; ++i) r.b[i] = b[i] & ~o.b[i];
        return r;
    }

    Bits operator-(const Bits& o) const {
        Bits r;
        unsigned long long borrow = 0;
#pragma GCC unroll 8
        for (int i = 0; i < MAXB; ++i) {
            borrow = _subborrow_u64(borrow, b[i], o.b[i], &r.b[i]);
        }
        return r;
    }

    Bits shl1_or1() const {
        Bits r;
        ull carry = 1;
#pragma GCC unroll 8
        for (int i = 0; i < MAXB; ++i) {
            r.b[i] = (b[i] << 1) | carry;
            carry = b[i] >> 63;
        }
        return r;
    }

    int count(int blocks) const {
        int res = 0;
        for (int i = 0; i < blocks; ++i) {
            res += __builtin_popcountll(b[i]);
        }
        return res;
    }
};

void solve() {
    int n;
    cin >> n;
    vector<int> a(n);
    for (int i = 0; i < n; ++i) cin >> a[i];

    int blocks = (n + 63) / 64;
    ull last = (n % 64 == 0 ? ~0ull : ((1ull << (n % 64)) - 1));

    vector<Bits> occ(1001);
    for (int j = 0; j < n; ++j) {
        int v = a[n - 1 - j];
        occ[v].b[j >> 6] |= 1ull << (j & 63);
    }

    int ans = 1;
    for (int S = 2; S <= 2000; ++S) {
        Bits D;

        for (int x : a) {
            Bits M{};
            int y = S - x;
            if (1 <= y && y <= 1000) M = occ[y];

            Bits X = D | M;
            Bits Y = D.shl1_or1();
            Bits T = X - Y;
            D = X.and_not(T);

            for (int i = blocks; i < MAXB; ++i) D.b[i] = 0;
            D.b[blocks - 1] &= last;
        }

        ans = max(ans, D.count(blocks));
    }
    cout << ans << endl;
}

头文件

#include <bits/stdc++.h>
#define pb push_back
#define pf push_front
#define pob pop_back
#define pof pop_front
#define eb emplace_back
#define fi first
#define se second
#define all(a) a.begin(), a.end()
#define rall(a) a.rbegin(), a.rend()
#define endl "\n"
using namespace std;

using ll = long long;
using ld = long double;
using ui = unsigned;
using ull = unsigned long long;
using i128 = __int128;
using PII = pair<int, int>;
using PLL = pair<ll, ll>;

void init() {
}

void solve() {
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);

    init();

    int t = 1;
    cin >> t;
    cout << fixed << setprecision(15);
    for (int _ = 1; _ <= t; ++_) {
        solve();
    }

    return 0;
}