2025/11/17~2025/11/23 做题笔记
2025/11/17
C. 区间
发现修改操作是 + 1,考虑如何维护这个比较简单的修改。可以想到他会对一些区间的贡献 + 1,使得 \(k\) 的答案也增加。不难发现对于会产生影响的 \(k\),它产生的贡献先上升,然后不变,最后再下降。直接用线段树维护即可。
2025/11/18
A. 某道题的checker
纯飞舞吧,这题都不会。显然对于一个合法的 \(x\) 使得 \(a_i < a_{i + 1}\),当且仅当需要的那一位 \(x\) 为 \(0\),并且前面的不同的位 \(x\) 都为 \(1\),那么剩下的没有限制的地方 \(x\) 随便填,所以高位前缀和对所有 \(x\) 算一下他能让多少 \(a_i < a_{i + 1}\) 满足。
主要是想到可以知道满足条件的 \(x\) 都是在一个最小的合法 \(x\) 上增加很多的 \(1\)
而我一直在想满足条件的 \(x\) 会是很多区间,但是当有很多相同的位的时候这个是不可维护的。之前一直不知道高为前缀和是个什么东西,现在了解了。
Code
#include <iostream>
#include <vector>
using namespace std;
using pii = pair<int, int>;
const int kN = 5e6 + 1;
int n, m, f[kN], ans[kN];
void Insert(int x, int y) {
for (int i = m, s = 0; i >= 0; i--) {
int o = x >> i & 1, _o = y >> i & 1;
if (o == _o)
continue;
if (!o)
f[s]++, f[s | (1 << i)]--;
s |= 1 << i;
}
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 1, x, lst = -1; i <= n; i++) {
cin >> x;
if (lst != -1)
Insert(lst, x);
lst = x;
}
for (int j = 0; j < m; j++) {
for (int i = 1; i < 1 << m; i++) {
if (i & (1 << j))
f[i] += f[i ^ (1 << j)];
}
}
for (int i = 0; i < 1 << m; i++)
ans[f[i]]++;
for (int i = 0; i < n; i++)
cout << ans[i] << ' ';
return 0;
}
B. dfsize
这 dp 设计和转移纯巧妙吧。首先对于一个位置 \(i\),之后的 \(a_i\) 个位置都是 \(i\) 的子树。显然有 \(n^3\) 的区间 dp,设 \(f_{l, r}\) 表示 \([l, r]\) 中最少需要的改变次数。转移直接枚举位置把两边拼起来即可。考虑如何优化。首先可以用 \(f_{l, r}\) 表示区间 \([l, r]\) 变成森林的最少改变次数,这样新加一个最左边的点的时候变成树的代价就为 \([a_{l - 1} == r - l] + f_{l, r}\)。
考虑如何转移。把整个区间变成一棵树是简单的,直接把最左边的点的值改为整个区间的大小即可,即 \(f_{l, r} = [a_l != r - l + 1] + f_{l + 1, r}\)。如果要把区间改成森林,发现最左边的点仍然是一颗树的根,完全可以把别的树的点也接到最左边的点上。只有一种情况不优,那就是最左边的点的值不用变就是一棵树的根,这种情况下是好转移的,即 \(f_{l, r} = f_{l, l + a_l - 1} + f_{l + a_l, r}\)。
但是按照一般的枚举区间长度转移会很慢,因为访问的数组内存不是连续的。发现倒着枚举即可解决这个问题。
这道题的想法简直是纯天才。
Code
#include <iostream>
using namespace std;
using ll = long long;
const int kN = 5e3 + 2;
int T, n, a[kN];
ll ans, f[kN][kN];
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--; ans = 0) {
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
auto Calc = [](int l, int r) { return f[l + 1][r] + (a[l] != r - l + 1); };
for (int l = n; l >= 1; l--) {
for (int r = l; r <= n; r++) {
int len = r - l + 1;
f[l][r] = 1e9;
if (a[l] <= len)
f[l][r] = f[l][l + a[l] - 1] + f[l + a[l]][r];
f[l][r] = min(f[l][r], Calc(l, r));
}
}
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++)
ans += a[i] ^ a[j] ^ Calc(i, j);
cout << ans << '\n';
}
return 0;
}
C. 构造倒水
设 \(A\) 表示 \(a\) 中不是空瓶的数量, \(B\) 表示 \(b\) 中不是空瓶的数量
神仙题
首先先从 \(b = 1\) 的构造想。可以先蒙一个全部塞到 \(n\) 再一个一个往前倒的方案。再考虑有没有更优的方法。思考这个方案会在什么情况下比较劣,发现只取决于最初有多少个地方是有值的,所以思考当这个数大于 \(\dfrac{n}{3}\) 的时候怎么做。所以需要 \(n - A\) 这种东西。发现将每个有值的地方倒到 1 再倒到其它地方恰好是这 \(2(n - A)\),可以通过 \(b = 1\)。
剩下的部分根本想不到一点。大致是说考虑全变成 1 后再满足目标是简单的,这两种方法都会多一个 \(n - B\) 的额外操作。但是这样不足以解决所有询问。还有一种可行的操作方式是先全部倒倒 \(n\),再倒到 \(b_i\) 的位置上再倒到 \(i\),这样的操作数是 \(A + 2B\),可以注意到对于一种序列,与前面两种方案最劣情况加起来为 \(5n\),也就是说他们三个的平均数恰为 \(\dfrac{5}{3}n\),这样同时应用三种方案即可得到至少一种方案满足条件。
Code
#include <cassert>
#include <iostream>
#include <vector>
using namespace std;
using pii = pair<int, int>;
const int kN = 5e5 + 1;
int T, n, k, a[kN], b[kN], A[kN], B[kN];
vector<pii> ans, res;
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
cin >> n >> k;
for (int i = 1; i <= n; i++)
cin >> A[i];
for (int i = 1; i <= n; i++)
cin >> B[i];
auto Fuck = []() {
fill(a + 1, a + n + 1, 1);
for (int i = 1, j = 1; i <= n; i++) {
if (b[i])
continue;
for (; j <= n && a[j] >= b[j]; j++);
// cerr << i << ' ' << j << '\n';
res.push_back({i, j}), a[j]++;
}
};
copy(A + 1, A + n + 1, a + 1), copy(B + 1, B + n + 1, b + 1), res.clear();
for (int i = 1; i < n; i++)
if (a[i])
res.push_back({i, n});
for (int i = n; i > 1; i--)
res.push_back({i, i - 1});
Fuck();
swap(ans, res);
copy(A + 1, A + n + 1, a + 1), copy(B + 1, B + n + 1, b + 1), res.clear();
for (int i = 2, j = 1; i <= n; i++) {
if (a[i])
continue;
if (a[1] >= 1) {
res.push_back({1, i}), a[1]--, a[i]++;
continue;
}
for (; a[j] <= 1 && j <= n; j++);
if (j != 1)
res.push_back({j, 1});
res.push_back({1, i});
a[j]--, a[i]++;
}
for (int i = 2; i <= n; i++)
if (a[i] > 1)
res.push_back({i, 1}), a[i]--, a[1]++;
for (int i = 1; i <= n; i++)
assert(a[i] == 1);
Fuck();
(res.size() < ans.size()) && (swap(ans, res), 0);
copy(A + 1, A + n + 1, a + 1), copy(B + 1, B + n + 1, b + 1), res.clear();
for (int i = 1; i < n; i++)
if (a[i])
res.push_back({i, n});
for (int i = n - 1; i >= 1; i--) {
if (b[i]) {
res.push_back({n, b[i]});
if (b[i] != i)
res.push_back({b[i], i});
}
}
(res.size() < ans.size()) && (swap(ans, res), 0);
cout << ans.size() << '\n';
for (auto [x, y] : ans)
cout << x << ' ' << y << '\n', assert(x != y);
}
return 0;
}
2025/11/21
A. 探测
其实感觉这道题思维难度不高,但是我写代码的时候思维很乱,导致我 T1 写了十万年,差一点把正解交上去
可以发现如果特殊点不在 \(x\) 的子树内时,\(x\) 子树内的限制全部移到 \(x\) 后一定是相同的,那么可以借此往父亲转移,否则直接标记答案在此子树内。如果没有限制不同的点,那就随便走都行。如果直接把根设为一个限制的话会省去很多麻烦
Code
#include <algorithm>
#include <iostream>
#include <map>
#include <vector>
using namespace std;
using pii = pair<int, int>;
const int kN = 5e5 + 1, kI = 2e9;
vector<int> ans;
vector<pii> e[kN];
map<int, int> li[kN];
int T, n, rt, k, a[kN];
int Push(int x, int fa) {
int mn = kI, mx = -kI;
if (a[x] != -kI)
mn = mx = a[x];
for (auto [i, w] : e[x]) {
if (i == fa)
continue;
li[x][i] = Push(i, x);
if (li[x][i] != -kI)
li[x][i] -= w, mn = min(mn, li[x][i]), mx = max(mx, li[x][i]);
}
if ((mn != mx && mn != kI && mx != -kI) || (mn != a[x] && a[x] != -kI) || mx > 1e9 || mn <= 0)
return kI;
if (mn != kI)
a[x] = mn;
return a[x];
}
void Find(int x, int fa, int dist) {
// cerr << x << ' ' << fa << ' ' << dist << '\n';
if (dist == 0)
return ans.push_back(x);
for (auto [i, w] : e[x])
if (i != fa && li[x][i] > 1e9)
return Find(i, x, dist - w);
map<int, int> mp;
if (a[x] != -kI)
mp[a[x]]++;
for (auto [i, w] : li[x])
if (w != -kI)
mp[w]++;
if (mp.size() == 0 || mp.begin()->first == dist) {
for (auto [i, w] : e[x])
if (i != fa && li[x][i] == -kI)
Find(i, x, dist - w);
} else if (mp.size() == 1) {
for (auto [i, w] : e[x])
if (i != fa && li[x][i] != -kI)
Find(i, x, dist - w);
} else {
for (auto [i, w] : e[x])
if (i != fa && li[x][i] == mp.begin()->first)
Find(i, x, dist - w);
}
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
cin >> n >> k;
fill(a + 1, a + n + 1, -kI);
for (int i = 1, x, y, z; i < n; i++) {
cin >> x >> y >> z, e[x].push_back({y, z}), e[y].push_back({x, z});
}
if (k == 0) {
cout << n << '\n';
for (int i = 1; i <= n; i++)
cout << i << ' ';
continue;
}
for (int i = 1, x, y; i <= k; i++)
cin >> x >> y, a[rt = x] = y;
Push(rt, 0);
Find(rt, 0, a[rt]);
cout << ans.size() << '\n';
sort(ans.begin(), ans.end());
for (int i : ans)
cout << i << ' ';
cout << '\n';
for (int i = 1; i <= n; i++)
li[i].clear(), e[i].clear(), ans.clear();
}
return 0;
}
C. 有向稀疏图上更快的三元环计数
之前没改这道题是感觉太难了,根本想不明白。虽然现在还是感觉很难
有一个小 trick 是对于每一个异色三角形一定有两个异色角,这样限制只有两条边而不必像求同色三角形一样限制为三条边。需要注意到对于一对相交的边 \(i, j\),能选择的第三条边只有 \(\min(i, j)\) 个,统计答案的时候对于 \(i < j\) 和 \(i >= j\) 分别计算贡献即可。
Code
#include <iostream>
using namespace std;
const int kN = 5e6 + 2, kM = 998244353;
int n, ans, a[3][kN], f[3][kN][2], s[3][kN][2];
char ch;
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int o : {0, 1, 2}) {
for (int i = 1; i <= n; i++) {
cin >> ch, a[o][i] = ch - '0';
for (int t : {0, 1})
s[o][i][t] = s[o][i - 1][t] + (a[o][i] == t), f[o][i][t] = (f[o][i - 1][t] + (a[o][i] == t) * i) % kM;
}
}
for (int o = 0; o < 2; o++) {
for (int _o = o + 1; _o < 3; _o++) {
for (int i = 1; i <= n; i++) {
int t = !a[_o][i];
ans = (ans + 1ll * (s[o][n][t] - s[o][max(i, n - i - 1)][t]) * i) % kM;
if (i >= n - i)
ans = (ans + f[o][i][t] - f[o][max(0, n - i - 1)][t]) % kM;
}
}
}
ans = 1ll * -ans * (kM + 1) / 2 % kM + kM;
for (int i = 1; i <= n; i++)
ans = (ans + 1ll * i * (i + 1) / 2 % kM) % kM;
for (int i = 0; i <= n; i += 2)
ans = (ans + 1ll * (n - i) * (n - i - 1) / 2 % kM) % kM;
cout << ans << '\n';
return 0;
}
2025/11/22
D. 序列求交
观察部分分发现 \(q = 0\) 的占比很大,思考没有修改怎么做。可以发现对于每一个在 \(A\) 中的元素能产生贡献的区间左端点范围是 \([max(1, i - k + 1), min(n - k + 1, i)]\),同理,在 \(B\) 序列中能产生贡献的也是这个东西。发现每一个 \(v\) 元素能产生贡献的点对构成一个矩形,那么对于这个矩形内部全部 + 1,扫描线一下即可知道答案。
对于有修改的部分,发现每个修改即为平移两个矩形,所以只会对四个 \(1 \times len\) 的矩形修改,类似可持久化线段树的区间修改操作做即可。这个东西我一开始还不会,只能说 SP11470 TTM - To the moon 全忘了
Code
#include <iostream>
#include <vector>
using namespace std;
using pii = pair<int, int>;
using ll = long long;
const int kN = 1e5 + 2;
int n, k, q, cnt, a[kN], rt[kN];
pii b[kN], c[kN];
struct Info {
ll mx, cnt;
Info operator+(const Info& x) const {
int _mx = max(mx, x.mx);
return {_mx, (_mx == mx) * cnt + (_mx == x.mx) * x.cnt};
}
};
namespace Seg {
Info tr[kN * 4];
void Update(int p, Info v, int x = 1, int l = 1, int r = n) {
if (l == r)
return tr[x] = v, void();
int m = (l + r) / 2;
if (p <= m)
Update(p, v, x * 2, l, m);
else
Update(p, v, x * 2 + 1, m + 1, r);
tr[x] = tr[x * 2] + tr[x * 2 + 1];
}
} // namespace Seg
struct Line {
int l, r, v;
};
vector<Line> li[kN];
struct Tr {
Info v;
int l, r, tag;
} tr[kN * 300];
void Build(int x, int l, int r) {
tr[x].v.cnt = r - l + 1;
if (l == r)
return;
int m = (l + r) / 2;
Build(tr[x].l = ++cnt, l, m), Build(tr[x].r = ++cnt, m + 1, r);
}
void Add(int y, int x, int nl, int nr, int v, int l = 1, int r = n) {
tr[x] = tr[y];
if (nl <= l && r <= nr)
return tr[x].v.mx += v, tr[x].tag += v, void();
int m = (l + r) / 2, L = tr[y].l, R = tr[y].r;
if (nl <= m)
Add(L, tr[x].l = ++cnt, nl, nr, v, l, m);
if (nr > m)
Add(R, tr[x].r = ++cnt, nl, nr, v, m + 1, r);
tr[x].v = tr[tr[x].l].v + tr[tr[x].r].v, tr[x].v.mx += tr[x].tag;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> k >> q;
for (int i = 1; i <= n; i++)
cin >> a[i], c[a[i]] = {max(i - k + 1, 1), min(i, n - k + 1)};
for (int i = 1, x; i <= n; i++) {
cin >> x, b[x] = {max(i - k + 1, 1), min(i, n - k + 1)};
li[c[x].first].push_back({b[x].first, b[x].second, 1}), li[c[x].second + 1].push_back({b[x].first, b[x].second, -1});
}
Build(rt[0], 1, n);
auto Update = [](int p, int l, int r, int v, int tmp = 0) { tmp = rt[p], Add(tmp, rt[p] = ++cnt, l, r, v); };
for (int i = 1; i <= n; i++) {
Add(rt[i - 1], rt[i] = ++cnt, 1, n, 0);
for (auto [l, r, v] : li[i])
Update(i, l, r, v);
Seg::Update(i, tr[rt[i]].v);
}
cout << Seg::tr[1].mx << ' ' << Seg::tr[1].cnt << '\n';
for (int p; q--;) {
cin >> p;
Update(c[a[p]].first, b[a[p]].first, b[a[p]].second, -1), Update(c[a[p + 1]].second, b[a[p + 1]].first, b[a[p + 1]].second, -1);
swap(a[p], a[p + 1]), swap(c[a[p]], c[a[p + 1]]);
Update(c[a[p]].first, b[a[p]].first, b[a[p]].second, 1), Update(c[a[p + 1]].second, b[a[p + 1]].first, b[a[p + 1]].second, 1);
if (c[a[p]].second == n - k + 1)
Update(n - k + 1, b[a[p]].first, b[a[p]].second, 1), Update(n - k + 1, b[a[p + 1]].first, b[a[p + 1]].second, -1);
if (c[a[p + 1]].first == 1)
Update(1, b[a[p + 1]].first, b[a[p + 1]].second, 1), Update(1, b[a[p]].first, b[a[p]].second, -1);
Seg::Update(c[a[p]].first, tr[rt[c[a[p]].first]].v), Seg::Update(c[a[p + 1]].second, tr[rt[c[a[p + 1]].second]].v);
cout << Seg::tr[1].mx << ' ' << Seg::tr[1].cnt << '\n';
}
return 0;
}
A. 天和地
乍一看有点唬人,但是仔细想想就会发现最后的图会形成一个内向基环树,而其它不能被 \(1\) 访问的点指向哪里都无所谓,直接枚举 \(1\) 能到多少点算一下即可
Code
#include <iostream>
using namespace std;
const int kM = 998244353;
int n, ans;
int Pow(int x, int y) {
int res = 1;
for (; y; y >>= 1, x = 1ll * x * x % kM)
(y & 1) && (res = 1ll * res * x % kM);
return res;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n, ans = Pow(n, n - 1);
for (int i = 2, base = n - 1; i <= n; i++) {
ans = (ans + 1ll * i * i % kM * base % kM * Pow(n, n - i) % kM) % kM;
base = 1ll * base * (n - i) % kM;
}
cout << 1ll * ans * Pow(Pow(n, n), kM - 2) % kM;
return 0;
}
C. 椅子与篮筐
感觉如果放 \(O(n\log n)\) 过的话比 T2 简单。
看到这题首先可以想到一个二分答案,但是看到数据范围直接开摆了。但是正解也是从 \(n \log n\) 优化的,实在是有点唐。
二分答案是显然可以的,考虑 \(Check\) 如何做。很明显不能让他走到任意一个 \(a_x \geq v\) 的位置,考虑 \(f_{x}\) 表示进入这颗子树前必须要做多少次操作才能不猜到这样的位置。显然状态转移方程即为 \(f_x = max(\sum\limits_{y \in son}f_y - 1, 0) + [a_x \geq v]\),这样常熟可以写的很小。我把他给的快读加上后程序快了一倍
Code
#include <iostream>
using namespace std;
const int kN = 2e7 + 1;
namespace io {
char inputbuf[1 << 23], *p1 = inputbuf, *p2 = inputbuf;
#define getchar() (p1 == p2 && (p2 = (p1 = inputbuf) + fread(inputbuf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
inline int read() {
int ret = 0;
char ch = getchar();
bool f = true;
for (; ch < '0' || ch > '9'; ch = getchar())
if (ch == '-')
f = false;
for (; ch >= '0' && ch <= '9'; ch = getchar())
ret = ret * 10 + (ch ^ 48);
return f ? ret : -ret;
}
} // namespace io
int n, l, r, a[kN], f[kN], fa[kN];
bool Check(int t) {
fill(f + 1, f + n + 1, 0);
for (int i = n; i >= 1; i--) {
f[i] = max(f[i] - 1, 0) + (a[i] >= t);
f[fa[i]] += f[i];
}
return f[1];
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
n = io::read(), l = 0, r = n - 1;
for (int i = 2; i <= n; i++)
fa[i] = io::read();
for (int i = 2; i <= n; i++)
a[i] = io::read();
for (int m = (l + r + 1) / 2; l < r; m = (l + r + 1) / 2) {
if (Check(m))
l = m;
else
r = m - 1;
}
cout << l;
return 0;
}

浙公网安备 33010602011771号