做题记录 #5
A. CF2135E1 Beyond the Palindrome (Easy Version) (7.5)
2025.11.10
容斥好题。首先发现这个消除 10 子串类似一个括号序列,最后只会剩下 前缀一堆 \(0\) 和 后缀一堆 \(1\)。经典 trick 转 \(\pm 1\) 前缀和折线,令 \(0\) 时 \(-1\)、\(1\) 时 \(+1\),那么不难发现前面 \(0\) 的个数是 \(-\min pre\) 个。反转后的前缀和折线,可以发现刚好是原折线绕 \((n,pre_n)\) 旋转 \(180^\circ\),因此不难得到 \(\max pre - pre_n = -\min pre\),即 \(\max pre + \min pre = sum\)。非常好的转化。
那么接下来怎么做呢?可以枚举这个折线的值域,即 \(\max=u\) 和 \(\min=d\)。此时非常想要反射容斥,但是这个条件是“恰好”的,不好处理,考虑容斥成 \((d, u) - (d + 1, u) - (d, u - 1) + (d + 1, u - 1)\),就可以改成 \(\max \leq u, \min \geq d\) 了。
于是考虑反射容斥。与 Catalan 的反射容斥不太相同,它有两条线卡着。但是反思反射容斥的过程,我做一次反射可以容斥完所有的只触碰当前 \(y=i\) 这个限制直线 的方案,所以连续的触碰同一个限制是无需考虑的,我只需要考虑交替的触碰。当然,会有两种交替的方式(向上或向下)。由于 \(x=n\) 的情况下无法走到 \(|y| > n\) 的点位,而且每两次反射都会增长 \(2(u-d)\) 的纵坐标绝对值,因此做一个 \((d,u)\) 的复杂度是 \(\mathcal{O}(\frac{n}{u-d})\) 的。暴力枚举 \(n^2\ln n\)。
发现我有很多的 \(u-d\) 相同,他们考虑的反射其实是类似的,我们可以直接把它连起点平移到 \((0, u-d)\) 的限制中。此时,起点是 \((0, 0\sim u-d)\),对应的终点是 \((n, u-d\sim 0)\)。不难发现我做的翻折不会给点横坐标带上系数,因此每个起点对应的终点横坐标一定也是段区间。此时存在两种情况,一种差值全相等,组合数直接乘起点数即可;另一种差值是个公差为 \(2\) 的等差数列,考虑到 \((0, 0)\rightarrow (n, x)\) 的方案数其实是 \(n\choose (n+x)/2\),因此是个组合数区间和,总数都是 \(n\),预处理一下前缀和即可。时间复杂度 \(\mathcal{O}(n\ln n)\)。
代码细节挺多,因为会有上面容斥的 减去项,需要深思熟虑。今天状态有点差,想不明白这个 E2,看题解也没有很懂,但是感觉是特殊化的推式子,感觉不太 educational,故没有补/hsh
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e6 + 1, kP = 998244353;
int T, n;
int inv[kN], a[kN];
LL sum[kN];
LL Get(int l, int r) {
l > r && (swap(l, r), 0);
return sum[min(r, n)] - (l > 0 ? sum[l - 1] : 0);
}
int Func(int d, bool o) {
LL ret = 0, len = d + 2 - o;
for (int k = 0, l; (l = (n + d - o) / 2 - k * len) >= 0; k++) //* UD
ret += Get((n - d) / 2 - k * len, l);
for (int k = 1, r; (r = (n - d) / 2 + k * len) <= n; k++) //* DU
ret += Get(r, (n + d) / 2 - o + k * len);
for (int k = 0, p; (p = (n - d) / 2 - 1 - k * len) >= 0; k++) //* D
ret -= (d + 1ll - o) * a[p] % kP;
for (int k = 1, p; (p = (n - d) / 2 - 1 + k * len) <= n; k++) //* U
ret -= (d + 1ll - o) * a[p] % kP;
return (ret % kP + kP) % kP;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
inv[1] = 1;
for (int i = 2; i < kN; i++)
inv[i] = 1ll * (kP - kP / i) * inv[kP % i] % kP;
for (cin >> T; T--;) {
cin >> n, sum[0] = a[0] = 1;
for (int i = 1; i <= n; i++) {
a[i] = 1ll * a[i - 1] * inv[i] % kP * (n - i + 1) % kP;
sum[i] = (sum[i - 1] + a[i]) % kP;
}
LL ans = 0;
for (int d = 2 - (n % 2), lst = 0; d <= n; d += 2) {
ans += lst;
ans += (lst = Func(d, 0));
ans -= 2 * Func(d, 1);
}
cout << (ans % kP + kP) % kP << '\n';
for (int i = 0; i <= n; i++)
sum[i] = a[i] = 0;
}
return 0;
}
B. P5307 Mobitel (3.5)
2025.11.10
很有意思的题目。如果没有想歪,不难想到一个 \(\mathcal{O}(rsn)\) 的暴力 DP 转移,即 \(f_{i,j,w}\) 表示当前走到 \((i,j)\),路径数乘积为 \(w\) 的方案数,转移是非常简单的。考虑优化,发现很多的 \(w\) 其实是等价的,他们之间值的微小差距并不影响乘到 \(n\) 所需的倍数。那么就可以改变状态,改成走到 \((i,j)\) 还要乘 \(w\) 才能达到 \(n\) 的方案数。此时不难发现有效的 \(w\) 只有 \(\mathcal{O}(\sqrt{n})\) 个,稍微改一下,就可以简单做到 \(\mathcal{O}(rs\sqrt{n})\)。
这年头紫题这个难度吗,有意思。
Code
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 300 + 1, kV = 1e6 + 1, kC = 2e3 + 1, kP = 1e9 + 7;
void Add(int &x, int w) { x = (x + w) % kP; }
int r, s, n, a[kN][kN];
int rem[kV], id[kV], tot, val[kC];
int f[2][kN][kC];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> r >> s >> n;
for (int i = 1; i <= r; i++) {
for (int j = 1; j <= s; j++)
cin >> a[i][j];
}
for (int i = 1; i <= n; i++) {
rem[i] = (n - 1) / i + 1;
if (rem[i] != rem[i - 1])
id[rem[i]] = ++tot, val[tot] = rem[i];
}
bool o = 1;
f[o][1][id[(n - 1) / a[1][1] + 1]] = 1;
for (int i = 1; i <= r; i++) {
o ^= 1;
fill(f[o][0], f[o + 1][0], 0);
for (int j = 1; j <= s; j++)
for (int k = 1; k <= tot; k++) {
int nxt = id[(val[k] - 1) / a[i][j] + 1];
Add(f[o][j][nxt], f[!o][j][k]);
Add(f[o][j][nxt], f[o][j - 1][k]);
}
}
cout << f[o][s][tot] << '\n';
return 0;
}
C. CF1827D Two Centroids (4.5)
2025.11.10
双中心很明显是指,在无根条件下,存在两个大小为 \(n/2\) 的子树。考虑操作次数,其实就是求 \(\min |n-2sz_i|\),即最接近 \(n/2\) 的 \(sz_i\)。不难发现这个 \(i\) 一定挂在重心的某个子树上,或者正好是重心所在子树。否则一定可以往重心走获得一个更靠近 \(n/2\) 的子树。因此维护当前的重心,维护当前每个子树的大小,拍到 dfn 序上,重剖做到根链加,查询只看 \((dfn_x, R_x]\) 最大 \(sz_y\) 以及 \(sz_x\) 即可。更改中心只会在 \(y\) 和 \(fa_x\) 中做选择。复杂度 \(\mathcal{O}(n\log^2 n)\)。
\(n=5\times 10^5\),看上去很悬。考虑优化:发现若才更换重心 \(x\),则最优的 \(sz\) 一定是 \(n-sz_x\);若还有插新的叶子,那么一定只有插过叶子的子树根才有可能作为新的重心。于是考虑倍增跳定位,维护当前可能作为下一个重心的点位,只需要再维护当前每个子树的大小即可。因为有新加入的操作,所以还需要维护每个子树当前的大小,不过不需要每个 \(i\) 的位置都直接存储 \(sz_i\),因此树状数组维护即可。复杂度 \(\mathcal{O}(n\log n)\)。
难度不高,但是还是非常需要优化智慧的/qiang
Code
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e5 + 1;
int T, n;
vector<int> e[kN];
int fa[kN][20], dep[kN], dfn[kN], R[kN], dfc;
void Dfs(int x, int Fa) {
dfn[x] = ++dfc;
dep[x] = dep[Fa] + 1;
for (int d = 1; d < 20; d++)
fa[x][d] = fa[fa[x][d - 1]][d - 1];
for (auto v : e[x])
Dfs(v, x);
R[x] = dfc;
}
int Jump(int x, int l) {
for (int d = 0; d < 20; d++)
(l >> d & 1) && (x = fa[x][d]);
return x;
}
int sz[kN], t[kN];
bool vis[kN];
void Add(int x, int w) {
for (; x <= n; x += x & -x)
t[x] += w;
}
int Ask(int x) {
int ret = 0;
for (; x > 0; x -= x & -x)
ret += t[x];
return ret;
}
int Query(int x) { return Ask(R[x]) - Ask(dfn[x] - 1); }
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
cin >> n;
for (int i = 2, x; i <= n; i++) {
cin >> x, fa[i][0] = x;
e[x].push_back(i);
}
Dfs(1, 0);
int ctr = 1, mx = 0;
vector<int> V;
for (int i = 2; i <= n; i++) {
bool in = dfn[ctr] <= dfn[i] && dfn[i] <= R[ctr];
int x = in ? Jump(i, dep[i] - dep[ctr] - 1) : fa[ctr][0];
if (!vis[x]) {
vis[x] = 1, V.push_back(x);
sz[x] = in ? Query(x) : (i - 1 - Query(ctr));
}
mx = max(mx, ++sz[x]);
Add(dfn[i], 1);
if (2 * mx > i) {
// cerr << i << ' ' << ctr << ' '<< mx << '\n';
for (auto u : V)
sz[u] = vis[u] = 0;
V = {ctr}, vis[ctr] = 1;
mx = sz[ctr] = i - mx, ctr = x;
}
cout << i - 2 * mx << ' ';
}
cout << '\n';
for (auto u : V)
sz[u] = vis[u] = 0;
V.clear(), dfc = 0;
for (int i = 1; i <= n; i++)
e[i].clear(), sz[i] = t[i] = 0;
}
return 0;
}
NOIP Day4C. 构造 LIS (5)
2025.11.11
考场上最有想法的一道题。
首先反思如何构造出很多的 LIS,发现直接提前分层,每层设置一个递减的区间,区间之间整体递增,这样 LIS 可以在每层中任意选择,方案数为每层个数乘积。虽然 \(290\) 长度以内最大方案数已经可以轻松超过 \(10^{35}\),反思一下此类构造的确是最优的。反思求 LIS 的过程,其实也是给每个位置上的数分配一个层,那么放在一起很明显可以让方案数尽量大。因此这大概率是做法的必然前缀。
考虑构造精确的 \(x\),经典的思路是考虑进制表示。发现可以在第 \(i\) 层的最开头放置一个极大值,即可获取一个 \(k^i\),排一小段即可成乘一个系数。但是我需要他成为 LIS 的一部分,因此还需要向上延申很长一条,每次都延申那么长一段估计是拿不到分的。反思一下,发现我可以把新增的“极大值”互相利用,每层都放一个极大值点,把 LIS 设为 层数+1,最后一层可以把阶梯本身全部删掉,排除层阶梯本身的影响。此时取 base = 3 可以做到 350 左右。赛时止步于此,但是对于 LIS 的细节没有想清楚,因此没有写,赛时豪取 0 分。考虑到赛时只用了 40min 想到此处,其实是潜在前途不小的。
继续反思,发现我并不一定每一层都要把极大值点放在最开头。如果能放在阶梯中间,那么还可以用到阶梯的一个前缀。这样不需要自己插数来给位填系数了,每一层只需要塞一个极大值。与上一段类似,最后一个阶梯可以只保留极大值对应的前缀。并且其实这个极大值不需要保留,因为它使得 \(p_n=n\),删去并不影响任何东西,形象地看就是把 LIS 改为了 \(n\),删去了最后一步汇总。此时因为位权大小不影响答案长度,因此可以取 base = 4,比 3 更优,直接做到 291 的答案,获取 90 分。
然后发现其实整进制会很亏。因为 \(4^{\lceil\log_4 10^{35}\rceil}=3.3\times 10^{35}\),有三倍的空闲空间!考虑混进制,发现 \(3.3 \times 5 / (4^2) \approx 1.03 > 1\),因此删去两个 4 进制位,加入一个 5 进制位即可。方便实现,放在最后。至此,做到 289,代码及其好写。
Code
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
int T;
__int128 x;
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
char ch = '.';
for (; ch < '0' || ch > '9'; ch = cin.get());
for (; ch >= '0' && ch <= '9'; ch = cin.get())
x = 10 * x + ch - '0';
int tot = 0, tag = 0;
vector<int> ans, pos;
for (; x > 4; x /= 4) {
int r = x % 4;
tot += 4;
if (!tag && !r) {
for (int i = 0; i < 4; i++)
ans.push_back(tot - i);
continue;
}
tag = 1;
for (int i = 0; i < 4; i++) {
if (i == r)
pos.push_back(ans.size()), ans.push_back(0);
ans.push_back(tot - i);
}
}
tot += x;
for (int i = 0; x > 0; x--, i++)
ans.push_back(tot - i);
for (int i = 0; i < pos.size(); i++)
ans[pos[i]] = ++tot;
cout << ans.size() << '\n';
for (int i = 0; i < ans.size(); i++)
cout << ans[i] << ' ';
cout << '\n';
ans.clear();
}
return 0;
}
NOIP Day4A. 二分图 (5.5)
2025.11.11
又是被 T1 干翻的一天,状态实在令人堪忧。由于对二分图和欧拉路径的性质并不算熟悉,进行了长时间的对峙。由于其诡异描述,看上去其特殊性质很重要,因此长时间尝试观察大样例性质,但是毫无收获。经过了两个多小时后,我达到了最接近正解思路的一刻:我去想特殊性质 A,我忘记了只需要走过他的所有边,我考虑上了所有的孤点。首先度数全偶连通块可以走出一条欧拉回路,但是孤点若同侧则可以直接做完,答案为连通块数量 - 1。否则,我需要暂时放弃走完这条欧拉回路,留在另一边,清掉所有孤点后再回来,走完回路。答案应该刚好是连通块个数。当我发现该情况并不存在,立马写出了 20 分超高暴力。
不难发现偶度点作用不大,除非造成整块否则毫无贡献。此时在同一连通块中,传送相当于给同侧的两个点加一条有代价的边。于是一个连通块可以任意的选择两个同侧奇度点,花 1 的代价消除;异侧奇度点可以直接通过走欧拉路径解决,但是还需要通过传送来解决其他奇度点。因此,每两个奇度点都要花 1 代价消除,加上全偶连通块个数,再加上 若存在异侧的奇度点,但不存在一个连通块有异侧奇度点,那么无法通过欧拉路径免费换边,因此还要再加 1 代价,最后 -1 即可。
太脑子了,为什么我不会?
Code
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 2e4 + 1;
int n, m;
vector<int> e[kN];
bool vis[kN], deg[kN];
int cl, cr;
int Size(int x) {
vis[x] = 1;
int ret = 1;
(x > n ? cr : cl) += deg[x];
for (auto v : e[x]) {
if (!vis[v])
ret += Size(v);
}
return ret;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v, v += n;
e[u].push_back(v), e[v].push_back(u);
deg[u] ^= 1, deg[v] ^= 1;
}
int ev = 0, cnt = 0;
bool tag = 1, sl = 0, sr = 0;
for (int i = 1; i <= 2 * n; i++) {
if (vis[i] || Size(i) == 1)
continue;
cerr << i << ' ' << cl << ' ' << cr << '\n';
ev += !cl && !cr;
cnt += cl + cr;
sl |= cl, sr |= cr, tag &= !(cl && cr);
cl = cr = 0;
}
cout << ev + cnt / 2 - 1 + (tag && sl && sr) << '\n';
return 0;
}
NOIP Day4B. 斜堆 (3.5)
2025.11.12
或许是这场最可做的题目。
首先 \(\sum dep\) 非常恶心,反思一下,转化为 \(\sum siz\) 很可能更好做。
手玩一下,结合基础知识,可以发现斜堆的性质极好,因为左子树的大小一定形如 \(k^x\),右子树大小一定在 \([k^{x-1}, k^{x+1})\) 之间。因此对于一个 \(n\),暴力插入的过程中只有一直向右的一条链被考虑,所以现在考虑这样的一条链,每次我都可以算出左子树的大小,递归考虑右子树。但是当 \(k\) 很大的时候,每次对 \(n\) 的削减量不理想,因为 \(x\) 变化很少,一直向右暴力减一个很小的数,非常耗时。于是发现 \(x\) 从高到低是单调递减的,\(x\) 的变化次数是 \(\log_{k}n\) 的,而且对于每个左子树大小都可以直接算出 接下来有多长的一条链左子树都挂的这个 \(k^x\)。直接做即可。至于 \(k^x\) 这样的树的本身贡献,发现对于每个 \(k\) 也只有 \(\log_k n\) 个,因此可以简单预处理。
发现遗漏了一种情况,当 \(k=1\) 的时候前面的结论不成立。但是发现如果当前不是满二叉树,一定会在最底层插入一个新的叶子。因此枚举每一层,看有多少点,直接算即可。
考场上并没有细想这个题,因为打开题面的页面顺序是 C 在前,以为 C 是 B 啊/hsh 不过改题的时候一遍写对了,只有两个好发现的小错误,果真代码能力有提升吗/se
Code
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using ULL = unsigned long long;
using PII = pair<int, int>;
constexpr int kN = 70 + 1;
int m, q, K[kN];
ULL f[kN][kN], pw[kN][kN];
ULL Calc(ULL x, int id) {
int cur = 0, k = K[id];
for (ULL c = x; c > 0; c /= k, cur++);
cur--;
ULL ret = 0;
for (; x > 1;) {
for (; cur > 0 && pw[id][cur] + pw[id][cur + 1] >= x; cur--);
ULL tl = pw[id][cur + 1], p = (x - pw[id][cur]) / (tl + 1);
ret += p * f[id][cur + 1] + __int128(x + (x - (p - 1) * (tl + 1))) * p / 2;
x -= p * (tl + 1);
}
return ret + x;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> m >> q;
for (int i = 1; i <= m; i++) {
cin >> K[i];
if (K[i] == 1)
continue;
int lg = 0;
for (LL x = (1e18 + 1); x >= K[i]; x /= K[i], lg++);
pw[i][1] = f[i][1] = 1;
for (int d = 2; d <= lg + 1; d++) {
pw[i][d] = pw[i][d - 1] * K[i];
f[i][d] = Calc(pw[i][d], i);
}
}
for (ULL n; q--;) {
cin >> n;
ULL ans = 0;
for (int i = 1; i <= m; i++) {
ULL x = n;
if (K[i] == 1) {
for (ULL cnt = 1, d = 1; x > 0; d++, cnt *= 2)
cnt = min(cnt, x), ans += cnt * d, x -= cnt;
continue;
}
ans += Calc(x, i);
}
cout << ans << '\n';
}
return 0;
}
NOIP Day5A. 矩阵操作 (5)
2025.11.13
难度不低的一道 T1。看到数据范围,可以猜是 \(\mathcal{O}(qm)\) 的。观察操作性质,可以看作为按照某个方向扫,对于一个负数找到其前最靠后的正数相消,暴力的话大概可以线段树二分之类的。
我一开始的想法大概是尝试每行内部先缩掉,一定是缩成 前缀负后缀正的形式。只扣掉中间几行,很大概率是前缀拼后缀,但是我当时没能想明白这个咋拼,也没想明白后缀我想要什么信息才好做,因此又觉得后缀也很难,于是非常倒闭。后来想了想,觉得复杂度里有带 \(m\),说不定是按列考虑,一列一列合起来。然后可能是线段树维护一些东西,但是这样按我的想法,合并一定会枚举到所有的负数位置,非常炸,跟暴力无异。
后来反思自己做法的时候,发现每行自己消掉这个过程,虽然它正确性也是错的,但是这样的行直接合并的方式,也体现出一个事情:对于一段行的后缀,我其实只在乎它消除之后,每列还有多少负数要抵消。相似的,我只在乎行的前缀,最优化消除之后序列长什么样子。再合并,就是枚举后缀的负数去消前缀。这样用栈维护下正数位置就可以做到所有事情,复杂度 \(\mathcal{O}(qm+nm)\)。
其实我做这道题的整个无效的点极多,我非常需要能够在转换思路之前想清楚自己先前的做法为什么做不了,即自己想不到什么样的东西,不然只能等待思路跳回来,浪费的时间成倍增加。
Code
#pragma GCC optimize("Ofast,unroll-loops")
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e3 + 2, kM = 500 + 1;
int n, m, q;
int a[kN][kM];
LL pre[kN], suf[kN];
LL lst[kN][kM], upd[kN][kM], cur[kM];
int pos[kN], top;
LL s[kN];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m >> q;
for (int i = 1; i <= n; i++) {
s[i] = s[i - 1];
for (int j = 1; j <= m; j++)
cin >> a[i][j], s[i] += abs(a[i][j]);
}
for (int i = 1; i <= n; i++) {
pre[i] = pre[i - 1];
for (int j = 1; j <= m; j++) {
int w = a[i][j];
lst[i][j] = lst[i - 1][j] + max(w, 0);
if (lst[i][j] > 0)
pos[++top] = j;
if (w < 0) {
for (w = -w; top > 0 && w > 0;) {
int p = pos[top], del = min(LL(w), lst[i][p]);
lst[i][p] -= del, w -= del;
pre[i] += 2 * del;
if (lst[i][p] == 0)
top--;
}
}
}
top = 0;
}
for (int i = n; i >= 1; i--) {
suf[i] = (i < n ? suf[i + 1] : 0);
for (int j = 1; j <= m; j++) {
if (a[i][j] > 0)
pos[++top] = j;
upd[i][j] = upd[i + 1][j] - min(0, a[i][j]);
for (; top > 0 && upd[i][j] > 0;) {
int p = pos[top], del = min(LL(a[i][p]), upd[i][j]);
a[i][p] -= del, upd[i][j] -= del;
suf[i] += 2 * del;
if (a[i][p] == 0)
top--;
}
}
top = 0;
}
cout << s[n] - pre[n] << '\n';
for (int l, r; q--;) {
cin >> l >> r;
for (int i = 1; i <= m; i++)
cur[i] = lst[l - 1][i];
LL ans = pre[l - 1] + suf[r + 1];
for (int i = 1; i <= m; i++) {
if (cur[i] > 0)
pos[++top] = i;
LL d = upd[r + 1][i];
for (; top > 0 && d > 0;) {
LL p = pos[top], del = min(cur[p], d);
cur[p] -= del, d -= del;
ans += 2 * del;
if (cur[p] == 0)
top--;
}
}
top = 0;
cout << s[l - 1] + s[n] - s[r] - ans << '\n';
}
return 0;
}
NOIP Day5C. 有向稀疏图上更快的三元环计数 (?)
2025.11.13
这个题用了一个 trick,即当对像这样以特殊规则对三角形黑白染色,要统计异色三角形个数,可以通过统计异色角的贡献来处理。对于一个异色角,能形成的全都是异色三角形;而对于一个异色三角形,存在两个异色角,除以 \(2\) 即可。比如这道题,枚举两条异色线 \(i,j\) 所夹异色角,首先线相交意味着 \(i+j\geq n\),若已相交则有 \(\min(i, j)\) 个第三边可用。此时只需要计算这个 \(\sum \min\),此时枚举一条边与其他边相交,相交的编号一定是一个后缀,然后拆成 \(\sum i\) 和 \(cnt * j\) 这样贡献即可。线性是简单的。
剩下的只需要算出总方案数,减去即可。这个可以拆为正三角和倒三角处理,枚举其大小计算,非常简单。但是 cyx oeis 出了一个很厉害的式子:\(cnt = \lfloor n(n+2)(2n+1) / 8\rfloor\),非常神奇,甚至带取整,简直毫无道理,但还真是对的。搜了一下,发现定义如此。不取整的有 \(cnt = (2n(n+2)(2n+1) + (-1)^ n - 1) / 16\),递推转移有 \(f_n=3f_{n-1}-2f_{n-2}-2f_{n-3}+3f_{n-4}-f_{n-5}\),甚至有边长为 \(k\) 的子三角形个数
完全不懂,不过挺有意思的。
Code
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e6 + 1, kP = 998244353;
int n;
char ch;
int a[3][kN], s[3][kN][2], f[3][kN][2];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int o = 0; o < 3; o++)
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] + 1ll * (a[o][i] == t) * i) % kP;
}
}
int ans = 0;
for (int p = 0; p < 3; p++) {
int q = (p + 2) % 3;
for (int i = 1; i <= n; i++) {
bool o = !a[q][i]; // i + j >= n
if (i >= n - i)
ans = (ans + f[p][i][o] - (i != n ? f[p][n - i - 1][o] : 0)) % kP;
ans = (ans + 1ll * (s[p][n][o] - s[p][max(i, n - i - 1)][o]) * i) % kP;
}
}
// cout << ans << '\n';
ans = (kP + 1ll) / 2 % kP * ans % kP;
ans = -ans;
for (int i = 1; i <= n; i++)
ans = (ans + i * (i + 1ll) / 2) % kP;
for (int i = n - 1; i >= 1; i -= 2)
ans = (ans + i * (i + 1ll) / 2) % kP;
cout << ans << '\n';
return 0;
}
NOIP Day5D. 序列求交 (5)
2025.11.14
发现这个题所查询的“区间对相似程度”即为难刻画,很明显直接维护是没有任何可能的,加上 \(a, b\) 是排列的优秀性质,考虑拆贡献。对于同一个数 \(a_i=b_j=w\),在 \(a\) 序列中的 \([i-k+1, i]\) 开头的区间是能取到的,\(b\) 同理,那么意味着 \(w\) 会给这样的一个矩形产生 \(1\) 的相似度贡献。此时静态做法呼之欲出,直接把每个数的矩形拿下来,扫描线求总的最大值个数即可。
此时需要带修改。若沿用上述做法,最后求答案会有单点修改,需要线段树维护每个位置;需要访问到扫描线每一列的信息,需要主席树。发现我只会对 \(a\) 做修改,因此相当于每次修改两个矩形,让他向 左 / 右平移一格,其实这样只会修改 \(2\times 2\) 列的一个区间。此时可以把扫描线得出的每个主席树当成一个独立的动态开点线段树,自由的进行区间修改即可。
今天写的时候没有想明白这个独立的事情,浪费了一个晚上用来瞪其他地方。但是尝试了一下之前提出的,时间辍不同才加点。但是没能写对,不知道是不是想法假了,assert 了一些儿子父亲的等量关系狠狠 failed,但是搞不明白,故放弃。不过这个如果能是真的,又能减小多少常数呢?不知道。/hsh
Code
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e5 + 2;
int n, k, q, N;
int a[kN], b[kN], pa[kN], pb[kN];
struct Rec { int x, y, l, r; } rc[kN];
struct Scan { int l, r, w; };
vector<Scan> sc[kN];
struct Info {
int mx;
LL cnt;
Info operator+(const Info &x) const {
int Max = max(mx, x.mx);
LL Cnt = (mx == Max) * cnt + (x.mx == Max) * x.cnt;
return {Max, Cnt};
}
};
int rt[kN], tot;
struct Node {
Info f;
int ls, rs, tag;
} t[350 * kN];
int Build(int l, int r) {
int x = ++tot;
t[x] = {0, r - l + 1, 0};
if (l < r) {
int mid = (l + r) / 2;
t[x].ls = Build(l, mid);
t[x].rs = Build(mid + 1, r);
}
return x;
}
int Add(int x, int L, int R, int w, int l = 1, int r = N) {
if (r < L || R < l)
return x;
int y = ++tot;
t[y] = t[x];
if (L <= l && r <= R) {
t[y].f.mx += w, t[y].tag += w;
assert(t[y].f.mx == (t[t[y].ls].f + t[t[y].rs].f).mx + t[y].tag);
return y;
}
int mid = (l + r) / 2;
t[y].ls = Add(t[x].ls, L, R, w, l, mid);
t[y].rs = Add(t[x].rs, L, R, w, mid + 1, r);
t[y].f = t[t[y].ls].f + t[t[y].rs].f;
t[y].f.mx += t[y].tag;
return y;
}
Info ans[4 * kN];
void Update(int p, const Info &f) {
int x = 1, l = 1, r = N;
for (; l != r;) {
int mid = (l + r) / 2;
x = 2 * x + (p <= mid);
p <= mid ? (r = mid) : (l = mid + 1);
}
ans[x] = f;
for (; (x /= 2) > 0;)
ans[x] = ans[2 * x] + ans[2 * x + 1];
}
void Modify(int x, int l, int r, const int &w) {
rt[x] = Add(rt[x], l, r, w);
Update(x, t[rt[x]].f);
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> k >> q;
N = n - k + 1;
for (int i = 1; i <= n; i++)
cin >> a[i], pa[a[i]] = i;
for (int i = 1; i <= n; i++)
cin >> b[i], pb[b[i]] = i;
for (int i = 1; i <= n; i++) {
int x = max(1, pa[i] - k + 1), y = min(N, pa[i]),
l = max(1, pb[i] - k + 1), r = min(N, pb[i]);
rc[i] = {x, y, l, r};
sc[x].push_back({l, r, 1});
if (y < N)
sc[y + 1].push_back({l, r, -1});
}
rt[0] = Build(1, N);
for (int i = 1; i <= N; i++) {
rt[i] = rt[i - 1];
for (auto P : sc[i])
rt[i] = Add(rt[i], P.l, P.r, P.w);
Update(i, t[rt[i]].f);
}
cout << ans[1].mx << ' ' << ans[1].cnt << '\n';
for (int pos; q--;) {
cin >> pos;
swap(a[pos], a[pos + 1]);
pa[a[pos]] = pos, pa[a[pos + 1]] = pos + 1;
for (int i : {a[pos], a[pos + 1]}) {
int x = max(1, pa[i] - k + 1), y = min(N, pa[i]);
int rx = rc[i].x, ry = rc[i].y, l = rc[i].l, r = rc[i].r;
if (x < rx)
Modify(x, l, r, 1);
else if (x > rx)
Modify(rx, l, r, -1);
if (y < ry)
Modify(ry, l, r, -1);
else if (y > ry)
Modify(y, l, r, 1);
rc[i].x = x, rc[i].y = y;
}
cout << ans[1].mx << ' ' << ans[1].cnt << '\n';
}
return 0;
}
NOIP Day6B. 食物 (4)
2025.11.15
想了很久怎么很好的维护当前的所有区间,后面反思发现根本做不到,但是可以利用其他节点的信息。因为我的走路方案一定是,两侧直走,或先向一边再调头走另一边,难以计算的是后者。其实,比如点 \(i\) 走 \(j\) 步,向左再向右的方案,可以规约为 \((i-1,j-1)\) 的方案,即钦定提前走一步。右侧同理。于是就可以轻松做到 \(\mathcal{O}(n^2)\)。
按这样的说法,直接做可能会让空间炸掉,因为我两种方案推点的方向是不同的,需要开 \(2n^2\) 的 long long 数组存储另一侧的信息,用于取 max 求答案。考场上我写了一个分块存储,非常丑陋。赛后发现,其实根本没必要,因为我虽然 \(i\) 的方向不同,但是我 \(j\) 的转移时一定 \(-1\) 的!因此考虑用步数递推,非常好写,毫无空间问题。
Code
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 8e3 + 1;
int n, a[kN];
LL sum[kN], fl[kN], fr[kN], ans[kN];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i], sum[i] = sum[i - 1] + a[i];
for (int t = 1; t <= 2 * n; t++) {
for (int s = n; s >= 1; s--)
fl[s] = max(fl[s - 1], sum[min(n, s + t)] - sum[s - 1]);
for (int s = 1; s <= n; s++)
fr[s] = max(fr[s + 1], sum[s] - sum[max(0, s - t - 1)]);
for (int s = 1; s <= n; s++)
ans[s] ^= t * max(fl[s], fr[s]);
}
LL Ans = 0;
for (int i = 1; i <= n; i++)
Ans ^= i + ans[i];
cout << Ans << '\n';
return 0;
}
C. Bus Routes (5)
2025.11.16
很有意思的题目,但是不知道为什么 *3400 /hsh
进行一些思考性质,可以发现,由于对于一条路径只能通过两个公交线路,因此必然可以只有一个“换色点”。用一个自下而上合并路径集的思想,考虑当前在点 \(i\) 的某两个子树的两个点,他们的路径必然经过 \(i\),但是只有一个换色点,因此必然有一个点到 \(i\) 的路径只需要通过一条公交线路。这个性质非常美丽,但是用来做这个合并路径集合还是不好维护。
考虑换色点具体会在什么位置上。设两个点为 \(x,y\),则分别找到最高的,能只通过一条路径达到的点 \(f_x,f_y\),可以发现存在方案必然有一个 \(f\) 比 LCA 更高,而且换色点一定会在 \(f_x,f_y\) 中更深的一个上,除非两个都比 LCA 高,换色点只可能在 LCA 上。由于我们只需要找出一个不符合条件的方案,因此我们直接拿最严格的限制,即深度最深的那一个 \(f\),所有的点都需要能够通过一条路径到达 \(f\),于是很好判断和构造。
之前写了个新优选,因为查询复杂度带个 \(\log\),复杂度写成了 \(\mathcal{O}((n+m)\log n)\),被卡常了,改成 dfn 求 LCA 就过了。新优选真有点难用呗。
Code
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e5 + 1;
int T, n, m;
vector<int> e[kN];
int dep[kN], st[20][kN], dfn[kN], dfc;
int High(int x, int y) { return dep[x] < dep[y] ? x : y; }
void Init(int x, int f) {
dep[x] = dep[f] + 1;
st[0][dfn[x] = ++dfc] = f;
for (auto v : e[x]) {
if (v != f)
Init(v, x);
}
}
int LCA(int x, int y) {
if (x == y)
return x;
x = dfn[x], y = dfn[y];
x > y && (swap(x, y), 0);
int k = __lg(y - x);
return High(st[k][x + 1], st[k][y - (1 << k) + 1]);
}
int anc[kN];
vector<PII> Road;
void Dfs(int x, int fa) {
int &f = anc[x];
for (auto v : e[x]) {
if (v != fa)
Dfs(v, x), f = High(f, anc[v]);
}
}
void GetAnc(int rt) {
dfc = 0;
for (int i = 1; i <= n; i++)
dep[i] = dfn[i] = anc[i] = 0;
Init(rt, 0);
for (int d = 1; d < 20; d++) {
for (int i = 1; i + (1 << d) - 1 <= n; i++)
st[d][i] = High(st[d - 1][i], st[d - 1][i + (1 << d - 1)]);
}
iota(anc, anc + n + 1, 0);
for (PII P : Road) {
int x = P.first, y = P.second, l = LCA(x, y);
anc[x] = High(anc[x], l), anc[y] = High(anc[y], l);
}
Dfs(rt, 0);
}
int id[kN];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
cin >> n >> m;
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
e[u].push_back(v), e[v].push_back(u);
}
for (int i = 1, x, y; i <= m; i++)
cin >> x >> y, Road.emplace_back(x, y);
[&]() {
GetAnc(1);
iota(id, id + n + 1, 0);
sort(id + 1, id + n + 1, [&](int x, int y) { return dep[anc[x]] > dep[anc[y]]; });
int p = id[1], cp = anc[p]; // Color Changing Point
GetAnc(cp);
int sb = 0;
for (int i = 1; i <= n; i++)
anc[i] != cp && (sb = i);
if (sb == 0)
cout << "Yes\n";
else {
cout << "No\n";
cout << sb << ' ' << p << '\n';
}
}();
Road.clear();
for (int i = 1; i <= n; i++)
e[i].clear();
}
return 0;
}
NOIP Day6C. 区间 (5.5)
2025.11.17
考场上不太会做,因为在想一些按位考虑之类的拆贡献 trick,然而 5e5 的限制让这种做法走向只能 \(O(n)\) 处理 01 问题,过于严苛,因此本身其实是没有前途的。但是的确想了挺久,也不咋会低于平方的做,应该及时切换思路的。
反思一下,其实可以直接维护对于每个 \(k\) 的答案。考虑修改的影响,一个 \(a_i\) 对答案的贡献,可以找到在 +1 之前作为不严格最大值的区间 \([l,r]\),在其中包含 \(i\) 的所有区间都会使其 \(max\) 加一。通过一些分类讨论,可以发现这个贡献是一个 等差数列加/减 和区间加。这是好维护的。同理,对于最开始的答案,贡献是类似的。非常好做。
其实有挺多这样比较直接的题目,思考问题应当由简单到复杂,切换做法的时候想明白为什么做不了。
Code
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e5 + 1;
int n, m, ID, a[kN];
namespace Array {
int mx[4 * kN];
void Build(int x, int l, int r) {
if (l == r)
return mx[x] = a[l], void();
int mid = (l + r) / 2;
Build(2 * x, l, mid);
Build(2 * x + 1, mid + 1, r);
mx[x] = max(mx[2 * x], mx[2 * x + 1]);
}
void Add(int p, int x = 1, int l = 1, int r = n) {
if (l == r)
return mx[x]++, void();
int mid = (l + r) / 2;
p <= mid ? Add(p, 2 * x, l, mid) : Add(p, 2 * x + 1, mid + 1, r);
mx[x] = max(mx[2 * x], mx[2 * x + 1]);
}
int Pre(int p, int x = 1, int l = 1, int r = n) {
if (mx[x] <= a[p] || l >= p)
return 0;
else if (l == r)
return l;
int mid = (l + r) / 2;
int R = Pre(p, 2 * x + 1, mid + 1, r);
if (R == 0)
return Pre(p, 2 * x, l, mid);
return R;
}
int Next(int p, int x = 1, int l = 1, int r = n) {
if (mx[x] <= a[p] || r <= p)
return n + 1;
else if (l == r)
return l;
int mid = (l + r) / 2;
int L = Next(p, 2 * x, l, mid);
if (L == n + 1)
return Next(p, 2 * x + 1, mid + 1, r);
return L;
}
}
struct Tag {
LL k, b;
Tag Add(int x) { return {k, b + x * k}; }
void operator+=(const Tag &x) { k += x.k, b += x.b; }
} t[4 * kN];
void PushDown(int x, int l, int r) {
int mid = (l + r) / 2;
t[2 * x] += t[x];
t[2 * x + 1] += t[x].Add(mid - l + 1);
t[x] = {0, 0};
}
void Update(int L, int R, Tag w, int x = 1, int l = 1, int r = n) {
if (r < L || R < l || R < L)
return;
else if (L <= l && r <= R)
return t[x] += w;
int mid = (l + r) / 2;
PushDown(x, l, r);
Update(L, R, w, 2 * x, l, mid);
Update(L, R, w.Add(max(0, mid - max(l, L) + 1)), 2 * x + 1, mid + 1, r);
}
LL Query(int p, int x = 1, int l = 1, int r = n) {
if (l == r)
return t[x].b + t[x].k;
int mid = (l + r) / 2;
PushDown(x, l, r);
return p <= mid ? Query(p, 2 * x, l, mid) : Query(p, 2 * x + 1, mid + 1, r);
}
void Contri(int l, int r, int x, int w) {
PII P = minmax(x - l + 1, r - x + 1);
int p = P.first, q = P.second;
Update(1, p, {w, 0});
Update(p + 1, q, {0, 1ll * p * w});
Update(q + 1, r - l + 1, {-w, 1ll * p * w});
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m >> ID;
for (int i = 1; i <= n; i++)
cin >> a[i];
Array::Build(1, 1, n);
[&]() {
int top = 0;
vector<int> s(n + 1, 0);
for(int i = 1; i <= n; i++) {
for (; top > 0 && a[s[top]] < a[i]; top--)
Contri(s[top - 1] + 1, i - 1, s[top], a[s[top]]);
s[++top] = i;
}
for (int i = 1; i <= top; i++)
Contri(s[i - 1] + 1, n, s[i], a[s[i]]);
}();
for (int o, x; m--;) {
cin >> o >> x;
if (o == 2) {
int l = Array::Pre(x) + 1, r = Array::Next(x) - 1;
Array::Add(x), a[x]++;
Contri(l, r, x, 1);
} else
cout << Query(x) << '\n';
}
return 0;
}
NOIP Day7B. dfsize (4.5)
2025.11.18
考场上唯一会做的题目。
探究这个 "dfsize" 序列的性质。因为 DFS 序,所以对于一个位置 \(i\),它所对应的子树正好覆盖了 \([i, i+a_i-1]\) 这段区间,然后可以有一些树的性质。此时求 \([l,r]\) 成树的最小操作次数,可以改为 \([l+1,r]\) 成森林的最小操作次数 +1,然后通过拼凑所有树的区间得到最小次数。这个事情可以区间 DP 做,于是就有了 \(\mathcal{O}(n^3)\)。
反思这个做法转化,发现区间 DP 的拼森林有着类似的性质。如果现在考虑 \(l\) 这个位置是否进行修改,那么就只有两种转移:
- 修改。\(f_{l,r}=f_{l+1,r}+[a_l\neq r-l+1]\)。
- 不修改。\(f_{l,r}=f_{l,l+a_l-1}+f_{l+a_l, r}, (r\geq l+a_l)\)。
此时转移就是 \(\mathcal{O}(1)\) 的,总体 \(\mathcal{O}(n^2)\)。
其实在考场上,我思考了很久能否改变这个区间 DP 的转移方式,来降低它的复杂度。但是事实是,直接做的区间 DP,是十分平凡又朴素的,这方面的考虑毫无意义。应该思考的是,这个区间 DP 有什么性质(像外星人输出转移发现默认不修改更优)?这个 DP 能不能改写转移方式?清楚自己在做什么才能有效思考,更多有效思考才有可能打出理想表现。
Code
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e3 + 2;
int T, n, a[kN], b[kN];
int f[kN][kN];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
cin >> n;
for (int i = 1; i <= n; i++)
cin >> b[i], a[i] = i + b[i] - 1;
for (int i = n; i >= 1; i--) {
f[i][i] = 1;
for (int j = i + 1; j <= n; j++)
f[i][j] = f[i + 1][j] + 1;
if (a[i] <= n)
f[i][a[i]]--;
for (int j = a[i] + 1; j <= n; j++)
f[i][j] = min(f[i][j], f[i][a[i]] + f[a[i] + 1][j]);
}
LL ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j++)
ans += b[i] ^ b[j] ^ (f[i + 1][j] + (a[i] != j));
}
cout << ans << '\n';
}
return 0;
}
NOIP Day7A. 某道题的 checker (?)
2025.11.18
考场上的确没有一个很合理的思路,想过按位考虑想过高维前缀和但是没想到差分。因为我对高维前缀和的理解和应用能力太差,也没有在此方向思考多久,去想能否 DFS 搜索每种高位状态,然后下传地去考虑限制。虽然下传只会走一边,问题不大,但是对于没有限制的位可能会算好多遍。我当时的想法是,我可以复制一下之类的事情。没有细想,但是感觉很诡异。
事实上的确没啥前途。但是对于一种贡献方式,强制填 0 的限制此时变得很诡异。不妨类似 CSP2025 T4 的那个做法,直接差分成 0/1 - 1 容斥掉,这样就能直接高维前缀和做掉了。按理说是比较送的,是我太菜。
Code
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e5 + 1, kB = 22;
int n, m, a[kN];
int f[1 << kB], ans[kN];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i < n; i++) {
int s = 0;
for (int d = m - 1; d >= 0; d--) {
int l = a[i] >> d & 1, r = a[i + 1] >> d & 1;
if (l < r)
f[s]++, f[s | (1 << d)]--;
if (l != r)
s |= 1 << d;
}
}
for (int d = 0; d < m; d++)
for (int s = 0; s < (1 << m); s++) {
if (s >> d & 1)
f[s] += f[s ^ (1 << d)];
}
for (int s = 0; s < (1 << m); s++)
ans[f[s]]++;
for (int i = 0; i < n; i++)
cout << ans[i] << ' ';
return 0;
}
但是如果没能想到差分,或者这个信息不可差分,金巨仍然有办法。可以发现,可以按照强制 0 的位置对贡献模式分类,然后比 0 更高位的还是可以高维前缀和,这一位定下来了,更低位的是任取的。合理规划一下枚举顺序即可。复杂度不变,多个两倍常数,但是可以做 max/min 之类的信息。
jsq Orz
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e5 + 1, kB = 22;
int n, m, a[kN];
vector<int> f[kB];
int ans[kN], g[1 << kB];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 0; i < m; i++)
f[i].resize(1 << i);
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i < n; i++) {
int s = 0;
for (int d = 0; d < m; d++) {
int t = (1 << m - 1 - d), l = a[i] & t, r = a[i + 1] & t;
f[d][s] += (l < r);
if (l != r)
s |= 1 << d;
}
}
for (int i = 0; i < m; i++) {
for (int d = 0; d <= i; d++)
for (int s = 0; s < (1 << i); s++) {
if (s >> d & 1)
f[i][s] += f[i][s ^ (1 << d)];
}
for (int s = 0; s < (1 << i); s++) {
if (i > 0 && !(s >> i & 1))
g[s | (1 << i)] = g[s];
g[s] += f[i][s];
}
}
for (int s = 0; s < (1 << m); s++)
ans[g[s]]++;
for (int i = 0; i < n; i++)
cout << ans[i] << ' ';
return 0;
}
NOIP Day7C. 构造倒水 (5)
2025.11.19
这个题,发现 sub4 不包含 sub2,很明显了是拼 sub 来的。然后这个题有个关键思想,就是如果有三种方案,他们的和是一个定值,那么他们的最小值一定不大于这个定值的三分之一。马后炮的来看,其实下取整的超强限制也暗示着这一点。
考虑 \(b_i=1\),最初肯定可以想到,可以全部推到 \(n\) 的位置,然后一个一个往前推,因为会填满前面的瓶子,所以刚好全部变成 1。但是很容易卡到 \(2n\),只要最初也全 1 就可以了()考虑更优,发现这个次数取决于最初有水的瓶子个数 \(A\),操作次数是 \(n+A\) 的。如果想要 \(\lfloor 4n/3\rfloor\),那么需要 \(A\geq n/3\) 的时候有更优的次数,即需要一个带 \(n-A\) 的次数。可以发现,可以每次都放到第一个瓶子,再放到空位上,这样次数就是 \(2(n-A)\) 的,可以通过 \(b_i=1\)。
将 \(b_i=1\) 转化为正常的 \(b\) 序列,可以对于不足的 \(b\) 位置找 \(b_i=0\) 的位置堆,令 \(B\) 为目标序列有水瓶子个数,那么可以简单再花 \(n-B\) 次操作做到。为了不被卡,需要有一个正 \(B\) 相关的方案。发现可以利用 \(i\) 瓶子容量为 \(i\),可以对于对于每个 \(i\),先放满 \(b_i\) 号瓶子,然后再倒到 \(i\) 瓶即可一次满足。此时为了一次能装满水、排除初始状态水的影响,可以先全放到 \(n\) 瓶,然后 \(i\) 从大到小满足条件。此时操作数是 \(A+2B\)。这个最小操作数的证明,大概是三种方案的操作数的和是 \(5n-O(1)\),最小值一定不大于 \(\lfloor 5n/3\rfloor\),可以通过此题。
考场上想到了全部 3 种操作方式,也猜了要拼 sub,但是没想到可以分析每种操作方式的操作次数,我只考虑了分别的上界,但没考虑到他们的操作次数是比较互补的。说实话想到这个取 min 也脑电波了吧,如何想到去考虑这些奇奇怪怪的东西啊?
Code
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e5 + 1;
int T, n, k;
int a[kN], b[kN];
vector<int> pa[2], pb[2];
vector<PII> ret;
void Do(int i, int j) { (i != j) && (ret.emplace_back(i, j), 0); }
vector<PII> AllOne() {
ret.clear();
// op = A + n - 1
for (auto p : pa[1])
Do(p, n);
for (int i = n; i > 1; i--)
Do(i, i - 1);
vector<PII> tmp;
tmp.swap(ret);
// op = 2 * (n - A)
int j = (!pa[0].empty() && pa[0][0] == 1);
if (j == 1) {
int i = pa[1].size() - 1;
for (; a[pa[1][i]] == 1; i--);
Do(pa[1][i], 1);
}
for (int i = 0; i < pa[1].size(); i++) {
int p = pa[1][i];
for (int w = 1; w < a[p] && j < pa[0].size(); w++, j++)
Do(1, pa[0][j]), Do(p, 1);
}
// cerr << ret.size() << ' ' << tmp.size() << '\n';
return ret.size() < tmp.size() ? ret : tmp;
}
vector<PII> Get() {
ret.clear();
for (auto p : pa[1])
Do(p, n);
for (int i = pb[1].size() - 1; i >= 0; i--) {
if (int p = pb[1][i]; p != n)
Do(n, b[p]), Do(b[p], p);
}
return ret;
}
void Output(vector<PII> &ans) {
cout << ans.size() << '\n';
for (auto P : ans)
cout << P.first << ' ' << P.second << '\n';
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
// cerr << T << ' ';
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
pa[bool(a[i])].push_back(i);
}
for (int i = 1; i <= n; i++) {
cin >> b[i];
pb[bool(b[i])].push_back(i);
}
[&]() {
auto ans = AllOne();
if (pb[0].empty())
return Output(ans);
int i = 0;
for (auto p : pb[1]) {
for (int w = 1; w < b[p]; w++, i++)
ans.emplace_back(pb[0][i], p);
}
auto cur = Get();
Output(cur.size() < ans.size() ? cur : ans);
// Output(cur);
} ();
for (int o : {0, 1})
pa[o].clear(), pb[o].clear();
}
return 0;
}

浙公网安备 33010602011771号