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

A. Tokitsukaze and Absolute Expectation

题意

对于一个长为 的序列 ,第 个元素 的值将会从 中独立等概率生成。定义:

的期望。

思路

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

对于 ,记 ,并钦定 。那么分母是很显然的,为两个区间的长度之积,接下来我们考虑如何求分子。

可以发现,期望的计算涉及相交的区间和不相交的区间。记 ,那么我们可以将区间拆分为两两相交的 ,以及两两不相交的

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

剩下的就是相交的情况,也就是 。虽然注意力没那么好,但是打表发现貌似有规律。丢进 OEIS 发现确实有公式,即 ,这里丢一个链接:A007290 - OEIS

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

时间复杂度:

对应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. Tokitsukaze and Balance String (hard)

题意

定义当一个字符串满足连续子串中 的个数相等时,该字符串是平衡的。

定义一个字符串 为满足将 翻转后字符串变为平衡的下标 的个数。

给定一个由 组成的字符串, 可以被替换为 ,输出所有替换方案对应的 之和。

思路

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

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

上述结论对于替换操作来说,替换 中的字符对平衡性毫无影响,反之能反转平衡性。所以对于一个平衡字符串,,否则

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

时间复杂度:

对应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

题意

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

思路

首先,钦定 更长,便于后面的讨论。

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

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

考虑拼接后的样子,剩下的大概是 剩下的字符, 中的最多 对字符(对称放在中间), 剩下的字符。

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

时间复杂度:

对应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

题意

给定一个 的矩阵,定义一个格子 的贡献为以这个格子为中心向 一直延伸得到的 "X" 形区域内格子的值的总和,求单个格子的最大贡献。

思路

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

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

时间复杂度:

对应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)

题意

给定一个长为 的序列 ,定义 。给定一个整数 ,输出 的所有前 大,不存在输出

思路

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

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

因为值域可能会跑到 里去,所以我们可以记 为满足 数量,那么总数量就是

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

时间复杂度:

对应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

题意

给定一个字符串 ,对于模式串 代表匹配至少一个任意字符,输出 中能匹配该模式串的子串数量。

思路

我们将模式串看成两部分,即 。当我们枚举到位置 时,如果 开始的子串能匹配上模式串的右半部分,那么我们就只需知道 中有多少 能作为模式串的左半部分,这些 可以和我们当前枚举到的右半部分组成模式串。

时间复杂度:

对应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

题意

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

思路

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

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

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

时间复杂度:

对应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

题意

三个东西,分别需要 个,做一个分别需要 分钟,不同东西可以并行制作,计算至少需要多久做完。

思路

这能有什么思路。

时间复杂度:

对应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

题意

给定两个长为 的数组 ,对于 次询问,每次询问给定一个区间 ,求:

思路

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

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

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

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

因为 也是二进制序列,所以针对 的操作只有两种,。所以我们只需分别求出 的个数,经过简单的计算即可得到结果,而前者用前缀和即可实现。

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

显然有:

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

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

式子的左半部分即为 ,类似于前缀和,而右边按照我们最开始考虑的式子的解法做即可。

时间复杂度:

对应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';
    }
}

推式子题