CF1621D 题解

原题链接 一句话总结:四个关键点是“支架”。 首先注意到右下角的 $n \times n$ 区域内的雪必须全部清除,否则连目的地都不安全,一定会有人生病。 于是我们只需要在左下和右上两块区域中设法开出一条路来。 注意到有八个点,一旦清扫完成就不需要清扫别的点。这些点是 $(n+1, 1), (n+1, n), (2n, 1), (2n, n), (1, n+1), (1, 2n), (n, n+1), (n, 2n)$。感性理解一下。 事实上,只需要在这些点中取最小代价,就得到了最优答案。 因为任何一种合法答案,都必然包含这八个点中的一个。 回到开头的省流,四个关键点是 $(1, 1), (1, n), (n, 1), (n, n)$。 直观上理解,它们不能全都走别的点的路径离开,必须有一个有属于自己的路。或者说,它们支撑了整个网格的结构,如果不移走其中一个的话就动弹不得。 具体的,我们最终肯定需要挪动这四个点到右下角。那么考虑第一次操作它们所在的任意行列。不可能在不使这四个点都不超出左上角 $n \times n$ 区域的情况下进行一次操作。 那么至少有一个点要移动到有雪的区域,我们需要清理它的落点。这落点一共可能有八个,就是上面列出的那些。 所以必然清理这八个点中的至少一个。同时,清理其中任意一个都足够了,不需要额外清理其他点。所以这八个点的代价取 $\min$ 就是答案。 代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <bits/stdc++.h> using namespace std; int t; int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cin >> t; for (int _ = 0; _ < t; ++_) { int n; cin >> n; int64_t term1 = 0; vector c(2 * n + 1, vector<int64_t>(2 * n + 1)); for (int i = 1; i <= 2 * n; ++i) { for (int j = 1; j <= 2 * n; ++j) { cin >> c[i][j]; if (i > n && j > n) { term1 += c[i][j]; } } } int64_t term2 = c[1][n + 1]; term2 = min(term2, c[1][2 * n]); term2 = min(term2, c[n][n + 1]); term2 = min(term2, c[n][2 * n]); term2 = min(term2, c[n + 1][1]); term2 = min(term2, c[n + 1][n]); term2 = min(term2, c[2 * n][1]); term2 = min(term2, c[2 * n][n]); cout << term1 + term2 << '\n'; } return 0; }

March 5, 2026

CF1632C 题解

原题链接 一句话总结:两个只能动一个 经过艰苦卓绝的努力,我们发现在 $a \geq b$ 时,答案为 $a - b$。原因很简单:这时除了将 $b$ 加一,我们的任何操作都会让 $a$ 变大,而无益于让二者相等。 这就让我们可以用 $b$ 控制 $a$ 的大小,让数据范围中的值域限制有了意义。 注意到执行一次操作 3 就会让 $a$ 变得不小于 $b$。这样就会落入 $a \geq b$ 的情况中,最优策略是固定的了。 所以我们最多执行一次操作 3。 那么,我们需要决策的,就仅仅是在这次操作之前,操作 1 和操作 2 的执行情况。 一种方法是枚举让 $a$ 加了多少,让 $b$ 加了多少。但平方复杂度肯定会超时。 回收伏笔:两个只能动一个。也就是说,操作 1 和操作 2 中至少有一种没有被执行。 证明:假设操作 1 执行了 $i$ 次,操作二执行了 $j$ 次。 那么我们的总代价就是 $i + j + ((a + i) \text{ or } (b + j)) - (b + j)$。 ...

March 5, 2026

哈集幂!

原题链接 挺神妙的一个题吧。自己做的时候完全没做明白,看题解也看到晕晕乎乎的。今天重新从哈集幂的视角理解了一下,才发现确实挺妙(而且典)。 面对这个题首先做一些小转化。考虑一条边,它在哪几条路径上?我们用一个掩码($k$ 位二进制数)表示这个集合。第 $i$ 位为 1 代表这条边在 $a_i$ 与 $b_i$ 间的简单路径上。 我们考虑给树赋一个根,然后把每条边和它连接的两个点中深度较大的对应起来。一条路径经过这条边,当且仅当一个端点在这个点的子树中,而另一个端点不在子树中。这样,我们对每个点维护一个掩码,第 $i$ 位代表第 $i$ 条路径在该点子树中的端点的数目的奇偶性。不难发现这个掩码其实和这个点对应的边的掩码相等。 容易预处理这个掩码。具体的,考虑对第 $i$ 条路径的一个端点 $a$,$\text{mask}_a = \text{mask}_a \text{ xor } 2^i$。然后我们做一次 dfs,对每个点,先处理它的所有子节点,然后它的掩码异或上它所有子节点的掩码的异或和。 然后,我们就注意到,挑选一些边涂色,这时被满足(即有至少一条边被涂色)的简单路径的集合就是这些边对应的掩码(集合)的并集(或者按位或)。 至此,我们就完成了所有平凡的预处理工作。问题被转化为: 给定若干集合,求最少需要选取多少集合才能使得它们的并集为全集。 这个问题是本题的核心,也是一个非常经典的 NP-hard 问题。 我们考虑将最优化问题转化为判定问题:从小到大枚举需要多少集合,再判定这是否可行。 注意到答案最大等于全集大小。因为每个集合至少贡献一位,否则直接去除它更好。 进一步转化为计数问题:选出 $x$ 个集合,有多少种方案使得它们的并集为全集? 原题在这里要求选出的集合互不相同,但选出重复集合并不影响。 考虑哈集幂。我们构造一个长度为 $2^k$ 的序列 $a_i$(数组),其中第 $s$ 个位置是选出 $i$ 个集合,其并集是掩码为 $s$ 的集合的方案数。 显然有 $a_i = (a_1)^i$,这里的乘法是并集卷积,即 $(\text{or}, \times)$ 卷积。意思是,$a\times b$ 的第 $k$ 位是所有满足 $i \text{ or } j = k$ 的 $a_i \times b_j$ 的和。 ...

February 19, 2026

解题过程实录 ARC117D

原题链接 首先看上去像差分约束,但边太多不太对。 假如一个点标 $x$,距离它 $d$ 的点可能标 $x-d$(如果 $x>d$),否则只能标 $x+d$。 这样是不是只需要考虑一个点对应的约束就做完了?但事实上(根据三角形不等式)其他约束几乎从来不会被自动满足(除非是链)。 绝对值。前两天做的题中就有绝对值。绝对值对应到距离。那么设法把树拍平到数轴上。 按照 DFS 序给树上节点标号?不太对。欧拉序?对完了。考虑将每个点的标号设为它在欧拉序中第一次出现的位置。 随手写一发哦哦过不了样例啊。有点麻烦,欧拉序一定是合法的,但未必是最优的。 我需要枚举每个点作为根吗?复杂度太高。怎么办? 注意到根的标号一定是 1。那么,考虑标号最大的节点,它是在欧拉序中最后出现的节点。我们要尽可能把它往前排。 也就是说,最小化它前面的“冗余”(那些在退出一个点时产生的记录)的节点数。 冗余的节点数……注意到我们可以挑一条(起点为根的)路径不产生冗余节点数,这时我们遍历的最后一个点是这条路径的终点。而其他地方必然产生冗余记录。 那么挑选直径作为这条路径。大胆写一发,发现 AC 了。 不过为什么这是对的?首先我们的构造能够达到 $2n-l$,其中 $l$ 是树的直径的长度(点数)。 能不能更优?答案是否定的。如何证明呢? 考虑将所有点按标号大小升序排序。我们假想进行一次遍历:从标号最小的点开始,每次沿两点间唯一简单路径走到下一个点,直到标号最大的点结束。 我们记 $d_i$ 是遍历到第 $i$ 个(意味着标号第 $i$ 大)点时已经走过的距离(边数),$p_i$ 是第 $i$ 个点,$a_i$ 是第 $i$ 个点的标号。 注意到 $d_{i+1} - d_i = \operatorname{dis}(p_{i+1}, p_i) \leq a_{i+1}-a_i$,即标号之差不小于距离。 注意到让第 $i$ 个点标号为 $d_i+1$(加一是为了让所有点标号不小于 1)时,每个不等号都可以取等。由于 $a_1$ 不变,取等显然更优。 并且取等的情况同样都可以建模为一种遍历顺序。 那么考虑最优化遍历顺序。 优化目标是 $d_n$,即总共经过的边的数目。 使用一种拆贡献的方法:考虑每条边被经过了多少次。 每条边被经过两次是显然上界。 那么考虑让尽可能多的边只被遍历一次。 注意到遍历一次就像“过桥”,如果从这条边砍断后的两个联通分量中的一个到了另一个,就无法再回去了。 显然所有遍历一次的边形成了一条链(这可以用归纳法证明)。 最大化它们的数目就是最大化链的长度。 不妨取直径作为这条链。 注意到只遍历一次的边的数目不可能更多,即不会因为有边走了超过两次而增加。 所以剩下的边一定遍历两次。 ...

January 10, 2026

解题过程实录 P10283

备注:解题过程实录不是题解。它是做题过程中的思维草稿,可能比较语无伦次。 并且由于中途有中断(比如去上学),可能会有一些内容看上去“奇迹般地突然出现”。 原题链接 这是个什么玩意儿。即没有任何奶牛是另一个奶牛的前缀。 首先前缀具备偏序性质。是不是该考虑拓扑排序? 不过挂上 01 Trie 更好。这样获得一个简洁的模型:每个点走到它子树中的一个位置去,最终没有任何点的子树中有别的东西。 最小化走动的距离的和。 对于一个节点对应的子树,记“边缘节点集合”为: 如果这个节点上有奶牛(即有奶牛的 01 串是这个节点对应的 01 串),且子树中没有奶牛:包含它自己的集合。 如果这个节点的子树中是空的(即没有任何奶牛的 01 串在这个节点的子树中):包含它自己的集合,但是有标记表明它是另一类节点。 其他情况:两个子树的边缘节点集合的并集。 (将 1、2 两个 case 合称原子子树) 将两类节点分开维护并按深度排序,从两类中的最浅点中抉择。这里可以考虑优先队列启发式合并。 之后,整个过程就转化为递归的类似树形 dp 的不断合并。 对于一个非原子子树(即除根节点外的其他点中有奶牛),考虑将根节点插入到子树中的某个边缘节点上。 注意被新节点插入之后空节点会转化为满节点。而满节点会转化为它的两个子节点。 满节点的代价是深度 +2,空节点的代价是深度。 一个重要的问题是,一个点上可能有多个奶牛。这也比较好解决:多次插入同样的节点即可。 而对于原子子树上的多个奶牛,可以先构造一个只有一个奶牛的原子子树,然后将剩余的节点变成插入过程。 一般来说,ad-hoc 题想到核心 idea 就过了。然而这个题考验精细实现和卡常。 如果你不使用指针,可能会多出许多不必要的拷贝,这样就会 TLE。换用 pbds 配对堆会 T 的更惨,也许是因为指针结构 copy 和 insert 的常数更大。 代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 #include <bits/stdc++.h> using namespace std; struct node { int cnt; node *left, *right; node() = default; } *rt = nullptr; void trie_insert(node *&p, const basic_string<char> &s, const int depth) { if (!p) { p = new node(); } if (depth == s.size()) { p->cnt++; } else { if (s[depth] == '0') { trie_insert(p->left, s, depth + 1); } else { trie_insert(p->right, s, depth + 1); } } } using set_t = priority_queue<int, vector<int>, greater<>>; int64_t ans = 0; void ins(set_t &f, set_t &e, int c, const int depth) { while (c) { if (f.empty()) { ans += e.top() - depth; f.push(e.top()); e.pop(); } else if (e.empty()) { const int x = f.top(); f.pop(); ans += x - depth + 2; f.push(x + 1); f.push(x + 1); } else { if (e.top() < f.top() + 2) { ans += e.top() - depth; f.push(e.top()); e.pop(); } else { const int x = f.top(); f.pop(); ans += x - depth + 2; f.push(x + 1); f.push(x + 1); } } c--; } } pair<set_t *, set_t *> query(const node *const p, const int depth) { // 前满后空 if (!p) { auto *q = new set_t(); q->push(depth); return {new set_t(), q}; } else { if (!p->left && !p->right) { auto *q = new set_t(), *tmp = new set_t(); q->push(depth); ins(*q, *tmp, p->cnt - 1, depth); return {q, tmp}; } else { auto [x,y] = query(p->left, depth + 1); auto [u,v] = query(p->right, depth + 1); if (x->size() < u->size()) { swap(x, u); } while (!u->empty()) { x->push(u->top()); u->pop(); } if (y->size() < v->size()) { swap(y, v); } while (!v->empty()) { y->push(v->top()); v->pop(); } ins(*x, *y, p->cnt, depth); return {x, y}; } } } int n; int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cin >> n; for (int i = 0; i < n; ++i) { basic_string<char> s; cin >> s; trie_insert(rt, s, 0); } query(rt, 0); cout << ans; return 0; }

January 10, 2026

Solution of ABC438G

题目链接 题外话: 数竞党的防御性证明会用严谨的逻辑说明结论的正确性,而对“解题思路是如何想到的”一类的问题没有任何启发;oier/acmer 的题解则大多会提示我们从哪里入手,却不加严谨的证明。而本题解采取折中的方案:既对解题思路没有任何启发,也不加严谨的证明。 考虑 gcd 的动机在于,对于这样的题,$k$ 巨大。 于是考虑循环节。发现循环节是 $\mathrm{lcm}(n, m)$。 这时,我们考虑完整的循环节。 发现在 $n$ 与 $m$ 不互素时这是麻烦的,而 $n$ 与 $m$ 互素时一个循环节内每对 $(a_i, b_j)$ 刚好出现且仅出现一次。 这时考虑 $g = \gcd(n, m)$。 发现,对于任意 $r$,一个模 $g$ 余 $r$ 的 $i$ 只会和模 $g$ 余 $r$ 的 $j$ 配对。 这样就能够把非互素情形转化为互素情形。 对于互素情形,循环节是容易处理的。 考虑多余的一部分。这时,考虑枚举 $i$,寻找能与 $i$ 配对的 $j$。 记 $n' = n / g, m' = m / g$。 这时,对于一个下标 $i$,能够与它配对的任意 $j$ 与 $kn'+i, k \in \N$ 在模 $m'$ 意义下同余。 ...

December 28, 2025