2022牛客多校07题解 CFGJK
牛客多校07题解 CFGJK
赛时CFG(都是1A,开心) + 补题JK
https://ac.nowcoder.com/acm/contest/33192
C - Constructive Problems Never Die
题意
给定数列a,求构造同等长度的数列p,满足所有 \(p_i\neq a_i\)
分析
先按照 \(1,2,...,n\) 升序构造p,顺便把\(p_i=a_i\)的所有位置 \(i\) 记录下来
若冲突数为 \(cnt\),则:
- \(cnt=0\), 无冲突,直接输出
- \(cnt=1\), 有一个冲突,要在其他p位置上能找到一个能交换的 \(p_i\)
- \(cnt>1\), 有多个冲突,由于构造的时候保证了所有 \(p_i\) 互不相同,所以只需将这些冲突的交换下顺序即可(偏移一位)
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int a[N], ans[N], n;
void solve () {
scanf ("%d", &n);
vector <int> v;
for (int i = 1; i <= n; i ++) {
scanf ("%d", &a[i]);
ans[i] = i;
if (i == a[i]) v.push_back(i);
}
int m = v.size();
if (m == 1) {
bool all_same = true;
for (int i = 1; i <= n; i ++) {
if (a[i] != a[v[0]]) {
swap (ans[i], ans[v[0]]);
all_same = false;
break;
}
}
if (all_same) {
printf ("NO\n");
return ;
}
}
else if (m > 1) {
//冲突的集体偏移一位即可
int tmp = ans[v[m-1]];
for (int i = m-1; i > 0; i --)
ans[v[i]] = ans[v[i-1]];
ans[v[0]] = tmp;
}
printf ("YES\n");
for (int i = 1; i <= n; i ++)
printf ("%d ", ans[i]);
printf("\n");
}
int main () {
int t;
scanf ("%d", &t);
while (t --) {
solve ();
}
}
//按照升序构造,找冲突
//冲突数>1,偏移构造
//冲突数=1,枚举可能交换的情况
//冲突数=0,直接输出
//要用scanf
F - Candies
题意
数列 \(a\) 种相邻两个一样的数字或相邻两个和为x的数字可以被一起删掉
问最多能删多少次
注:\(a_1\) 与 \(a_n\) 也相邻
分析
对于每一对可删的向外拓展比如当前删了\(a_i,a_j\),则接下来看\(a_{i-1},a_{j+1}\),若可删,则继续看\(a_{i-2},a_{j+2}\)...以此类推,若拓展到某一对没法删除,则中断拓展(对应到代码中就是出队),继续找下一对能删的
用deque可以实现便捷操作
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n, x, a[N], ans;
int main () {
cin >> n >> x;
for (int i = 1; i <= n; i ++) cin >> a[i];
deque <int> q;
for (int i = 1; i <= n; i ++) {
if (q.empty()) q.push_back (a[i]);
else {
int cur = q.back();
if (cur == a[i] || cur + a[i] == x) q.pop_back (), ans ++;
else q.push_back(a[i]);
}
}
//仍有剩余则队内合并
while (q.size() > 1) {
if (q.front() == q.back() || q.front() + q.back() == x) {
ans ++, q.pop_back(), q.pop_front();
}
else break; //不能连续向外拓展了
}
cout << ans << endl;
}
//1. 相邻两个一样的数字删掉
//2. 相邻两个和为x的数字删掉
//问最多能删多少次
//注:a[1]与a[n]也相邻
//对于一对可以拓展的,往两边延伸
G - Regular Expression
题意
给了一堆符号,分别有不同的含义(正则表达式)
现给n个字符串,分别求出每个字符串使用上述符号能表示出来的最小长度,以及最小程度下有多少种可能的表示。(最小长度+种类)
分析
虽然给了很多符号,但是实际上能实现减少长度的符号表示就只有* + .
这三种
*
代表复制*
前的字符0至无穷次
+
代表复制+
前的字符1至无穷次
.
可以指带任一字符
按照字符串长度可划分为三类:
- 长度为1,只可能是
a, .
共2种可能(自身/.指代) - 长度为2,若所有字符相同,则有
aa, a., .a, a*, a+, .., .*, .+
共8种可能;若有字符不同,则有aa, a., .a, .., .*, .+
共6种可能 - 长度大于2,若所有字符相同,则有
a*, a+, .*, .+
共4种可能;若有字符不同,则有.*, .+
共2种可能
Code
#include <bits/stdc++.h>
using namespace std;
bool same (string s) {
for (int i = 1; i < s.size(); i ++)
if (s[i] != s[i-1])
return false;
return true;
}
void solve () {
string s;
cin >> s;
int n = s.size(), len = 2, cnt;
if (n == 1) len = 1, cnt = 2; //a, .
else if (n > 2) {
if (same (s)) cnt = 4; //a*, a+, .*, .+
else cnt = 2; //.*, .+
}
else if (n == 2) {
if (same(s)) cnt = 8; //aa, a., .a, a*, a+, .., .*, .+
else cnt = 6; //ab, a., .b, .*, .+, ..,
}
cout << len << ' ' << cnt << endl;
}
int main () {
int t;
cin >> t;
while (t --) {
solve ();
}
}
//有用的就只有* + .
//长度分为 1,2,>2考虑
J - Melborp Elcissalc
题意
给定一个数字 k, 问有多少种长度为 n的数组,满足:
- 数的范围为 [0,k−1]
- 有 t 个区间,其区间和为 k 的整数倍。
分析
转化为前缀和数组,区间和为k的倍数就等价于\((pre[r]-pre[l-1])\%k==0\)
即 \(pre[r]\%k==pre[l-1]\%k\),即前缀和数组有两点 \(\%k\) 后相等则存在这样的区间
数\([0,k-1]\)在pre[]中出现了\(cnt\)次,则会有\(\C_{cnt}^2\)个区间
\(f[i][j][r]\): 拿\([0,i]\)范围内的数,已经拿了 \(j\) 个数,数组\(goodness\) 为 \(r\) 的方案数
\(cnt[i]:\) 前缀和数组 \(s\) 下,模数\(i\) 出现的次数
转移:
\(f[i][j][r] += f[i-1][j-cnt][r-C[cnt][2]] * C[j][cnt];\) 选两个相同的端点
初始化:长度为0时,\([1,i]\) 的\(goodness\) 为 \(\C_2^i\)
记得预处理出组合数
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 70, mod = 998244353;
int C[N][N], f[N][N][N*N]; //拿[0,i]范围内的数,已经拿了j个数,数组goodness为r的方案数
void pre () {
for (int i = 0; i < N; i ++)
for (int j = 0; j <= i; j++) {
if (j == 0) C[i][j] = 1;
else C[i][j] = (C[i-1][j-1] + C[i-1][j]) % mod;
}
for (int i = 0; i <= 65; i ++) {
f[0][i][(i+1)*i/2] = 1; //长度为0时有一种方案
}
}
signed main () {
pre();
int n, k, t;
cin >> n >> k >> t;
for (int i = 1; i < k; i++)
for (int j = 0; j <= n; j++)
for (int cnt = 0; cnt <= j; cnt++)
for (int r = C[cnt][2]; r <= t; r++)
f[i][j][r] = (f[i][j][r] + f[i-1][j-cnt][r-C[cnt][2]]*C[j][cnt]) % mod;
k --;
cout << f[k][n][t] << endl;
}
//f[i][j][r]: 拿[0,i]范围内的数,已经拿了j个数,数组goodness为r的方案数
//cnt[i]:前缀和数组s下,模数i出现的次数
//i出现的次数为cnt
//f[i][j][r] += f[i-1][j-cnt][r-C[cnt][2]] * C[j][cnt]; //选两个相同的端点
//长度为0时,[1,i]的goodness为C[2][i]
//预处理出组合数
K - Great Party
博弈论+莫队
题意
两个人玩石子游戏。有多堆石子,两个人轮流操作。
每一轮,都必须选择一堆石子,拿走其中的至少一个石子。
然后你可以将选择的这一堆石子,和另外的一堆石子合并,也可以不合并。
现有m个询问,每个询问下都有区间[L,R],问该区间下有多少个子区间,在该子区间上先手必胜
分析
区间长度为奇数时先手必胜,因为
先手可以通过一次操作,带上合并,使得出现两个相同的数。转化为必胜态。
区间长度为偶数时转化为\(a_i-1\)的Nim游戏,因为
两人都不会在这一轮减少石子堆数(即进行合并操作或拿完一堆石子),原因是这么做会使得下一个人进入必胜态。那么该僵局会在1,1,...,1时被打破(此时下一个操作的人必定会使得石子堆数减少,即必输)。类比推理Nim游戏最后会出现0,0,...,0
的局面,容易将本问题转化为\(a_i-1\)的nim游戏
分奇偶,用莫队统计即可
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5, M = 4000000;
int n, m, len, num;
int a[N], pre[N], ans[N], cot[2][M]; //分奇偶统计
struct Node {
int l, r, id;
bool operator<(const Node &t) const {
if (l/len != t.l/len) return l < t.l;
if (l/len & 1) return r < t.r;
return r > t.r; //奇偶排序优化
}
}Q[N];
inline int c2(int n) {
return n * (n - 1) / 2;
} //组合数
inline void add(int p){
// pre[p]
num -= c2(cot[p % 2][pre[p]]);
cot[p % 2][pre[p]]++;
num += c2(cot[p % 2][pre[p]]);
}
inline void del(int p){
num -= c2(cot[p % 2][pre[p]]);
cot[p % 2][pre[p]]--;
num += c2(cot[p % 2][pre[p]]);
}
signed main () {
cin >> n >> m;
len = sqrt(n);
for (int i = 1; i <= n; i++) {
cin >> a[i];
pre[i] = pre[i-1] ^ (a[i] - 1); //a[i]-1的nim游戏
}
for (int i = 0; i < m; i++) {
int l, r;
cin >> l >> r;
l --; //[l-1, r]
Q[i] = {l, r, i};
}
sort(Q, Q + m);
//后面直接套莫队
int l = 1, r = 0;
for (int i = 0; i < m; i ++) {
int L = Q[i].l, R = Q[i].r;
while (l > L) add (--l); //左端点左移+
while (r < R) add (++r); //右端点右移+
while (l < L) del (l++); //左端点右移-
while (r > R) del (r--); //右端点左移-
ans[Q[i].id] = c2(Q[i].r - Q[i].l + 1) - num;
}
for (int i = 0; i < m; i++) cout << ans[i] << endl;
}
//区间长度为奇数时,先手必胜。
//区间长度为偶数时,执行减一的Nim游戏。