2022 牛客多校 第七场 题解

比赛链接

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

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

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

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

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

如果愿意,其实是可以优化的(例如优先选 Si 大的),不过没必要,因为是签到(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,xa),(xa,a),(xa,xa)

那么我们尝试将所有大于 x2 的数都变成 xa,那么我们就可以完全消去操作 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(思维)

对于任意字符串,都可以使用 .* 来匹配,那么最小长度的最大值就一定是 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,k1]
  2. 恰好有 t 个子区间,区间和是 k 的倍数

n,k64

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

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

(为了方便,原题中的 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],那么我们可以枚举用了 xi,那么之前的数就还有 jx 个,意味着现在有 n(jx) 个空位,那么我们直接在这些空位上选 x 个位置,也就是 Cnj+xxx 个数可以贡献 Cx2 对,所以我们从 dp[i1][jx][kCx2] 转移过来,方程为:

dp[i][j][k]=Cx2kCnj+xxdp[i1][jx][kCx2]

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

本题复杂度上限是 O(645),也就是 O(230) 这样,不过跑的不是很满,加一下优化啥的就能过了。

#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 堆的石子的个数为 ai

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

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

n,q105,1ai106

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

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

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

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

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

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

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

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

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

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

#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; }

__EOF__

本文作者cyhforlight
本文链接https://www.cnblogs.com/cyhforlight/p/16585616.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   cyhforlight  阅读(40)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· Apache Tomcat RCE漏洞复现(CVE-2025-24813)
点击右上角即可分享
微信分享提示