2022 牛客多校 第七场 题解
C题 Constructive Problems Never Die(思维,构造)
大胆猜结论:除了所有元素相同的情况,否则一定存在一组解(实际上也确实是的)。
这题似乎解法特别多,这里贡献一下我们的做法:
- 按照顺序,统计一下不可以填 1 的位置的集合 \(S_1\),不可以填 2 的位置的集合 \(S_2\),一直到 \(S_n\),外加一个空的 \(S_{n+1}\)
- 维护一个包含 1-n 内所有数的集合 \(T\)
- 先检查 \(S_1\)
- 如果空,则跳过
- 如果非空,那么
- 从后面非空集合中随便选出一个位置,填上 1,并从 \(T\) 中删除 1
- 现在 \(S_1\) 内的所有位置都是可选的了,将他们都放入 \(S_{n+1}\)
- 一直重复该流程,直到 \(S_n\)
- 现在将 \(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。
-
\(|s|=1\),此时只有两种匹配:\(a\) 和 \(.\)
-
\(|s|=2\)
- \(ab\) 型字符串,样例给出了答案,即六种:\((ab),(a.),(.b),(..),(.*),(.+)\)
- \(aa\) 型字符串还多了两种:\((a+),(a*)\)
-
\(|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\) 的数列,满足:
- 每个数都在 \([0,k-1]\) 内
- 恰好有 \(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]\) 转移过来,方程为:
对于 \(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 后异或,和大于 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;
}