2022 牛客多校 第七场 题解

比赛链接

C题 Constructive Problems Never Die(思维,构造)

大胆猜结论:除了所有元素相同的情况,否则一定存在一组解(实际上也确实是的)。

这题似乎解法特别多,这里贡献一下我们的做法:

  1. 按照顺序,统计一下不可以填 1 的位置的集合 \(S_1\),不可以填 2 的位置的集合 \(S_2\),一直到 \(S_n\),外加一个空的 \(S_{n+1}\)
  2. 维护一个包含 1-n 内所有数的集合 \(T\)
  3. 先检查 \(S_1\)
    1. 如果空,则跳过
    2. 如果非空,那么
      1. 从后面非空集合中随便选出一个位置,填上 1,并从 \(T\) 中删除 1
      2. 现在 \(S_1\) 内的所有位置都是可选的了,将他们都放入 \(S_{n+1}\)
  4. 一直重复该流程,直到 \(S_n\)
  5. 现在将 \(T\) 内元素随机分给 \(S_{n+1}\) 里面的位置即可

这种方式,每填一个数,就让限制值的位置减少了至少一个,所以是一定有解的(所有元素相同除外,因为根本填不了)

如果愿意,其实是可以优化的(例如优先选 \(S_i\) 大的),不过没必要,因为是签到(bushi

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
struct Node { int x, pos; };
vector<int> G[N];
int n, a[N], p[N];
bool solve()
{
    //read
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    //solve
    for (int i = 1; i <= n; ++i) G[i].clear();
    for (int i = 1; i <= n; ++i)
        G[a[i]].push_back(i);
    deque<Node> q;
    queue<Node> t;
    for (int x = 1; x <= n; ++x)
        for (int pos : G[x])
            q.push_back((Node){x, pos});
    set<int> s;
    for (int i = 1; i <= n; ++i)
        s.insert(i);
    while (q.front().x > 0) {
        int x = q.front().x;
        while (q.front().x == x) {
            t.push((Node){0, q.front().pos});
            q.pop_front();
        }
        if (q.empty()) return false;
        p[q.front().pos] = x; q.pop_front();
        s.erase(x);
        while (!t.empty()) {
            q.push_back(t.front());
            t.pop();
        }
    }
    while (!q.empty()) {
        Node now = q.front(); q.pop_front();
        int pos = now.pos;
        p[now.pos] = *s.begin();
        s.erase(s.begin());
    }
    return true;
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) {
        if (!solve()) puts("NO");
        else {
            puts("YES");
            for (int i = 1; i <= n; ++i)
                printf("%d ", p[i]);
            puts("");
        }
    }
    return 0;
}

F题 Candies(思维,deque)

额,能够删除的情况一共只有四种:\((a,a),(a,x-a),(x-a,a),(x-a,x-a)\)

那么我们尝试将所有大于 \(\frac{x}{2}\) 的数都变成 \(x-a\),那么我们就可以完全消去操作 2,就变成了一个环上面玩消消乐的流程了(直接 deque 模拟,先消中间的,再消两端的)。

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, x, a[N];
int main()
{
    cin >> n >> x;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
        if (2 * a[i] > x) a[i] = x - a[i];
    }
    deque<int> q;
    int ans = 0;
    for (int i = 1; i <= n; ++i) {
        if (q.empty()) q.push_back(a[i]);
        else {
            if (q.back() == a[i]) {
                ans++;
                q.pop_back();
            }
            else q.push_back(a[i]);
        }
    }
    while (q.size() > 1) {
        if (q.front() == q.back()) {
            q.pop_back(); q.pop_front();
            ans++;
        }
        else break;
    }
    cout << ans;
}

G题 Regular Expression(思维)

对于任意字符串,都可以使用 \(\text{.*}\) 来匹配,那么最小长度的最大值就一定是 2。

  1. \(|s|=1\),此时只有两种匹配:\(a\)\(.\)

  2. \(|s|=2\)

    • \(ab\) 型字符串,样例给出了答案,即六种:\((ab),(a.),(.b),(..),(.*),(.+)\)
    • \(aa\) 型字符串还多了两种:\((a+),(a*)\)
  3. \(|s|>2\)

    此时如果并非所有字符相同,那么仅剩 \((.*),(.+)\) 两种,否则还有 \((a+),(a*)\) 两种

(附:上面的括号啥的仅作分隔用途,不属于正则的一部分)

#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
char s[N];
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) {
        scanf("%s", s + 1);
        int n = strlen(s + 1);
        if (n == 1)
            printf("1 2\n");
        else if (n == 2) {
            if (s[1] == s[2])
                printf("2 8\n");
            else
                printf("2 6\n");
        }
        else {
            int flag = true;
            for (int i = 2; i <= n; ++i)
                if (s[i] != s[i - 1]) flag = false;
            printf("2 %d\n", flag ? 4 : 2);
        }
    }
}

J题 Melborp Elcissalc(前缀和,组合,DP)

给定数字 \(k\),问有多少长度为 \(n\) 的数列,满足:

  1. 每个数都在 \([0,k-1]\)
  2. 恰好有 \(t\) 个子区间,区间和是 \(k\) 的倍数

\(n,k\leq 64\)

区间和的统计,不妨通过前缀和的方式转成单点查询,构造前缀和数组 \({s_n}\)(同时还要对 \(k\) 取模),区间 \([l,r]\) 的和是 \(k\) 的倍数就意味着 \(s_r=s_{l-1}\)。同时,因为元素范围限制,一个前缀和数组恰好对应一个原数组。现在问题转化为了,有多少长度为 \(n+1\) 的数组 \({s_n}\)\(s_0=0\),别的位随机),能够从中找出恰好 \(t\) 对相同的元素?

注意到填的数字的具体位置不影响对数,只影响方案,那么我们可以考虑枚举 \(0,1,\cdots,k-1\) 分别出现了几次,通过这个来判断对数,然后再具体看能构成多少方案。

(为了方便,原题中的 \(k\),我们使用 \(m\) 来代替)定义 \(dp[i][j][k]\) 为已经填到了 \(i\),这些数总计 \(j\) 个(例如填了 3 个 0,4 个 1,2 个 2,那么 \(j=3+4+2=9\)),一共贡献了 \(k\) 对,此时的方案数,那么最终答案是 \(dp[k][n][t]\)

似乎大家基于此得到的 DP 方程各有所不同(正推反推,集中填还是随机填啥的),我的 DP 方程如下:

当前为 \(dp[i][j][k]\),那么我们可以枚举用了 \(x\)\(i\),那么之前的数就还有 \(j-x\) 个,意味着现在有 \(n-(j-x)\) 个空位,那么我们直接在这些空位上选 \(x\) 个位置,也就是 \(C_{n-j+x}^x\)\(x\) 个数可以贡献 \(C_{x}^2\) 对,所以我们从 \(dp[i-1][j-x][k-C_x^2]\) 转移过来,方程为:

\[dp[i][j][k]=\sum\limits_{C_x^2\leq k}C_{n-j+x}^xdp[i-1][j-x][k-C_x^2] \]

对于 \(i=0\) 的地方要额外注意一下,一个是因为 DP 边界条件的原因,还有就是因为维护的是一个前缀和数组的计数 DP,别忘了 \(s_0=0\) 这玩意。

本题复杂度上限是 \(O(64^5)\),也就是 \(O(2^{30})\) 这样,不过跑的不是很满,加一下优化啥的就能过了。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 70, mod = 998244353;
LL c[N][N], dp[N][N][N * N];
int main()
{
    for (int i = 0; i < N; i++)
        for (int j = 0; j <= i; j++)
            c[i][j] = j ? (c[i - 1][j] + c[i - 1][j - 1]) % mod : 1;
    //
    int n, m, t;
    cin >> n >> m >> t;
    for (int i = 0; i <= n; ++i) // i = 0
        dp[0][i][c[i + 1][2]] = c[n][i];
    for (int i = 1; i < m; ++i)
        for (int j = 0; j <= n; ++j)
            for (int x = 0; x <= j; ++x)
                for (int k = c[x][2]; k <= t; ++k)
                    dp[i][j][k] = (dp[i][j][k] + dp[i - 1][j - x][k - c[x][2]] * c[n - j + x][x]) % mod;
    cout << dp[m - 1][n][t] << endl;
    return 0;
}

K题 Great Party(博弈)

给定 \(n\) 堆石子,第 \(i\) 堆的石子的个数为 \(a_i\)

Alice 和 Bob 两个人轮番进行操作,操作选手可以选定某一堆石子,从中拿走一些石子,并可以选择将剩下来的石子合并到另一堆中,最终无法操作者判负。

有多次询问,每次询问中需要求出区间 \([l,r]\) 有多少子区间是先手必胜的。

\(n,q\leq 10^5,1\leq a_i\leq 10^6\)

区间长度为 1,先手必胜(全拿走就行了)。

区间长度为 2,如果两堆石子数量相同,则先手必败。(Alice 选择某一堆拿走 x 个,Bob 则选取另一堆也拿走同样个数,又回到了两队石子相同的情况,以此类推)。同理,两队石子数量不同时则先手必胜。当然,也可以理解为,没有人会手动使得状态回到只剩一堆的情况。

区间长度为 3 时,先手必胜(三个相同或者有两个相同的情况下,一定能先手使得其变为必败态;三个互不相同时(例如 \((x,y,z)\)),直接把 \(z\) 拿到 \(y-x\) 个,然后合并一下)。

区间长度为 4 时,没有人想要让区间回到长度为 3 的状态,也就是最后必然会达到 \((1,1,1,1)\) 的状态,面临该状态者必败,而这可以理解为 \((a_1-1,a_2-1,a_3-1,a_4-1)\) 玩一个 nim 游戏。

接下来,可以找到的题解都没有给出过多解释,只提出了一个结论:区间长度为奇数时,先手必胜(把最多的那一堆拿掉一部分,然后剩下来的合并到另外一堆里面);同时,也得到了偶数时候的策略:集体减 1 的 nim 游戏。

我有一个不太成熟的,关于区间长度是奇数时候的思路:假设以长度为 5 为例,且已经都减去了 1,那么按照大小顺序从小到大排列,为 \((a,b,c,d,e)\),那么我们可以消去 \(e\),并给前面某个数加上 \([0,e]\) 间的所有数。我们令 \(x=a\oplus b\oplus c\oplus d\),记 \(x\) 的最高是 1 的位为 \(k\),那么至少有一个数的这一位不是 \(1\),我们假设就是 \(a\) 吧,那么显然就有 \(a\oplus x>a\),那么我们只需要给 \(a\) 加上 \(a\oplus x-a\),就能使得剩下来的数的异或和是 0 了。虽然不会严格证明,但我凭直觉认为 \(a\oplus b\leq e\)

综上,我们得到的游戏策略如下:

  1. 区间长度为奇数时,先手必胜
  2. 区间长度为偶数时,对区间内所有元素减去 1 后异或,和大于 0 说明显示必胜,反之先手必败

对于查询,考虑到这是离线不带修的,而且似乎也可以 \(O(1)\) 的扩展边界,所以可以莫队搞一手。

(技巧:我们查询的是有多少子区间代表的是胜利,那么我们直接用总区间数量减去负的,一段区间 \([l,r]\) 为负,意味着 \(s_{l-1}=s_r\),那么我们就对于一组查询 \([l,r]\) 变为 \([l-1,r]\),这样就可以变成查询区间里面类似 \(s_x=s_y\) 的数量了)

#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 100010;
int n, q, k, a[N], s[N];
LL C2(LL n) {
    return n * (n - 1) / 2;
}
LL answer, ans[N];
LL cnt[2][N << 6];
struct Node { int L, R, p; } ask[N];
bool cmp(Node a, Node b) { return a.L / k == b.L / k ? a.R < b.R : a.L < b.L; }
void del(int p) {
    LL &v = cnt[p % 2][s[p]];
    answer -= C2(v), v--, answer += C2(v);
}
void add(int p) {
    LL &v = cnt[p % 2][s[p]];
    answer -= C2(v), v++, answer += C2(v);
}
int main()
{
    cin >> n >> q;
    k = sqrt(n);
    for (int i = 1; i <= n; i++)
        cin >> a[i], s[i] = s[i - 1] ^ (a[i] - 1);
    for (int i = 1; i <= q; i++) {
        cin >> ask[i].L >> ask[i].R;
        ask[i].L--, ask[i].p = i;
    }
    sort(ask + 1, ask + 1 + q, cmp);
    int curl = 1, curr = 0;
    for (int i = 1; i <= q; i++) {
        int L = ask[i].L, R = ask[i].R;
        while (curl > L) add(--curl);
        while (curr < R) add(++curr);
        while (curl < L) del(curl++);
        while (curr > R) del(curr--);
        ans[ask[i].p] = C2(R - L + 1) - answer;
    }
    for (int i = 1; i <= q; i++)
        cout << ans[i] << endl;
    return 0;
}
posted @ 2022-08-14 16:07  cyhforlight  阅读(27)  评论(0编辑  收藏  举报