Solved 10 of 12.

欸我去出题人怎么是go批,欸我去mygo还在追我

A. Tokitsukaze and Absolute Expectation

题意

对于一个长为 \(n\) 的序列 \(a\),第 \(i\) 个元素 \(a_i\) 的值将会从 \([l_i, r_i]\) 中独立等概率生成。定义:

\(\displaystyle \text{val}(a) = \sum_{i=2}^n |a_{i - 1} - a_i|\)

\(\text{val}(a)\) 的期望。

思路

首先,因为每个元素值生成的事件是相互独立的,所以根据期望的可加性,将式子化简如下:

\(E(\text{val}(a)) = E(\displaystyle \sum_{i=2}^n |a_{i - 1} - a_i|) = \displaystyle \sum_{i=2}^n E(|a_{i - 1} - a_i|)\)

对于 \(E(|a_{i - 1} - a_i|)\),记 \(a_{i - 1} \in [l_1, r_1], a_i \in [l_2, r_2]\),并钦定 \(l_1 < l_2\)。那么分母是很显然的,为两个区间的长度之积,接下来我们考虑如何求分子。

可以发现,期望的计算涉及相交的区间和不相交的区间。记 \(x = \max(l_1, l_2), y = \min(r_1, r_2), p = \max(r_1, r_2)\),那么我们可以将区间拆分为两两相交的 \([x, y], [x, y]\),以及两两不相交的 \([l_1, x - 1], [l_2, r_2]\)\([x, y], [y + 1, p]\)

我们先来考虑简单的不相交。对于两个不相交的区间 \([l_1, r_1], [l_2, r_2]\),假设我们在第二个区间选择了左端点 \(l_2\),那么由第一个区间所有选择构成的答案序列满足公差为 \(1\) 的等差数列,首项为 \(l_2 - r_1\),项数为第一个区间的长度。而当我们在第二个区间选择 \(l_2 + 1\) 时,其距离第一个区间的所有点都增大了 \(1\),所以对应地答案是原来的基础上增加一倍第一个区间的长度。以此类推,可以发现增大的倍数构成一个公差为 \(1\) 的等差数列,首项为 \(1\),项数为 \(r_2 - l_2\),因而求得式子。

剩下的就是相交的情况,也就是 \([x, y]\)。虽然注意力没那么好,但是打表发现貌似有规律。丢进 OEIS 发现确实有公式,即 \(2 \cdot \binom{n + 1}{3}\),这里丢一个链接:A007290 - OEIS

那么本题就做完了,不过注意一些边界的判断。

时间复杂度:\(O(n)\)

对应AC代码

constexpr int P = 1e9 + 7;
using Z = MInt<P>;

void solve() {
    int n;
    cin >> n;
    vector<int> l(n + 1), r(n + 1);
    for(int i=1;i<=n;i++) cin >> l[i] >> r[i];
    auto cal1 = [&](int l1, int r1, int l2, int r2) -> Z {
        return Z(l2 - r1 + l2 - l1) * (r1 - l1 + 1) * (r2 - l2 + 1) / 2 + Z(r1 - l1 + 1) * (r2 - l2 + 1) * (r2 - l2) / 2;
    };
    auto cal2 = [&](int l, int r) -> Z {
        int len = r - l + 1;
        return Z(len + 1) * len * (len - 1) / 3;
    };
    Z ans = 0;
    for(int i=2;i<=n;i++) {
        int l1 = l[i - 1], r1 = r[i - 1], l2 = l[i], r2 = r[i];
        if(l1 > l2) swap(l1, l2), swap(r1, r2);
        int x = max(l1, l2), y = min(r1, r2);
        Z now = 0;
        if(x > y) now += cal1(l1, r1, l2, r2);
        else {
            if(l1 != l2) now += cal1(l1, x - 1, l2, r2);
            if(r1 != r2) now += cal1(x, y, y + 1, max(r1, r2));
            now += cal2(x, y);
        }
        ans += now / (r1 - l1 + 1) / (r2 - l2 + 1);
    }
    cout << ans << '\n';
}

注意到我没有注意到,所以我没注意到

B. Tokitsukaze and Balance String (easy)

详见 \(C\),区别是本题可以暴力

C. Tokitsukaze and Balance String (hard)

题意

定义当一个字符串满足连续子串中 \(\mathtt{01}\)\(\mathtt{10}\) 的个数相等时,该字符串是平衡的。

定义一个字符串 \(s\)\(\text{val}(s)\) 为满足将 \(s_i\) 翻转后字符串变为平衡的下标 \(i\) 的个数。

给定一个由 \(\mathtt{'0', '1', '?'}\) 组成的字符串,\(\mathtt{'?'}\) 可以被替换为 \(\mathtt{'0', '1'}\),输出所有替换方案对应的 \(\text{val}(s)\) 之和。

思路

首先,替换完成后我们会得到一个二进制串,只包含两种字符,所以除非 \(\mathtt{01}\) 后面没有 \(\mathtt{0}\),不然一定会出现一个与之对应的 \(\mathtt{10}\)。反过来也是一样的。

所以说,我们其实只需关注头尾两个字符的情况,而中间是无所谓的,因为可以被抵消。而若要让字符串平衡,我们只能让头尾两个字符相等。所以如果 \([2, n - 1]\) 中包含 \(q\)\(\mathtt{?}\),那么这些位置都是无所谓的,我们在完成后续计算后乘上 \(2^q\) 即可。

上述结论对于替换操作来说,替换 \([2, n - 1]\) 中的字符对平衡性毫无影响,反之能反转平衡性。所以对于一个平衡字符串,\(\text{val}(s) = n - 2\),否则 \(\text{val}(s) = 2\)

然后我们针对开头和结尾是否为 \(\mathtt{?}\) 进行分讨即可。如果都不满足,那么我们根据平衡性即可得到答案;否则,我们就可以根据不同的选择得到不同的平衡性。

时间复杂度:\(O(n)\)

对应AC代码

void solve() {
    int n;
    cin >> n;
    string s;
    cin >> s;
    if(n == 1) {
        cout << (s[0] == '?' ? 2 : 1) << '\n';
        return;
    }
    int ans = 1;
    for(int i=1;i<n-1;i++) {
        if(s[i] == '?') ans = ans * 2 % mod;
    }
    if(s[0] != '?' && s[n - 1] != '?') {
        if(s[0] != s[n - 1]) ans *= 2;
        else ans *= n - 2;
    }else {
        if(s[0] == s[n - 1]) ans *= 2;
        ans *= n;
    }
    cout << ans % mod << '\n';
}

很思维

D. Tokitsukaze and Concatenate‌ Palindrome

题意

给定一个长为 \(n\) 的字符串 \(a\) 和一个长为 \(m\) 的字符串 \(b\),定义一次操作为选择任意字符串的任意位置,并将其替换为任意字符。输出使得两个字符串任意排列后拼接得到回文串的最小操作数。

思路

首先,钦定 \(b\) 更长,便于后面的讨论。

因为需要操作数尽可能小,显然我们可以先把两者的相同字符排列在两端,然后考虑剩余的字符。

如果我们有 \(x\) 个多余的字符,并想让其排列后构成回文串,那么我们只需修改其中的 \(\lfloor \frac{x}{2} \rfloor\) 个字符即可,所以我们现在的目标是求得 \(x\)

考虑拼接后的样子,剩下的大概是 \(a\) 剩下的字符,\(b\) 中的最多 \(\lfloor \frac{m - n}{2} \rfloor\) 对字符(对称放在中间),\(b\) 剩下的字符。

所以我们只需处理中间的配对。不过可能会出现拼接后为奇数长度的情况,此时需要将 \(x\) 减去 \(1\),避免重复计算。

时间复杂度:\(O(n + m)\)

对应AC代码

void solve() {
    int n, m;
    cin >> n >> m;
    string a, b;
    cin >> a >> b;
    if(n > m) swap(n, m), swap(a, b);
    int ans = 0;
    vector<int> cnt(26);
    for(auto &it : b) cnt[it - 'a'] ++;
    for(auto &it : a) {
        if(cnt[it - 'a']) cnt[it - 'a'] --;
        else ans ++;
    }
    int p = 0, q = 0; //(m - n) / 2 * 2
    for(auto &it : cnt) p += it, q += it / 2;
    if((n + m) % 2 == 1 && p > 0) p --;
    ans += p - min(q, (m - n) / 2) * 2;
    cout << ans / 2 << '\n';
}

细节太多了

E. Tokitsukaze and Dragon's Breath

题意

给定一个 \(n \times m\) 的矩阵,定义一个格子 \((x, y)\) 的贡献为以这个格子为中心向 \((x−1,y−1), (x−1,y+1), (x+1,y−1), (x+1,y+1)\) 一直延伸得到的 "X" 形区域内格子的值的总和,求单个格子的最大贡献。

思路

我们可以将矩阵视为平面直角坐标系,以 \((1, 1)\) 为原点,并将格子全都视为坐标点,那么可以发现点全都位于 \(x - y = a, x + y = b\) 直线上,且每个作为中心的格子延伸得到的区域恰好对应其中的两条直线。

所以我们将两类直线分开考虑,以 \(a, b\) 作为偏移量,记录该偏移量所对应的直线上的点权总和。然后就只需枚举每个点作为中心,将两条直线的总和加起来,再减掉重叠区域,也就是中心即可。

时间复杂度:\(O(n m \log (n + m))\)

对应AC代码

void solve() {
    int n, m;
    cin >> n >> m;
    vector<vector<int>> a(n + 1, vector<int>(m + 1));
    map<int, int> p, q;
    for(int i=1;i<=n;i++) {
        for(int j=1;j<=m;j++) {
            cin >> a[i][j];
            p[i + j] += a[i][j], q[i - j] += a[i][j];
        }
    }
    int ans = 0;
    for(int i=1;i<=n;i++) {
        for(int j=1;j<=m;j++) {
            ans = max(ans, p[i + j] + q[i - j] - a[i][j]);
        }
    }
    cout << ans << '\n';
}

经典 map 题

F. Tokitsukaze and Kth Problem (easy)

题意

给定一个长为 \(n\) 的序列 \(a\),定义 \(f(i, j) = (a_i + a_j) \bmod p\)。给定一个整数 \(k\),输出 \(f(i, j)\) 的所有前 \(k\) 大,不存在输出 \(-1\)

思路

首先,因为数组位置对答案没有影响,所以我们可以先将数组取模,然后排序。其次,因为这是两个项相加,所以值域最多只会到 \(2p - 2\)

针对前 \(k\) 大,我们可以先二分出第 \(k\) 大的大小 \(V\),然后再根据 \(V\) 找答案。\(\text{check}\) 函数可以用双指针实现,或者二分里面套二分。

因为值域可能会跑到 \([p, 2p - 2]\) 里去,所以我们可以记 \(h_x\) 为满足 \(a_i + a_j \geq x\)\(f(i, j)\) 数量,那么总数量就是 \(h_V + h_{h + V} - h_p\)

除此之外还有一个坑,极端情况下数组可能由一种数字组成,这可能会导致求最后的数组的时候复杂度爆炸。所以我们可以求出大于 \(V\) 的所有答案,然后用 \(V\) 填充。

时间复杂度:\(O(n \log p + k)\)

对应AC代码

void solve() {
    int n, p, k;
    cin >> n >> p >> k;
    vector<int> a(n + 1);
    for(int i=1;i<=n;i++) {
        cin >> a[i];
        a[i] %= p;
    }
    sort(a.begin(), a.end());
    auto sum = [&](int x) -> int {
        int cnt = 0;
        for(int i=1,j=n;i<j;i++) {
            while(j > 0 && a[i] + a[j] >= x) j --;
            if(i >= j) break;
            cnt += j - i;
        }
        return n * (n - 1) / 2 - cnt;
    };
    auto check = [&](int x) -> bool {
        return sum(x) - sum(p) + sum(p + x) >= k;
    };
    int l = 0, r = p - 1, mid;
    while(l <= r) {
        mid = (l + r) >> 1ll;
        if(check(mid)) l = mid + 1;
        else r = mid - 1;
    }
    vector<int> ans;
    for(int i=1,j=n,t=n;i<=n;i++) {
        while(j > 0 && a[i] + a[j] >= l) j --;
        while(t > 0 && a[i] + a[t] >= l + p) t --;
        for(int x=max(i,j)+1;x<=n;x++) {
            if(a[i] + a[x] >= p) break;
            ans.emplace_back(a[i] + a[x]);
        }
        for(int x=max(i,t)+1;x<=n;x++) {
            ans.emplace_back(a[i] + a[x] - p);
        }
    }
    sort(ans.begin(), ans.end(), greater<>());
    for(auto &it : ans) cout << it << ' ';
    k -= ans.size();
    while(k --) cout << l - 1 << ' ';
    cout << '\n';
}

二分写麻了,参考了一下出题人题解

G. Tokitsukaze and Kth Problem (hard)

待补充

H. Tokitsukaze and Necklace

待补充

I. Tokitsukaze and Pajama Party

题意

给定一个字符串 \(s\),对于模式串 \(\mathtt{u*uwawauwa}\)\(\mathtt{*}\) 代表匹配至少一个任意字符,输出 \(s\) 中能匹配该模式串的子串数量。

思路

我们将模式串看成两部分,即 \(\mathtt{u}\)\(\mathtt{uwawauwa}\)。当我们枚举到位置 \(p\) 时,如果 \(p\) 开始的子串能匹配上模式串的右半部分,那么我们就只需知道 \([1,p-2]\) 中有多少 \(\mathtt{u}\) 能作为模式串的左半部分,这些 \(\mathtt{u}\) 可以和我们当前枚举到的右半部分组成模式串。

时间复杂度:\(O(nm)\)

对应AC代码

void solve() {
    int n;
    cin >> n;
    string s;
    cin >> s;
    string p = "uwawauwa";
    int m = p.size();
    int ans = 0, cnt = 0;
    for(int i=0;i+m-1<n;i++) {
        bool ok = true;
        for(int j=0;j<m;j++) {
            if(s[i + j] != p[j]) ok = false;
        }
        if(ok) {
            ans += cnt;
            if(i > 0 && s[i - 1] == 'u') ans --;
        }
        if(s[i] == 'u') cnt ++;
    }
    cout << ans << '\n';
}

兄弟

J. Tokitsukaze and Recall

题意

给定一个不一定连通的无向图,以及一个整数 \(k\)。提供 \(k\) 次操作,一次操作为选择一个点并占领。可以从占领的任何位置通过边占领另一个位置。输出字典序最小的占领顺序。

思路

首先,我们可以用并查集找出所有连通块,并求出连通块中点的个数和编号最小的点。然后我们按照点的个数降序排序,个数相同时按照编号升序排序,得到若干个排序后的连通块,将前 \(k\) 个连通块中编号最小的点放入优先队列。剩下的就是遍历优先队列,拿出队列里最小的元素,并将其占领,然后枚举其儿子,放入队列即可。

但是当 \(k\) 很大时,这么做存在一个问题:我们可以用多余的 \(k\) 先占领某些编号小的点,不然 \(k\) 就浪费了。

不妨考虑贪心,只要 \(k\) 还有剩余,我们一定会按照 \(1, 2, 3, \ldots\) 顺序占领,所以我们只需在每次遍历队列前,判断队列的最小值是否过大,是则消耗 \(k\) 然后放入当前能放入的最小值。

时间复杂度:\(O(n \log n)\)

对应AC代码

struct DSU {
    vector<int> pa, siz;
    void init(int n) { pa = vector<int>(n + 1), siz = vector<int>(n + 1, 1), iota(pa.begin(), pa.end(), 0); }
    int find(int x) { while(x != pa[x]) x = pa[x] = pa[pa[x]]; return x; }
    bool unite(int u, int v) {
        int f1 = find(u), f2 = find(v);
        if (f1 == f2) return false;
        siz[f1] += siz[f2];
        pa[f2] = f1;
        return true;
    }
    int size(int x){ return siz[find(x)]; }
}dsu;

void solve() {
    int n, m, k;
    cin >> n >> m >> k;
    vector<set<int>> adj(n + 1);
    dsu.init(n);
    while (m --) {
        int u, v;
        cin >> u >> v;
        adj[u].emplace(v), adj[v].emplace(u);
        dsu.unite(u, v);
    }
    vector<int> mn(n + 1, inf), cnt(n + 1);
    for(int i=1;i<=n;i++) {
        int pa = dsu.find(i);
        if(mn[pa] == inf) mn[pa] = i;
        cnt[pa] ++;
    }
    vector<pair<int, int>> g;
    for(int i=1;i<=n;i++) {
        if(cnt[i] > 0) g.emplace_back(-cnt[i], mn[i]);
    }
    sort(g.begin(), g.end());
    priority_queue<int, vector<int>, greater<>> q;
    for(int i=0;i<min((int)g.size(),k);i++) q.emplace(g[i].second);
    k -= min((int)g.size(), k);
    vector<int> st(n + 1), ans;
    int now = 1;
    while(!q.empty()) {
        if(now < q.top() && k > 0) {
            q.emplace(now);
            k --;
        }
        int x = q.top();
        q.pop();
        if(st[x]) continue;
        st[x] = true;
        ans.emplace_back(x);
        if(x == now) now ++;
        for(auto &y : adj[x]) {
            if(st[y]) continue;
            q.emplace(y);
        }
    }
    cout << ans.size() << '\n';
    for(auto &it : ans) cout << it << ' ';
    cout << '\n';
}

写起来麻烦了点,但不知道为啥过的人这么少

K. Tokitsukaze and Shawarma

题意

三个东西,分别需要 \(x, y, z\) 个,做一个分别需要 \(a, b, c\) 分钟,不同东西可以并行制作,计算至少需要多久做完。

思路

这能有什么思路。

时间复杂度:\(O(1)\)

对应AC代码

void solve() {
    int x, y, z, a, b, c;
    cin >> x >> y >> z >> a >> b >> c;
    cout << max({a * x, b * y, c * z}) << '\n';
}

没事至少不是Hello World

L. Tokitsukaze and XOR-Triangle

题意

给定两个长为 \(n\) 的数组 \(a, b\),对于 \(q\) 次询问,每次询问给定一个区间 \([l, r]\),求:

\(\displaystyle \sum_{i=l}^r\sum_{j=i}^r a_i \oplus b_j\)

思路

首先,我们来考虑一下下面的式子怎么求:

\(\displaystyle \sum_{i=l}^r\sum_{j=l}^r a_i \oplus b_j\)

位运算一般来说拆位做会很方便,因为会出现一些奇妙的性质。假设 \(a_i^k\)\(a_i\) 的第 \(k\) 个二进制位,从 \(0\) 开始计数,\(b_i^k\) 同理,那么式子变成了下面这样:

\(\displaystyle \sum_{k=0}^{30}\sum_{i=l}^r\sum_{j=l}^r a_i^k \oplus b_j^k\)

假设我们有一个由 \(0\)\(1\) 构成的长为 \(n\) 的二进制序列 \(A\),将其全都异或上 \(1\) 相当于翻转二进制值,\(0\) 则是不变。而一个二进制序列的总和等于 \(1\) 的个数,我们记为 \(p\),那么全都异或上 \(1\) 后总和变为 \(n - p\)\(0\) 则是不变。

那么将上面的操作应用到这个式子上,我们相当于将 \(a^k\)\([l, r]\) 子序列视为 \(A\),然后枚举 \(b^k\)\([l, r]\) 区间内的每个元素,分别求出 \(A\) 全都异或上这个元素的值得到的总和,最后进行求和。

因为 \(b^k\) 也是二进制序列,所以针对 \(A\) 的操作只有两种,\(0\)\(1\)。所以我们只需分别求出 \([l, r]\)\(a^k\)\(b^k\)\(1\) 的个数,经过简单的计算即可得到结果,而前者用前缀和即可实现。

好的,现在我们回到原问题。直接求会很麻烦,但如果假设 \(r = n\),我们就可以进行递推了。记:

\(f_i = \displaystyle \sum_{i=l}^n\sum_{j=i}^n a_i \oplus b_j\)

显然有:

\(\displaystyle f_n = a_n \oplus b_n, f_i = f_{i + 1} + \sum_{k=i}^n a_i \oplus b_k\)

上面的求和可以用后缀和与 \(a_i\) 是否为 \(1\) 来快速计算,具体方法在上面已经提到了。

而有趣的是,题目的式子可以用这个进行化简:

\(\displaystyle \sum_{i=l}^r\sum_{j=l}^r a_i \oplus b_j = \displaystyle \sum_{i=l}^n\sum_{j=i}^n a_i \oplus b_j - \displaystyle \sum_{i=r+1}^n\sum_{j=i}^n a_i \oplus b_j - \displaystyle \sum_{i=l}^r\sum_{j=r+1}^n a_i \oplus b_j\)

式子的左半部分即为 \(f_l - f_{r + 1}\),类似于前缀和,而右边按照我们最开始考虑的式子的解法做即可。

时间复杂度:\(O(nm)\)

对应AC代码

constexpr int P = 1e9 + 7;
using Z = MInt<P>;

void solve() {
    int n, q;
    cin >> n >> q;
    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];
    vector<vector<Z>> pre_a(n + 1, vector<Z>(31)), pre_b(n + 1, vector<Z>(31));
    vector<vector<Z>> f(n + 2, vector<Z>(31));
    auto ret = [&](int t, int il, int ir, int jl, int jr) -> Z {
        Z cnt_a = pre_a[ir][t] - pre_a[il - 1][t], cnt_b = pre_b[jr][t] - pre_b[jl - 1][t];
        return cnt_a * (jr - jl + 1 - cnt_b) + (ir - il + 1 - cnt_a) * cnt_b;
    };
    for(int t=0;t<=30;t++) {
        for(int i=1;i<=n;i++) pre_a[i][t] = pre_a[i - 1][t] + (a[i] >> t & 1ll);
        for(int i=1;i<=n;i++) pre_b[i][t] = pre_b[i - 1][t] + (b[i] >> t & 1ll);
        for(int i=n;i>=1;i--) {
            f[i][t] = f[i + 1][t];
            if(a[i] >> t & 1ll) f[i][t] += n - i + 1 - (pre_b[n][t] - pre_b[i - 1][t]);
            else f[i][t] += pre_b[n][t] - pre_b[i - 1][t];
        }
    }
    while(q --) {
        int l, r;
        cin >> l >> r;
        Z ans = 0;
        for(int t=0;t<=30;t++) {
            Z now = f[l][t] - f[r + 1][t] - ret(t, l, r, r + 1, n);
            ans += (1ll << t) * now;
        }
        cout << ans << '\n';
    }
}

推式子题