2025 长训复盘
10.9 Day 16
2025 CSP-S 模拟赛 Day 16 - 比赛主页 - 比赛 - Daimayuan Online Judge
T1 双重心
题意:有一颗树,可以切断一条边,在连接一条边,对于每一个点都要求出:多少种方案使得操作后这个点是树的双重心之一。
考场上想了一对双重心的性质,发现一点用都没有,打了一个暴力
正解:分类讨论一下:
- 是原树的双重心之一,考虑把这条边割掉,接到另一个连通块的任意一个点上都是可行的。
- 不割掉原树上的双重心的边,两侧的连通块内的的任意一条边可以断开,连通块内相互连边就行。
- 考虑一条边成为新的双中心之间的边,设这条边为 \((u,v)\),\(u\) 为深度较深的点,如果 \(siz_u > n / 2\),那么就是要在 \(u\) 的子树内割下一块大小等于 \(siz_u - n / 2\) 的子树,可以直接开一个桶来计算,在进入子树 \(u\) 时记录一下大小,离开子树时再记录一下当前的大小,做差就可以得出。如果 \(siz_u < n / 2\),那么就是要在子树外选大小为 \(n/2-siz_u\) 大小的部分割掉,这个部分就是全局大小等于 \(n/2-siz_u\) 的子树减去 \(u\) 子树内大小等于 \(n/2-siz_u\) 的子树个数,这两个部分非常好算,此时我们发现在 \(v\) 到根的这条链上子树大小满足要求的点是不能取的,需要减掉,然而有一些点和子树 \(u\) 以外的点可以组成大小可以满足条件,要将这一部分减去,其实可以开一个桶记录一个点到根的路径上的点的子树大小,在 dfs 离开一个点时回溯,将他减去就可以了。此时的答案就是 全局的-子树内的-链上不符合条件的+链上新增的。
分别计算这些部分即可,1、2 可以一起算,第三个另外算。细节比较多。
核心代码:
int n, type, siz[MAXN], sz[MAXN], f[MAXN];
vector<int> g[MAXN], v;
vector<pair<int, int>> p;
ll buc[MAXN], cnt[MAXN], ins[MAXN], ous[MAXN];
ll ans[MAXN];
void dfs0(int x, int fa)
{
siz[x] = 1, f[x] = fa;
int wt = 0;
for (auto y : g[x])
{
if (y != fa)
{
dfs0(y, x);
siz[x] += siz[y];
wt = max(wt, siz[y]);
}
}
wt = max(wt, n - siz[x]);
if (wt <= n / 2) v.push_back(x);
}
void dfs1(int x, int fa)
{
int tmp = 0;
if (siz[x] > n / 2) tmp = cnt[siz[x] - n / 2];
else if (n / 2 > siz[x]) tmp = cnt[n / 2 - siz[x]];
cnt[siz[x]]++;
for (auto y : g[x])
{
if (y != fa)
dfs1(y, x);
}
if (siz[x] > n / 2)
{
ll num = cnt[siz[x] - n / 2] - tmp;
ans[x] += num * (siz[x] - n / 2) * (n - siz[x]);
ans[f[x]] += num * (siz[x] - n / 2) * (n - siz[x]);
}
else if (siz[x] < n / 2) ins[x] = cnt[n / 2 - siz[x]] - tmp;
}
void dfs2(int x, int fa)
{
if (siz[x] < n / 2)
{
ll num = cnt[n / 2 - siz[x]] - ins[x] - buc[n / 2 - siz[x]] + ous[n / 2 - siz[x]];
ans[x] += num * (n / 2 - siz[x]) * siz[x];
ans[f[x]] += num * (n / 2 - siz[x]) * siz[x];
}
buc[siz[x]]++;
for (auto y : g[x])
{
if (y != fa)
{
ous[n - siz[y]]++;
dfs2(y, x);
ous[n - siz[y]]--;
}
}
buc[siz[x]]--;
}
void dfs3(int u, int fa) {
sz[u] = 1;
for (auto v : g[u]) {
if (v != fa) {
dfs3(v, u);
sz[u] += sz[v];
}
}
}
T2 线段树
题目很难读懂。
做了弱化版:E - Segment-Tree Optimization
做法:
首先可以发现一个性质:如果所有询问至多扫到某一层,那么这一层中的区间对于每个询问要么包含,要么相离。所以,对于询问 \([l,r]\),这一层必然存在一个 \(l−1→l\) 的断点和一个 \(r→r+1\) 的断点。区间 \([1,n]\) 被所有断点切割后形成的区间个数就是这一层的结点个数。既然我们知道了结点个数,由于第 \(x\) 层至多有 \(2x\) 个结点,那么我们也能够知道它在哪一层。接下来考虑最小化 \(c\)。对于这一层的所有区间,有两种决策:
- 把这个区间拉到上一层,那么就不会造成贡献。
- 把这个区间和一个相邻区间合并,然后放到上一层。
对于决策 2 的贡献,我们这样考虑:如果这两个区间是 \([l,mid)\) 和 \([mid,r]\),合并后的区间就是 \([l,r]\)。根据题意,所有包含 \([l,r]\) 或者跟 \([l,r]\) 相离的区间都不会造成贡献。那么造成贡献的就是与 \([l,r]\) 相交或者被 \([l,r]\) 包含的区间,这些区间会造成 2 的贡献。根据之前的断点可以发现一个小性质:这些造成贡献的区间要么右端点是 \(mid−1\),要么左端点是 \(mid\),且满足条件的区间必然会造成贡献。可以维护以某个下标为左端点或者右端点的区间个数来快速求出贡献。唯一的限制是上一层的区间个数不能超过 \(2d−1\)。设 \(fi,j\) 表示考虑到这一层从左往右第 \(i\) 个区间,且上一层当前有 \(j\) 个区间的贡献最小值。按照决策 DP 即可。时间复杂度 \({O(n^2+q)}\)。
10.11 Day 17
T1. 优势种类
题意:有一个序列 \(a\),每次操作可以将任意一个区间的覆盖成这个区间的绝对众数,求所有覆盖方案中的字典序最小的方案。
考场想的做法是先将所有的区间离线后排序,然后类似双指针一样逐个每一个区间会覆盖那些位置,我当时算的是 \(\mathcal{O(n)}\) 的,实际上是 \(\mathcal{O(n^2)}\) 的,就寄了。
正解:我们不难发现性质:只要在任意一个长度为 3 的区间存在绝对众数,那么一定可以传播到整个区间,那么只需要将所有可能的区间存下来,然后从后向前扫一遍求出可以到达位置 \(i\) 的最小值,处理一个后缀最小值即可。然后从前向后贪心地确每一个 \(i\) 的值, 考虑是否需要被后面的覆盖,如果一个位置 \(i\) 不会被覆盖,那么就考虑它的下一个位置,如果要被覆盖,就把区间 \(i\) 到传播的起点 \(L\),然后再往后扫直到一个 \(R\),使得 \(a_R<a_L\) 此时停止,\(i \leftarrow R\),重复之前的操作,向后扫就可以了。
时间复杂度 \(\mathcal{O(n)}\)。
const int MAXN = 200005;
int n, a[MAXN], b[MAXN], c[MAXN];
void solve()
{
n = read();
for (int i = 1; i <= n; i++) b[i] = n + 1, c[i] = n;
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1; i <= n; i++)
{
if (i + 2 > n) b[i] = n + 1;
else if (a[i] == a[i + 1] || a[i] == a[i + 2]) b[i] = a[i];
else if (a[i + 1] == a[i + 2]) b[i] = a[i + 1];
else b[i] = n + 1;
}
for (int i = n - 2; i >= 1; i--)
{
c[i] = c[i + 1];
if (b[i] == n + 1) continue;
if (b[i] <= b[c[i]]) c[i] = i;
}
for (int i = 1; i <= n; i++)
{
int j = c[std::max(1, i - 2)];
if (i > 1 && b[i - 1] <= b[j]) j = std::min(i - 1, j);
if (i > 2 && b[i - 2] <= b[j]) j = std::min(i - 2, j);
int mn = b[j];
if (j > n || mn >= a[i]) continue;
int r = j + 3;
while (r <= n && mn <= a[r]) r++;
for (int x = i; x < r; x++) a[x] = mn;
i = r;
}
for (int i = 1; i <= n; i++) printf("%d ", a[i]);
puts("");
}
10.14 Day 18
T1. Easy Counting Problem
题意:有一个字符串和四种变换,求可以通过这四种操作得到多少种字符串。
- ABC→CBA
- CBA→ABC
- BCD→DCB
- DCB→BCD
NaN 考场上发现好像是个不可做的组合数学,没有思考。
正解:发现只有 AC 和 BD 会换位,将 AC 归为一类,将 BD 归为一类,对于相邻的两个位置,若它们属于同一类,则这两个位置两侧的字符不会相互影响,此时可将序列从这两个位置之间切开,将这个序列变成若干段,最终的答案就是所有切开后形成的各段的方案数的乘积。在每一段内部,不妨假设奇数下标位置属于 A、C 类,偶数下标位置属于 B、D 类。由于 A 和 D 无法直接交换,它们在序列中的相对顺序是固定不变的。因此,每一段的方案数可通过如下方式计算:在所有奇数下标位置中选择若干个放置 A(其余放 C),同时在所有偶数下标位置中选择若干个放置 D(其余放 B),且需保证 A 和 D 的相对顺序不变。这种情况下,方案数等于 $\tbinom{len - a - b}{b} $,其中 \(len\) 是这一段的长度,a 是 B、C 的数量,b 是 \(BC\) 或 \(CB\) 的个数。
整个过程的时间复杂度为 \(\mathcal{O(n)}\)。
代码:
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
init();
cin >> T;
while (T --) {
cin >> s; n = s.size();
s = " " + s;
int idx = 0;
for (int i = 1; i <= n; i ++) {
if (i == n || (~(s[i + 1] ^ s[i]) & 1)) {
cut[++ idx] = i;
}
}
ll ans = 1;
for (int i = 1; i <= idx; i ++) {
int l = cut[i - 1] + 1, r = cut[i], a = 0, b = 0;
for (int j = l; j <= r; j ++) if (s[j] == 'C' || s[j] == 'B') a ++;
for (int j = l; j < r; j ++) {
if (s[j] == 'B' && s[j + 1] == 'C' || s[j] == 'C' && s[j + 1] == 'B') b ++, j ++;
}
if (a) ans = (ans * C(r - l + 1 - a + b, b) % MOD) % MOD;
}
cout << ans << '\n';
}
return 0;
T2. 构造合并
感觉合并策略比较好猜,考场上猜到了,但是死在了细节上。
正解:发现答案只能是 1 或 2,当 \(n\) 是二的幂次时答案是 1,否则是 0。
答案为 1 非常简单,答案为二的合并策略就是现能和就和,把这些数变成若干二的幂次,然后拿出最大的,使最小的不断乘二,直到与次小的数相等,然后合并他们,重复直到只剩两个数。
用 set 维护最大值最小值和次小值即可。
有一点细节,在考虑和次小值合并后,发现能合要一直合并下去。
代码:
int n, a[N];
vector<pair<int, int>> ans;
vector<int> cnt[N];
int main() {
n = read();
F(i, 1 , n) a[i] = 1;
F(i, 1, n) {
if (cnt[a[i]].size()) {
while (cnt[a[i]].size()) {
int j = cnt[a[i]].back();
cnt[a[i]].pop_back();
ans.push_back(make_pair(a[i], a[j]));
a[j] = 0, a[i] *= 2;
if (a[i] == 0) break;
}
cnt[a[i]].push_back(i);
}
else cnt[a[i]].push_back(i);
}
int k = log2(n);
if ((1 << k) != n) {
set<int> s;
for (int i = 1; i <= n; i ++) if (a[i] != 0) s.insert(a[i]);
while (s.size() > 2) {
int x = *s.begin(), y = *(-- s.end()), z;
s.erase(*s.begin()); s.erase(y);
z = *s.begin();
s.erase(s.begin());
while (x < y && x != z) {
ans.push_back(make_pair(x, y));
y -= x, x *= 2;
}
s.insert(y);
if (x == z) {
ans.push_back(make_pair(x, z));
x *= 2;
while (s.find(x) != s.end()) {
ans.push_back(make_pair(x, x));
s.erase(x);
x *= 2;
}
s.insert(x);
continue;
}
}
}
cout << ans.size() << '\n';
for (auto [x, y] : ans) cout << x << " " << y << '\n';
return 0;
}
10.16 Day 19
T1. 排序大师
题意:给定一个序列和两个操作,每次可以将任意元素移动至队列的队首或队尾,求至少用多少次操作能将序列变成有序的。
考场上想了非常久的整体贪心,没有从局部入手,只拿了特殊性质是个排列的 40 分。
正解:
const int N = 3e5;
int n, a[N + 10], b[N + 10];
int lst[N + 10], l[N + 10], r[N + 10];
int f[N + 10][4];
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
b[i] = a[i];
}
sort(b + 1, b + n + 1);
for (int i = 1; i <= n; i++) l[i] = r[i] = 0, f[i][0] = f[i][1] = f[i][2] = 0;
int m = unique(b + 1, b + n + 1) - (b + 1);
unordered_map<int, int> cnt;
for (int i = 1; i <= n; i++) {
a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
r[a[i]] = i;
if (!l[a[i]]) l[a[i]] = i, lst[a[i]] = i;
cnt[a[i]]++;
}
int ans = 1;
for (int i = 1; i <= n; i++) {
f[i][0] = f[lst[a[i]]][0] + 1;
f[i][1] = max(f[lst[a[i]]][1], max(f[lst[a[i] - 1]][0], f[lst[a[i] - 1]][2])) + 1;
if (i == r[a[i]]) {
f[i][2] = f[l[a[i]]][1] + cnt[a[i]] - 1;
}
lst[a[i]] = i;
ans = max(max(ans, f[i][2]), max(f[i][0], f[i][1]));
}
cout << n - ans << '\n';
}

浙公网安备 33010602011771号