2025-07-22 模拟赛总结 😀
预期:\(100+100+70+30=300\)。
实际:\(100+100+70+30=300\)。
排名:\(rk26/136\)。
比赛链接:http://oj.daimayuan.top/contest/365。
A - LIS:
题意:
给定长度为 \(n(n\le 10^5)\) 的数组 \(a\),定义 \(f(l,r)\) 的值为:从 \(l\) 开始,每次指针指到其右边第一个严格大于它的元素,直到没有这样的元素为止,那么指针经过元素的个数为 \(f(l,r)\) 的值。
你需要求出 \(\sum_{i=1}^n\sum_{j=i}^n f(i,j)\)。
思路:
首先通过单调栈可以线性预处理出每一个元素的右侧第一个比他严格大的元素,令其为 \(p_i\)(不存在即为 \(n+1\)),那一次指针的移动可以抽象成边 \(i\to p_i\)。我们考虑一条路径 \(i\to p_i\to p_{p_i}\to\cdots\to n+1\) 会对答案产生什么影响,前缀路径 \(i\) 长度为 \(1\),那么 \(f(i,j)=1(j\in[i,p_i))\),前缀路径 \(i\to p_i\) 长度为 \(2\),那么 \(f(i,j)=2(j\in[p_i,p_{p_i}))\),以此类推。经典套路可以将其修正贡献为节点 \(i\) 对后面的 \(n-i+1\) 个元素都加一,节点 \(p_i\) 对后面的 \(n-p_i+1\) 个元素都加一,即一条路径的贡献为 \((n-i+1)+(n-p_i+1)+\cdots+(n-(n+1)+1)\)。
题目转化完后,考虑如何实现。我们需要对于每一个 \(i\),找到它走到底的路径,并加上路径中每个节点 \(j\) 的 \(n-j+1\),这显然可以用倍增实现,时间复杂度 \(O(n\log n)\)。
但是还有更好的做法,注意到将 \(i\to p_i\) 连边后,图是一个每个点指向根的森林,我们可以直接“dfs”求每个点到根的路径的所有点权的和,时间复杂度为 \(O(n)\)。
这里当然并不需要真正的 dfs,注意到 \(p_i>i\),那么直接按照 \(n\to 1\) 的拓扑序转移即可。
代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 4e5 + 5;
int T, n, a[kMaxN], stk[kMaxN], top, p[kMaxN];
long long f[kMaxN], ans;
int main() {
ios::sync_with_stdio(0), cin.tie(0);
for (cin >> T; T; T--, ans = top = 0) {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
for (; top && a[stk[top]] < a[i]; top--) {
p[stk[top]] = i;
}
stk[++top] = i;
}
for (int i = n; i; i--) {
ans += f[i] = f[p[i]] + n - i + 1;
p[i] = 0;
}
cout << ans << '\n';
}
return 0;
}
B - 碰瓷:
题意:
我们称数组 \(T\) 能够碰瓷数组 \(S\),当且仅当能通过操作任意次以下操作,使数组 \(T\) 变换为 \(S\)。操作如下:
选择下标 \(1≤i≤|T|\) 和一个整数 \(x\)(需满足 \(x\neq T_i\)),然后在数组 \(T\) 的下标 \(i\) 后插入 \(x\)。
给定长度为 \(m\) 的数组 \(b\) 和长度为 \(n\) 的数组 \(a\),你需要求出 \(a\) 有多少个子段能够被 \(b\) 碰瓷。
数据范围:\(n\le 2\times 10^5,m\le\min(n,100),1\le a_i,b_i\le m\)。
思路:
显然地,我们可以对于以 \(i\) 开头的包含 \(b\) 的最靠前的子段,然后有两个细节:
- 由于不能在第一个元素前面插入,所以第二个元素如果没有被匹配,那么第一个元素和第二个元素不能相同。
- 连续的相同段需要把每个元素挪到尽量靠后,这样就不会出现插入的时候插入与前一个元素相同的情况,贡献为 \(n-back+1\) 。
具体细节看代码。
代码:
#include <bits/stdc++.h>
#define EXIT { flag = 1; break; }
using namespace std;
const int kMaxN = 2e5 + 5, kMaxM = 105;
int T, n, m, a[kMaxN], b[kMaxM], nex[kMaxN][kMaxM], go[kMaxN], p[kMaxM];
long long ans;
int main() {
ios::sync_with_stdio(0), cin.tie(0);
for (cin >> T; T; T--, ans = 0) {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
fill(p, p + 1 + m, n + 1);
for (int i = n; i; i--) {
copy(p, p + 1 + m, nex[i]);
p[a[i]] = i;
go[i] = a[i] == a[i + 1] && i < n ? go[i + 1] : i;
}
go[n + 1] = n + 1, go[0] = 0;
for (int i = 1; i <= m; i++) {
cin >> b[i];
}
if (m == 1) {
for (int i = 1; i <= n; i++) {
if (a[i] == b[1]) {
ans++;
if (i < n && a[i] != a[i + 1]) {
ans += n - i;
}
}
}
} else {
for (int i = 1; i <= n; i++) {
if (a[i] == b[1]) {
bool flag = 0;
int j = 2, l = i, r = nex[l][b[j]];
if (l + 1 < r && a[l] == a[l + 1]) continue;
vector<int> v;
for (; j <= m; l = r, r = nex[l][b[++j]]) {
v.push_back(l);
if (r > n) EXIT
if (j == m) break;
}
v.push_back(r);
if (flag) continue;
vector<int> t = v;
for (int i = v.size() - 2; ~i; i--) {
v[i] = go[v[i]];
if (i != v.size() - 1) v[i] = min(v[i], v[i + 1] - 1);
}
if (v[0] != i) continue;
ans++;
v = t;
for (int i = v.size() - 1; ~i; i--) {
v[i] = go[v[i]];
if (i != v.size() - 1) v[i] = min(v[i], v[i + 1] - 1);
}
if (v[0] != i) continue;
ans += n - t.back();
}
}
}
cout << ans << '\n';
}
return 0;
}
反思:
我写的这玩意巨恶心,写了 2h,中途细节没考虑全面,对着大样例调了好久,其实有很简单的方法,在我思路的基础上再想一下即可,但是我的思路过于混乱,没想清楚就写代码。以后要在纸上多写写,尤其是细节多的题目,想清楚再写代码,不要被其他人的节奏带动,你想清楚后再写不一定比别人先写写得慢。
C - 旋律:
题意:
现在有一个字符串 \(s\) 初始为空,每次操作你需要在 \(s\) 的开头或者 \(s\) 的末尾插入一个字符 \(c\),每次操作完后求 \(s\) 的周期数量。
我们定义一个整数 \(k\) 是字符串 \(t\) 的周期当且仅当,\(\forall i+k\le|t|,t_i=t_{i+k}\)。
思路:
赛时:
求字符串的 \(s\) 的周期数,很经典,先 kmp 求出 border,那么 \(n-border[n]\) 肯定是 \(s\) 的一个周期,同理 \(n-border[border[n]]\) 肯定也是,以此类推,一直跳 border 即可。
时间复杂度:\(O(n^2)\)。
(十几分钟能拿 70pts,我为什么要去想正解。
赛后:
前面的暴力我们是枚举第几个字符串求所有的周期,这样做你必须使用 kmp(?)来求,时间复杂度优化不了,我们需要充分利用条件每次只在开头和结尾加字符。我们换一个维度思考,我们考虑枚举周期,看有多少个字符串符合这个周期(对于每个字符串判断是简单的,可以用 hash 实现),由于每次只在开头和结尾加字符,那么这个东西是有单调性的,我们可以直接二分,然后将答案区间加一即可。
这个题的难点在于转换枚举的维度,发现单调性。
思路:
70pts:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 1e6 + 5;
int T, n, o, nex[kMaxN], ans;
char c;
string s, t;
int main() {
ios::sync_with_stdio(0), cin.tie(0);
for (cin >> T; T; T--, ans = 0) {
cin >> o >> c, n++;
s = o == 1 ? c + s : s + c;
t = ' ' + s, nex[1] = 0;
for (int i = 2, j = 0; i <= n; i++) {
for (; j && t[j + 1] != t[i]; j = nex[j]) {
}
j += t[j + 1] == t[i], nex[i] = j;
}
for (int i = nex[n]; i; i = nex[i]) ans++;
cout << ans + 1 << '\n';
}
return 0;
}
100pts:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 1e6 + 5, kB = 131, kM = 998244353;
int n, o[kMaxN], l[kMaxN], r[kMaxN], L, R, ans[kMaxN];
char t[kMaxN], c[kMaxN];
long long h[kMaxN], p[kMaxN];
long long H(int l, int r) {
return (h[r] - h[l - 1] * p[r - l + 1] % kM + kM) % kM;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> o[i] >> t[i];
}
L = 1, R = n;
for (int i = n; i; i--) {
l[i] = L, r[i] = R;
if (o[i] == 1) c[L++] = t[i];
else c[R--] = t[i];
}
p[0] = 1;
for (int i = 1; i <= n; i++) {
h[i] = (h[i - 1] * kB + c[i]) % kM;
p[i] = p[i - 1] * kB % kM;
}
for (int i = 1, L, R, M; i <= n; i++) {
L = i, R = n, M = L + R + 1 >> 1;
for (; L < R; M = L + R + 1 >> 1) {
H(l[M], r[M] - i) == H(l[M] + i, r[M]) ? L = M : R = M - 1;
}
ans[i]++, ans[L + 1]--;
}
for (int i = 1; i <= n; i++) {
ans[i] += ans[i - 1];
cout << ans[i] << '\n';
}
return 0;
}
反思:
遇到字符串的题目,如果你想了很久做不出来时,往往需要充分利用题目的条件,题目的任何条件都可能成为解题的关键。并且不要老想着 kmp、AC 自动机等现成的算法,一道字符串题也有可能是 hash。
D - 非负:
题意:
定义一个整数数组 \(x_1,x_2,\cdots, x_n\) 为好的,当且仅当对于任意的 \(i\in[1,n]\),都满足 \(\sum_{j=1}^i x_j\ge 0,\sum_{j=i}^n x_j\ge 0\)。
现在给出初始的 \(a_1,a_2,\cdots,a_n\in\{-1,1\}\),有 \(q\) 次操作:
- 将其中一个数取相反数。
- 求一个区间内的最长的好的子序列长度。
思路:
赛时:
我们考虑 dp,前缀非负限制好处理,顺序做的时候记录在 dp 数组里的第二维即可,后缀非负的限制,我们可以记录考虑到当前还有多少个 -1 没有被其之后的 1 匹配,显然合法情况这个值为 0(但是 dp 转移的过程中可以出现正值)。那么设 \(f_{i,j,k}\) 表示选到第 \(i\) 位,前 \(i\) 位的和为 \(j\),还没有被其之后的 1 匹配的 -1 的数量为 \(k\) 的最长长度,转移很简单。时间复杂度 \(O(qn^3)\)。
考虑优化,显然我们想要让没有被匹配的 -1 数量尽可能的小,那么我们可以设出新的 dp 数组,设 \(f_{i,j}\) 表示选到第 \(i\) 位,前 \(i\) 位的和为 \(j\) 的最长长度,\(g_{i,j}\) 表示 \(f_{i,j}\) 最大的前提下,未匹配的 -1 数量最小是多少。这个也很好转移,但是我不确定这种方法是正确的,可能只是恰好能过大样例。时间复杂度:\(O(qn^2)\)。
子任务 1 的 2 分太简单了,不讲了。
赛后:
没搞懂,以后补。
代码:
\(O(qn^2)\) 的代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 3005;
int n, q, a[kMaxN], f[kMaxN][kMaxN], g[kMaxN][kMaxN], ans;
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
memset(f, 0xc0, sizeof(f));
memset(g, 0x3f, sizeof(g));
for (int o, l, r; q; q--, ans = 0) {
cin >> o;
if (o == 1) {
cin >> l, a[l] = -a[l];
} else {
cin >> l >> r;
f[l - 1][0] = g[l - 1][0] = 0;
for (int i = l; i <= r; i++) {
for (int j = 0; j <= i - l + 1; j++) {
if (f[i][j] < f[i - 1][j]) {
f[i][j] = f[i - 1][j], g[i][j] = g[i - 1][j];
} else if (f[i][j] == f[i - 1][j]) {
g[i][j] = min(g[i][j], g[i - 1][j]);
}
if (a[i] == 1) {
if (f[i][j + a[i]] < f[i - 1][j] + 1) {
f[i][j + a[i]] = f[i - 1][j] + 1;
g[i][j + a[i]] = max(0, g[i - 1][j] - 1);
} else if (f[i][j + a[i]] == f[i - 1][j] + 1) {
g[i][j + a[i]] = min(g[i][j + a[i]], max(0, g[i - 1][j] + 1));
}
} else if (j >= 1) {
if (f[i][j + a[i]] < f[i - 1][j] + 1) {
f[i][j + a[i]] = f[i - 1][j] + 1;
g[i][j + a[i]] = g[i - 1][j] + 1;
} else if (f[i][j + a[i]] == f[i - 1][j] + 1) {
g[i][j + a[i]] = min(g[i][j + a[i]], g[i - 1][j] + 1);
}
}
}
}
for (int i = 0; i <= n; i++) {
if (g[r][i] == 0) ans = max(ans, f[r][i]);
}
cout << ans << '\n';
for (int i = l - 1; i <= r; i++) {
for (int j = 0; j <= i - l + 2; j++) {
f[i][j] = -1e9;
}
}
}
}
return 0;
}
总结:
时间分配:\(30+120+15+70\)。(我晚来了 5min)
B 属实想太久了,D 很快就想出了暴力 dp,但是最后优化到 \(O(qn^2)\) 后发现 5000 的数据跑的很快,于是在卡常,卡了起码有 25 min,一点用都没有。其次我发现我代码能力真的不行,写有一点点细节的代码就要写好久,B 题和 D 题都是的,以后要加强一下。
总的来说,这次时间分配不合理,D 题不应该卡常卡那么久,放到 C 题上会更好;并且以后要提升提升代码能力。(不然太菜了)

浙公网安备 33010602011771号