蔚来杯2022牛客暑期多校训练营7 CFGJ

2022-10-15,,

比赛链接

C

题解

方法一

知识点:思维。

先统计没有出现的数,每个都可以随便放,所以作为补位用的。

将原数组左移一位作为预定的答案数组,然后开始检查。如果和原数组一样,则用补位数字填充,如果不一样就不动。

这样做是肯定能构造出一个合法的数组的,因为左移以后这个数字无论重复不重复一定能有一个是和对应位置不一样的,于是这个数字就被用掉了。因此,原数组出现的所有数字都能有一个到位,剩余位置都是和答案数组重复,就一定能给到补位的数字。

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

方法二

知识点:思维。

先构造一个 \([1,...,n]\) 的预定的答案数组,然后开始检查对应位置是否重复。如果重复就用后面一个数字和这个数字交换,一定能保证这个位置不重复。这样可以构造到 \(n-1\) 位置,最后一个位置要特殊处理,因为如果重复没有后面换的数字了,需要从前面找一个交换。

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

代码

方法一

#include <bits/stdc++.h>
#define ll long long using namespace std; int a[100007];
bool vis[100007]; bool solve() {
int n;
cin >> n;
for (int i = 1;i <= n;i++) vis[i] = 0;
for (int i = 1;i <= n;i++) {
cin >> a[i];
vis[a[i]] = 1;
}
vector<int> v;
int cnt = 0;
for (int i = 1;i <= n;i++) {
if (!vis[i]) v.push_back(i);
else vis[i] = 0, cnt++;
}
if (cnt == 1) return false;
cout << "YES" << '\n';
for (int i = 2;i <= n;i++) {
if (vis[a[i]] || a[i] == a[i - 1]) {
cout << v.back() << ' ';
v.pop_back();
}
else {
cout << a[i] << ' ';
vis[a[i]] = 1;
}
}
if (v.empty()) cout << a[1] << '\n';
else cout << v.back() << '\n';
return true;
} int main() {
std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t = 1;
cin >> t;
while (t--) {
if (!solve()) cout << "NO" << '\n';
}
return 0;
}

方法二

#include <bits/stdc++.h>
#define ll long long using namespace std; int a[100007], b[100007]; bool solve() {
int n;
cin >> n;
for (int i = 1;i <= n;i++) {
cin >> a[i];
b[i] = i;
}
bool ok = true;
for (int i = 2;i <= n;i++)
ok &= a[i] == a[i - 1];
if (ok) return false;
for (int i = 1;i <= n - 1;i++)
if (a[i] == b[i]) swap(b[i], b[i + 1]);
if (a[n] == b[n]) {
for (int i = 1;i <= n - 1;i++) {
if (a[i] != b[n]) {
swap(b[i], b[n]);
break;
}
}
}
cout << "YES" << '\n';
for (int i = 1;i <= n;i++) cout << b[i] << ' ';
cout << '\n';
return true;
} int main() {
std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t = 1;
cin >> t;
while (t--) {
if (!solve()) cout << "NO" << '\n';
}
return 0;
}

F

题解

方法一

知识点:模拟,贪心。

用一个数组存数字,另一个数组用下标指向模拟链表,每次删除完成修改下标指向,当作链。再用个数组记录是否被删掉。

由于是从左往右遍历,向左扩展就用链表数组,向右扩展就只需要加一。如果右端点碰到了之前删掉过的数字就可以直接结束了,因为这个数字只可能通过之前向左扩展到这里,也就是说之后的所有数字都被删掉了,不需要继续了。

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

方法二

知识点:贪心。

一个最简单的写法,直接在线处理输入的点。用双端队列存储数字,进入一个数就和队尾匹配看能不能删除,不能删除就存入;如果队空也直接存入。

最后输入完了,我们只处理了能向左删除的数字,没有处理向右的尾部到头部的环状性质删除,因此还需要继续处理。

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

代码

方法一

#include <bits/stdc++.h>

using namespace std;

int a[100007], p[100007];
bool vis[100007]; int main() {
std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, x;
cin >> n >> x;
int cnt = 0;
for (int i = 0;i < n;i++) cin >> a[i];
for (int i = 0;i < n;i++) p[i] = (i - 1 + n) % n;
for (int t = 0;t < n;t++) {
if (vis[t]) continue;
int i = t, j = (t + 1) % n;
if (vis[j]) break;
while (i != j && (a[i] == a[j] || a[i] + a[j] == x)) {
vis[i] = 1;
vis[j] = 1;
cnt++;
i = p[i];
j = (j + 1) % n;
if (vis[j]) break;
}
p[j] = i;
}
cout << cnt << '\n';
return 0;
}

方法二

#include <bits/stdc++.h>

using namespace std;

int main() {
std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, x;
cin >> n >> x;
deque<int> dq;
int cnt = 0;
for (int i = 0, tmp;i < n;i++) {
cin >> tmp;
if (!dq.empty() && (dq.back() == tmp || dq.back() + tmp == x)) dq.pop_back(), cnt++;
else dq.push_back(tmp);
}
while (dq.size() >= 2 && (dq.front() == dq.back() || dq.front() + dq.back() == x)) {
dq.pop_front();
dq.pop_back();
cnt++;
}
cout << cnt << '\n';
return 0;
}

G

题解

知识点:字符串,模拟。

分类讨论一下。

\(n=1\) ,最短长度 \(2\) , a. ,\(2\) 种。

\(n=2\) ,最短长度 \(2\) ,aba..b...*.+ ;如果两个字母一样还可以,a+a* ,\(6\) 或 \(8\) 种。

\(n\geq3\) ,最短长度 \(2\) ,.*.+ ;如果字母一样还可以,a+a* ,\(2\) 或 \(4\) 种。

也就是说,( ) | ? 都没用,\(998244353\) 也没用。诈骗题qwq。

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

代码

#include <bits/stdc++.h>

using namespace std;

const int mod = 998244353;

int main() {
std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int q;
cin >> q;
while (q--) {
string s;
cin >> s;
int n = s.size();
if (n == 1) cout << 1 << ' ' << 2 << '\n';
else if (n == 2) {
cout << 2 << ' ';
if (s[0] == s[1]) cout << 8 << '\n';
else cout << 6 << '\n';
}
else if (n >= 3) {
bool ok = true;
for (auto c : s) ok &= s[0] == c;
cout << 2 << ' ';
if (ok) cout << 4 << '\n';
else cout << 2 << '\n';
}
}
return 0;
}

J

题解

知识点:计数dp,排列组合。

首先是一个套路的区间和问题转化为前缀和端点问题。由于数字范围是 \([0,k-1]\) ,在 \(\mod k\) 下的前缀和是唯一对应的,即一个前缀和序列能唯一确定一个原序列,所以我们可以通过直接考虑前缀和序列,替代考虑原序列。

题目要求区间和能被 \(k\) 整除的区间是有贡献的,也就是前缀和序列的两个不同端点 \(i,j\) 满足 \(sum[i] \equiv sum[j](\mod k)\) 就对应了一个区间满足。特别地,当 \(sum[i] = 0\) 时,即单点整除,自己就能成为一个区间要特判。

对此不妨先看一个简单的版本,给定了一个序列,求区间满足区间和能被 \(k\) 整除的个数,即贡献。根据上面说的,这个序列对应了唯一一个前缀和序列,而前缀和序列只需要两个不同端点满足模 \(k\) 余数相同或者一个点自己能被 \(k\) 整除即可。因此,找出 \([0,k-1]\) 每个数在前缀和序列出现的次数 \(cnt_i\),于是每个数的贡献是 \(\sum C_{cnt_i}^2\) ,再特判单点整除的情况 \(C_{cnt_0}^1 = cnt_0\) 。

在回来看原问题,求构造由 \([0,k-1]\) 组成的长度 \(n\) 的序列,且所有区间中满足区间和被 \(k\) 整除的个数(贡献)为 \(t\) 的方案数。我们可以在原问题的求解过程内增加一个dp前缀和序列的过程,转化成一个计数dp。

\(dp[i][j][u]\) 表示为考虑到第 \(i\) 个数,已经选了 \(j\) 个数进入前缀和序列, 贡献为 \(u\) 的方案数。

初始状态是 \(dp[0][0][0] = 1\)。

对于 \(dp[i-1][j][u]\) 的下一个状态,在 \(i > 1\) 时,即选的数大于 \(0\) ,不用考虑自身成区间的问题,因此选了 \(v\) 个第 \(i\) 个数后状态是 \(dp[i][j+v][u+C_v^2]\) ,因为新选的数与以前的数没关系只会自己和自己组合,贡献只需要加上 \(C_v^2\) 即可;而 \(i = 1\) ,即选 \(0\) 时,由于自己可以单独代表一个长度为 \(1\) 合法区间,因此下一个状态是 \(dp[i][j+v][u+C_{v+1}^2]\) ,其中 \(C_{v+1}^2 = C_{v}^{2} + C_{v}^{1}\) ,可以理解为在前缀和序列首元素之前有个 \(0\) 元素可以匹配。

接下来是状态转移方程,由于 \(dp[i-1][j][u]\) 已经代表了这个状态的所有方案,而新加进来的 \(v\) 个数只需要在 \(v+j\) 的位置里面选 \(v\) 个放入,剩下的数的方案刚好就是 \(dp[i-1][j][u]\) ,因此下一个状态的方案数是 \(C_{v+j}^{v}dp[i-1][j][u]\) 。因此转移方程是:

\[\left \{
\begin{aligned}
dp[i][j+v][u+C_{v+1}^{2}] &= C_{v+j}^{v}dp[i-1][j][u],i = 1\\
dp[i][j+v][u+C_{v}^{2}] &= C_{v+j}^{v}dp[i-1][j][u],i > 1
\end{aligned}
\right.
\]

最后这样直接写是会超时的,因为有很多分支是不必要的,发现 \(dp[i-1][j][0] = 0\) 时后面压根不需要选数就少了一个 \(v\) 循环。其实这种状态转移的方法叫刷表法,通过之前一个状态去更新它可能到达的所有状态;而填表法,通过现在的状态去找和它相关的状态去更新自己。这道题用填表法就没法优化,而用刷表法能立马看出优化的点。

注意数组的 \(u\) 维要开到两倍 \(t\) 的大小,也就是 \(n^2\) ,防止 \(u+v = t+c[n][2] \approx n^2\) 的情况出现。

组合数可以用 \(C_n^m = C_{n-1}^m + C_{n-1}^{m-1}\) 递推。

时间复杂度 \(O(n^2tk)\)

空间复杂度 \(O(ntk)\)

代码

#include <bits/stdc++.h>

using namespace std;

const int mod = 998244353;
int c[70][70];
int dp[70][70][70 * 70];///考虑到 i-1 个数字 ,已经选了j个,贡献k;70*70比2t大,不用担心u+c[v][2]越界 ///由于数字范围[0,k-1]因此原数组和前缀和数组是一一对应的,因此考虑枚举前缀和数组,将区间和转化为端点判断
///区间和能被k整除对应端点i,j有sum[i]%k = sum[j]%k(i!=j)
///特殊地,当sum[i] = 0时,自己也可以符合条件,因此要特判 int main() {
std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, k, t;
cin >> n >> k >> t;
for (int i = 0;i <= n + 1;i++) {
for (int j = 0;j <= i;j++) {
if (!j) c[i][j] = 1;
else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
}
}
///刷表法能剪枝,打表法不能
dp[0][0][0] = 1;
for (int i = 1;i <= k;i++) {
for (int j = 0;j <= n;j++) {
for (int u = 0;u <= t;u++) {
if (!dp[i - 1][j][u]) continue;
if (i == 1) {
for (int v = 0;v + j <= n;v++)
dp[i][j + v][u + c[v + 1][2]] = (dp[i][j + v][u + c[v + 1][2]] + 1LL * c[j + v][v] * dp[i - 1][j][u]) % mod;
///原本是c[v][2] 但 0可以自成一个,所以是c[v][2] + v = c[v+1][2] ,可以理解为在前缀和0号位有个0匹配
}
else {
for (int v = 0;v + j <= n;v++) {
dp[i][j + v][u + c[v][2]] = (dp[i][j + v][u + c[v][2]] + 1LL * c[j + v][v] * dp[i - 1][j][u]) % mod;
}
}
}
}
}
cout << dp[k][n][t] << '\n';
return 0;
}

蔚来杯2022牛客暑期校训练营7 CFGJ的相关教程结束。

《蔚来杯2022牛客暑期多校训练营7 CFGJ.doc》

下载本文的Word格式文档,以方便收藏与打印。