更新中。
欢迎催更。
Part1 入门杂题
CF1407C Chocolate Bunny
记 (q(i, j)) 表示通过一次询问求出的 (p_imod p_j) 的值。
考虑两个数 (x, y),不妨设 (x < y),那么:(xmod y = x),(y mod x < x)。
也就是说,通过 ((x, y)), ((y, x)) 这样两次询问,我们可以获得的信息是:
- 两个数的大小关系。
- 较小的那个数的值。
我们依次扫描整个序列。维护一个变量 (i) 表示当前前缀最大值所在的位置。初始时 (i = 1)。设当前扫描到的位置为 (i')。通过两次询问 (q(i', i)) 和 (q(i, i')):
- 如果 (q(i', i) > q(i, i')),说明 (p_{i'} < p_{i}),且 (p_{i'} = q(i', i))。
- 如果 (q(i', i) < q(i, i')),说明 (p_{i'} > p_{i}),且 (p_{i} = q(i, i'))。记下 (p_{i}) 的值。然后令 (igets i')。
全部扫描完成后,最终的 (i),就是 (n) 所在的位置。且其他位置的值都已经确定。
共需要 (2n - 2) 次询问。
总结:
核心是要想到对两个数互相询问。即发现 (xmod y) 和 (ymod x) 的关系。
归类:取模,猜数列。
难度:易。
CF1479A Searching Local Minimum
维护一个区间 ([l,r]),满足:
- ([l,r]) 中至少存在一个位置,是局域最小值(也就是答案)。
- (a_{l - 1} > a_{l})。
- (a_{r + 1} > a_{r})。
初始时,(l = 1, r = n)。
如果某个时刻 (l = r),说明我们找到了答案。否则设 (m = lfloorfrac{l + r}{2} floor)。询问出 (a_{m}) 和 (a_{m + 1}) 的值。因为 (a) 是个排列,所以 (a_{m} eq a_{m + 1})。
- 如果 (a_{m} < a_{m + 1}),可以令 ([l, r]gets[l, m])。
- 如果 (a_{m} > a_{m + 1}),可以令 ([l, r]gets [m + 1, r])。
因为区间长度每次缩小一半,这相当于一个二分的过程。共需要 (2cdotlceillog_2(n) ceil leq 34) 次询问。可以通过本题。
总结:
本题里的这个“二分”非常巧妙。一般的二分,我们都试图寻找区间里第一个或最后一个满足某条件的位置,每次我们会判断中点 (m) 相比目标位置是大了还是小了。但在本题里,目标位置并不是唯一的。我们每次判断的是:左区间/右区间里是否一定存在至少一个目标位置,判断的条件是中点 (m) 是否具有和右端点/左端点相同的性质。
更形象地说,我们有一个 (01) 序列 (x)。(x_1 = 0, x_n = 1)。我们的目标是找到一个位置 (p),满足 (x_{p} = 0, x_{p + 1} = 1)。做法是二分,对当前中点 (m),若 (x_{m} = 0)(中点的性质和左端点一样),就令 ([l, r]gets[m, r]),否则(中点的性质和右端点一样)令 ([l, r]gets [l, m])。这样最终一定能找到符合要求的 (p)。
这种独特的二分方法,和微积分上的“区间套”有异曲同工之妙。感兴趣的读者可以自行了解。类似的题目还有:NFLSOJ601 秦始皇。
归类:二分。
难度:易。
CF1479C Continuous City
本题的答案一定是 ( exttt{Yes})。以下会逐步讲解构造方法。
记 ((u, v, w)) 表示一条从点 (u) 连向点 (v),边权为 (w) 的边((1leq u < vleq 32))。
子问题一:(L = 1, R = 2^k)。
(k = 0) 的情况:令 (n = 2),只需要连一条边 ((1, 2, 1)) 即可。
(k > 0) 时,考虑归纳地构造。假设我们已经构造出了一张 (k + 1) 个节点的图,满足对任意 (0leq i < k),点 (1dots i + 2) 构成的子图是 ((1, 2^{i}))-continuous 的。现在考虑添加一个节点 (k + 2),在不影响前面节点的前提下,使得整张图是 ((1, 2^{k}))-continuous 的。做法如下:
- 首先,从 (2dots k + 1) 中任意点连向点 (k + 2) 的路径,长度必定 (> 1)(因为至少含有两条边)。所以为了构造出长度为 (1) 的路径,我们必须从点 (1) 向点 (k + 2) 连一条长度为 (1) 的边,即 ((1, k + 2, 1))。
- 然后依次考虑 (i = 0, 1, dots ,k - 1),连边 ((i + 2, k + 2, 2^{i}))。可以发现,在点 (i + 2) 之前,所有连向点 (k + 2) 的路径,恰好覆盖了 ([1, 2^{i}]) 这些长度。而以点 (i + 2) 为终点的路径,它们覆盖的长度也是 ([1, 2^{i}])。从 (i + 2) 向 (k + 2) 连一条边权为 (w = 2^{i}) 的边,相当于把以点 (i + 2) 为终点的路径覆盖到的长度,平移了 (w) 位(([1, 2^i] o [2^{i} + 1, 2^{i + 1}])),于是现在所有连向 (k + 2) 的路径,就恰好覆盖了 ([1, 2^{i}]cup[2^{i} + 1, 2^{i + 1}] = [1, 2^{i + 1}])。最终,扫描完 (i = k - 1) 后,覆盖到的区间就恰好是我们需要的 ([1, 2^{k}]) 了。
于是我们对 (L = 1, R = 2^k) 的情况完成了构造。
子问题二:(L = 1),(R) 任意。
设 (k = lfloorlog_2(R - 1) floor)。
用“子问题一”部分的方法,我们可以先构造出一张 (k + 2) 个节点的图,满足对任意 (0leq ileq k),点 (1dots i+ 2) 构成的子图是 ((1, 2^{i}))-continuous 的。
新添加一个节点 (n = k + 3)。根据前面的分析,我们需要先连一条边 ((1, n, 1))。
设 (01) 序列 (r_{0dots k}) 为 (R - 1) 的二进制分解。即:(R - 1 = sum_{i = 0}^{k} r_icdot 2^i)((0leq r_ileq 1))。
对每个 (i)((0leq ileq k)),如果 (r_i = 1),那么我们连一条边:((i + 2, n, 1 + sum_{j = 0}^{i - 1} r_j cdot 2^{j}))。其中边权 (w = 1 + sum_{j = 0}^{i - 1} r_j cdot 2^{j}) 表示前面已经连出的路径,覆盖到的长度恰好是 ([1, w])。新的路径不能和它们重叠,所以要把自己的长度平移 (w),即 ([1, 2^{i}] o [w + 1, w + 2^{i}])。
那么最终,我们就能恰好覆盖长度 ([1, R])。
子问题三:(L > 1),(R) 任意。
先用“子问题二”部分的方法,构造出一张 ((1, R - L + 1))-continuous 的图。设用了 (n') 个节点((n' = lfloorlog_2(R - L) floor + 3))。
在图里新建一个节点 (n = n' + 1)。
连边:((n', n, L - 1))。相当于把前 (n') 个点构成的子图里,所有路径长度平移了 (L - 1)。原来覆盖到的区间是 ([1, R - L + 1]),现在覆盖到的区间就恰好是 ([L, R]) 了。
最多使用了 (lfloorlog_2(R - L) floor + 4 = 23) 个节点,可以通过本题。
总结:
看到点数 (nleq 32),要联想到二进制分解。直接思考有些困难,所以按照 ([1, 2^k] o [1, R] o [L, R]) 这样的思路,有条有理地分析。此外,在 ([1, 2^k]) 的部分,我们用了“归纳地构造”,这是需要掌握的一个重要技巧。
归类:二进制分解,归纳。
难度:中。
CF1485D Multiples and Power Differences
观察到 (a(i,j)) 不超过 (16),是个很小的数字,并且有 (operatorname{lcm}(1,2,dots,16) = 720720leq 10^6)。
记第 (i) 行第 (j) 列的格子为 ((i, j)),它的答案为 (mathrm{ans}(i, j))。则答案可以按如下方法构造:
容易发现,满足题目的所有要求。
总结:
在网格上,限制条件和“相邻位置”有关,一般可以考虑把格子按 (i + j) 的奇偶性分为两类。这样,任意相邻的两个格子一定来自不同的类。
归类:网格,奇偶性。
难度:易。
CF1470C Strange Shuffle
观察发现,内鬼的影响会逐轮向两侧扩散。
具体来说(以下说的“递增”、“递减”都是指非严格的,即可能存在等于):
-
对于 (1leq ileqlfloorfrac{n - 1}{2} floor),在第 (i) 轮后,内鬼左边的 (i) 个数都小于 (k),且从内鬼开始向左递增;内鬼右边的 (i) 个数都大于 (k),且从内鬼开始向右递减。内鬼始终等于 (k),距离内鬼超过 (i) 的数还未被影响到,所以也等于 (k)。
-
对于 (i > lfloorfrac{n - 1}{2} floor),在第 (i) 轮后序列里所有数都被影响过了。此时序列里最大的数就是内鬼右边的数,从它开始向右依次递减,在内鬼的对面减到 (k),然后继续递减,直到到达内鬼的左边,内鬼的左边就是序列里最小的数。内鬼始终保持为 (k)。特别地,(n) 为奇数时,“对面”其实是两个数中间的位置,故可以理解为两个数一个大于等于 (k),一个小于等于 (k)。
暴力做法:先在任意位置询问 (1) 次。在接下来的任意时刻,序列里至少有 (1) 个大于 (k) 的数,它在内鬼的右侧;也至少有一个小于 (k) 的数,它在内鬼的左侧。用 (n) 次询问,暴力找到这样两个数。然后在它们之间二分出内鬼的位置。操作次数:(1 + n + lfloorlog_2(n) floor)。
当 (nleq 900) 时,我们就使用上述做法,这可以避免讨论很多特殊情况。下面考虑 (n) 较大时:
观察操作次数的限制:(1000)。容易联想到根号做法。设 (B = lfloorsqrt{n} floor)。先在任意位置询问 (B) 次。在接下来的任意时刻,序列里总存在连续的 (B) 个大于 (k) 的数,和连续的 (B) 个小于 (k) 的数。我们每隔 (B) 个位置询问一次,就一定能找出这样的两个数。然后再和上面一样二分答案。操作次数:(B + lceilfrac{n}{B} ceil + lfloorlog_2(n) floor approx 2sqrt{n} + log_2(n)),可以通过。
总结:
一开始要多观察和分析,不要被题面里的上取整、下取整搞晕,可以借助程序,就能发现整个过程的规律其实是简洁优美的,细节并不繁琐。
然后分块和二分,都是交互题里比较经典的做法。
归类:分块,二分。
难度:中。
CF1100D Dasha and Chess
先将白棋移到棋盘正中央,也就是 ((500, 500)) 的位置,这需要花费不超过 (499) 步。
如果此时有黑棋也在第 (500) 行或第 (500) 列,那么直接获胜。排除这种情况后,现在整个棋盘被我们划分为左上方、右上方、左下方、右下方四个部分,每个部分大小均为 (499 imes 499)。设四个部分内各有 (a, b, c, d) 枚黑棋,则 (a + b + c + d = 666)。
我们考虑挑选一个方向,沿着对角线走过去。例如,如果选择向左上方走,我们就会从 ((500, 500)) 走到 ((1, 1))。我们一共会走 (499) 步。称所选方向以及与它相邻的两个方向所对应的区域,为被我们“覆盖到”的区域(例如,与左上方相邻的是右上方和左下方。如果向左上方走,那么这三个区域都是被我们覆盖到的)。
被覆盖到的区域里的黑棋,如果全程没有被移动(全程:指从白棋位于 ((500, 500)) 开始算起,直到白棋沿着对角线走到某个角落为止),那么必定会在某个时刻和白棋共行或共列,这样我们就赢了。
现在我们要证明,通过合理地挑选方向,一定能使我们覆盖到的区域里,黑棋数量之和 (> 499),这样在我们扫描完之前,对手来不及把所有黑棋都移出去。
因为 (a + b + c + d = 666),根据鸽巢原理可知,(min{a, b, c, d}leq lfloorfrac{666}{4} floor = 166)。因此如果我们选择一个方向,使得所覆盖的三个区域内的黑棋数量之和最大,这个最大值一定 (geq 666 - 166 = 500 > 499)。
于是我们找到了必胜策略,且可以在 (499 + 499 leq 2000) 步以内完成。
值得注意的是,如果在移动过程中,下一步要去的位置上已有一枚黑棋,那不能直接走上去。但此时我们一定可以在一步以内直接取胜(走到一个和它共行或共列的点)。
总结:
通过直觉或灵感,首先想到走到中间。然后使用鸽巢原理。
归类:网格,鸽巢原理。
难度:中。
CF1081F Tricky Interactor
本题最困难的地方是,操作是不会还原的,而我们并不知道它反转了哪一边。于是我们试图发现一些性质,来判断它反转的是哪一边。
考虑一次反转产生的影响。设反转的区间长度为 (L),这个区间里原有 (k) 个 (1)。那么,反转后区间里会有 (L - k) 个 (1)。考虑 (1) 的变化量,是 (|k - (L - k)| = |L - 2k|)。于是可以得到一个重要结论:反转后【区间里 (1) 的变化量】与【区间长度】奇偶性相同。
那么,如果操作 ([l, r]) 满足:([1, r]) 和 ([l, n]) 长度的奇偶性不同,我们不就能判断反转的是哪一边了吗?!
如果每次都能知道反转的是哪一边,我们只要写一个 ( exttt{while}) 循环,就可以实现各种想要的反转效果。举个例子,我们想要把 ([1, r]) 反转。用三个变量 (x, y, z) 分别表示 ([1, l), [l, r], (r, n]) 这三个区间有没有被反转过。初始时 (x = y = z = 0)。我们想要的最终效果是 (x = 1, y = 1, z = 0)(也就是反转 ([1, r]))。每次进行一个操作(随机反转 ([1, r]) 或 ([l, n])),操作后通过 (1) 的变化量的奇偶性,判断反转的是哪一边:如果是 ([1, r]),则改变 (x, y) 的值,否则改变 (y, z) 的值。如果达到最终效果就 ( exttt{break}),否则继续操作。直觉上,所需的操作次数应该是很小的常数。可以证明,期望操作次数为 (3)。
证明:期望操作次数为 $3$
设 (E(x, y, z)) 表示从状态 ((x, y, z)) 达到最终状态 ((1, 1, 0)) 的期望操作次数。则有转移式:
- (E(1, 1, 0) = 0)。
- (E(x, y, z) = frac{1}{2}(E(xoperatorname{xor}1, yoperatorname{xor}1, z) + E(x, yoperatorname{xor}1, zoperatorname{xor}1)) + 1)。
自己写一个高斯消元,不难解得:(E(0, 0, 0) = 3)。
利用上述的分析来构造方案。考虑三种情况:
- 如果 (n = 1),输入的数就是答案,直接输出即可。
- 如果 (n) 是偶数,那么对于所有 (i),([1, i]) 与 ([i, n]) 长度的奇偶性不同,于是可以用上述的方法实现【把 ([1, i]) 反转】。具体来说,我们从小到大遍历所有位置 (i)。先将 ([1, i]) 反转。通过【原序列里 (1) 的数量】和【反转后整个序列里 (1) 的数量】,可以计算出原序列 ([1, i]) 里 (1) 的数量。减去上一步算出的 ([1, i - 1]) 里 (1) 的数量,就知道第 (i) 位的答案了。最后把 ([1, i]) 再反转一次,将序列还原回去。再继续考虑下一个位置。如此可以求出所有位置的答案。
- 如果 (n) 是大于 (1) 的奇数,那么对于所有 (igeq 2),([1, i]) 与 ([i - 1, n]) 长度的奇偶性不同。如果知道了位置 (1) 的答案,那么可以用类似的方法可以依次推出 (2dots n) 的答案(也就是每次对区间 ([i - 1, i]) 操作,实现反转 ([1, i]))。考虑求位置 (1) 的答案,可以对区间 ([2, n]) 操作,实现反转 ([2, n]),从而可以算出位置 (1) 的答案。
因为每次“反转 ([1, i])”期望需要 (3) 次操作,反转后我们还要还原,所以总共期望需要 (6n) 次操作,可以通过本题。
总结:
首先分析:因为操作不还原,如果无法确定反转的是哪一边,那么是很难做的。于是我们试图发现一些性质,来判断它反转的是哪一边。进而我们分析出了奇偶性的这个性质。利用这个性质,不难构造出操作的方法。
归类:奇偶性。
难度:中。
CF1188A2 Add on a Tree: Revolution
请注意重要条件:所给局面中,边权都是偶数,且互不相同。
引理:有解当且仅当不存在度数为 (2) 的点。
必要性是很显然的。因为如果存在度数为 (2) 的点,考虑它连接的两条边,每次操作,这两条边要么都不被经过,要么一起被经过。所以最终局面下,这两条边的边权一定相同。又因为题目所给局面里边权互不相同,矛盾了。所以:若有解,一定不存在度数为 (2) 的点。
充分性,我们通过构造来证明。
首先,对于任意叶子节点 (u)、任意其他节点 (v)、以及任意偶数 (x),可以实现一种基本操作是:给 ((u, v)) 路径上所有边边权加上 (x),且不改变其他边的边权。具体做法是:
- 如果 (v) 也是叶子节点,一步操作即可实现。
- 如果 (v) 不是叶子节点,那么 (v) 度数至少为 (3)。不妨以 (v) 为根。考虑 (v) 除了包含 (u) 的儿子外的另外两个儿子,记为 (s_1, s_2)。从 (s_1, s_2) 的子树里各取一个叶子,记为 (l_1, l_2)。那么可以通过如下三次操作,实现我们想要的效果:
- 对 ((l_1, u)) 的路径加上 (frac{x}{2})。
- 对 ((l_2, u)) 的路径加上 (frac{x}{2})。
- 对 ((l_1, l_2)) 的路径加上 (-frac{x}{2})。
有了上述基本操作后,可以这样构造出本题的解法:取一个度数为 (1) 的节点为根,记为 (r)。从 (r) 出发 dfs。具体来说,我们实现一个函数:( ext{solve}(u)),它的任务是,通过操作使得 (u) 子树内所有边边权都变为 (0),且可以任意改变 (u) 到根路径上的边权,但不允许改变 (u) 子树外其他边的边权。同时,要保证任意时刻所有边边权均为偶数。
考虑 (u) 的每个儿子 (v)。先调用 ( ext{solve}(v)),那么此时 (v) 的子树已经被解决了,之后我们不会再去碰它。考虑 ((u, v)) 这条边此时的边权,记为 (x)((x) 一定是偶数)。我们通过上述基本操作,令 (r) 到 (v) 路径上所有边边权加上 (-x)。这样使得 ((u, v)) 的边权变为 (0),且 (u) 到根路径上的边权仍然都是偶数。
把上述的【将所有边边权变为 (0) 的】的过程反过来(加变成减,减变成加),就是答案了。
朴素的实现方法,就是在操作时暴力更改所有祖先的边权,时间复杂度 (mathcal{O}(n^2))。也可以用树上差分来优化,时间复杂度 (mathcal{O}(n))。
总结:
一开始不知道有解的条件,我们就先蒙一个显然的必要条件。然后在满足这个条件的前提下,尝试去构造解法。如果构造出来了,说明该条件也是充分的。
首先构造出一个基本操作,它像是我们的砖头,我们用它来盖大房子。
树上的问题,要注意到树本身特有的、“递归式”的结构,以子树作为天然的子问题,递归地解决。
归类:树,递归。
难度:中。
GYM102392C Find the Array
第一步:找出最大或最小值的位置。
先询问下标集合 ([1, n])。得到的答案集合里的最大值,显然是序列中的 ( ext{最大值} - ext{最小值}),即 (max{a_i} - min{a_i}),记为 (D)。
接下来考虑对某个 (i),询问下标集合 ([1, i])。记得到的答案集合里的最大值为 (d)。显然 (dleq D)。并且如果 (d < D),说明原序列的最大、最小值至少有一个下标大于 (i)。
根据这个观察,可以二分求出最大或最小值的位置,记为 (p)(此时只知道 (p) 上是最大或最小值,但具体是最大还是最小我们并不清楚)。
然后我们试图确定,位置 (p) 上究竟是最大值还是最小值。任取一个其他位置 (q) ((q eq p)),询问 (a_p), (a_q) 后,比较它们的大小关系即可。
至此一共使用了 (1 + lfloorlog_2(n) floor + 2leq 10) 次询问,找出了最大或最小值的位置,并且知道了它是最大值还是最小值。
第二步:二进制分组。
因为 (a_p) 已知,所以我们只要求出每个数和 (a_p) 的差,就相当于求出了这个数的值(这里 (a_p) 是最大或最小值的好处就体现在,绝对值符号不会困扰我们了)。
对一个下标的集合 (S) ((p otin S)),可以用 (2) 次询问求出里面的数的集合 (A(S) = {a_i | i in S}):
- 先询问 (S),记答案集合为 (a)。
- 再询问 (S cup{p}),记答案集合为 (b)。则 (A(S) = bsetminus a),也就是从 (b) 里把所有 (a) 中的数扣除一次后,得到的集合。
特别地,如果 (pin S),我们先用上述方法对 (Ssetminus {p}) 询问,然后在结果里加入 (a_p) 即可。
接下来我们把下标按二进制分组,然后对每组用上述方法询问。具体来说,对所有二进制位 (j) ((0leq jleq lfloorlog_2(n) floor)),用两次询问,求出所有【下标第 (j) 位上为 (1)】的数的集合,记为 (F(j) = {a_i | i operatorname{and} 2^j eq 0})。
因为每个下标 (i) 的二进制表示是唯一的,这意味着,对所有 (i),(a_i) 被分配到的集合互不相同。具体来说,假设 (i) 在 (u_0, u_1, dots, u_x) 这些二进制位上为 (1),(v_0, v_1, dots, v_{y}) 这些二进制位上为 (0),那么 (a_i) 一定在所有 (F(u_0), F(u_1), dots, F(u_x)) 里出现过,并且在 (F(v_0), F(v_1), dots, F(v_y)) 里都没有出现过,并且这样的数有且仅有一个(如果存在第二个,说明它们下标的二进制表示完全相同),找出这个数,它就是 (a_i)。
对每个二进制位都要问两次。所以所需的询问次数是:(lfloorlog_2(n) floorcdot 2 leq 16) 次。
总询问次数不超过 (10 + 16 = 26) 次。
时间复杂度很松,朴素的 (mathcal{O}(n^3log n)) 实现即可通过。
总结:
因为绝对值不好处理,所以想到先求出最大或最小值。然后就可以实现查询一个集合。于是常用的技巧是把数按二进制编码,分成若干个集合,分别查询即可。
归类:二分,二进制编码,猜数列。
难度:中。
Part2 wzy 课件
CF1375H Set Merging
朴素的做法是,把区间 ([l_i, r_i]) 里的数值从小到大排序,依次合并。共需要 (mathcal{O}(qn)) 次合并,太多了。
优化上述做法,考虑对值域分块。设块的大小为 (B),分出了 (lceilfrac{n}{B} ceil) 个块。我们在每一块内,把元素按照原序列里的出现位置排序。也就是说,每个块都是原序列的一个子序列。
考虑查询 ([l_i, r_i]) 时,我们从小到大遍历所有块,把每一块里位置在 ([l_i, r_i]) 内的元素提取出来,显然每一块里提取出的一定是一段连续的元素。假设它们的集合是已知的,那么只需要把所有块对应的集合依次合并即可。单次询问需要 (mathcal{O}(frac{n}{B})) 次合并操作。在每个块里提取区间时,可能需要二分,所以总时间复杂度是 (mathcal{O}(qfrac{n}{B}log B))。
那么问题转化为,预处理出每个块内所有区间对应的集合。这样的区间共有 (mathcal{O}(frac{n}{B}cdot B^2) = mathcal{O}(nB)) 个。
每个块单独预处理。在值域上分治。具体来说,我们会发现,原序列的一段区间,在某一段值域里的数,可以表示为该区间的一个子序列。在分治的第一层,这个子序列就是我们的当前块,设它为 (p_1, p_2, dots ,p_{|p|})。下面把值域一分为二:([l, m], [m + 1, r])。那么子序列 (p),又会对应地划分为两个子序列:(u_1, u_2, dots ,u_{|u|}) 和 (v_1, v_2, dots, v_{|v|})。其中 (lleq a_{u_i}leq m),(m < a_{v_i}leq r),并且 (|u| = m - l + 1),(|v| = r - m),(u cup v = p)。
我们有 (mathcal{O}(B^2)) 个区间需要求答案(这个“答案”就是指构造出的一个集合)。递归下去。对每个区间,求出它在 (u) 序列上的答案,和在 (v) 序列上的答案,然后用一次操作合并起来,就能得到它在 (p) 序列上的答案。
朴素实现所需的操作次数是 (mathcal{O}(Blog Bcdot B^2)),因为总共递归地调用了 (mathcal{O}(Blog B)) 次函数,每次对 (mathcal{O}(B^2)) 个集合更新答案。这样太多了。考虑某个需要求答案的区间 ([i, j])(注意这里 (i, j) 指的是位置,即原序列里的下标,而不是数值。(l, r, m) 这些都是数值,我们在值域上做的分治),它在 (p) 序列上的答案,等同于区间 ([p_{i'}, p_{j'}]) 的答案。其中 (p_{i'}) 是 (p) 序列里第一个 (geq i) 的元素,(p_{j'}) 是 (p) 序列里最后一个 (leq j) 的元素。因此我们只需要对 (mathcal{O}(|p|^2)) 个区间求答案(而不是原来的 (mathcal{O}(B^2)) 个)。分析现在的操作次数,设某次调用分治函数时,(r - l + 1 = L),则操作次数为 (T(L) = 2T(frac{L}{2}) + mathcal{O}(L^2)),解得 (T(L) = mathcal{O}(L^2))。所以现在操作次数被优化到 (mathcal{O}(B^2)),可以接受。
还有一个小问题就是找 (i', j')。可以用主席树查询,但没必要。我们从前往后、从后往前扫描两遍 (p) 序列,就能对所有 (i,j in p),推出它们对应的 (i', j')。所以整个分治的时间复杂度也是 (mathcal{O}(B^2))。
对每个块都预处理一次,总共所需操作次数和总时间复杂度都是:(mathcal{O}(frac{n}{B}cdot B^2) = mathcal{O}(nB))。
综上,分析一下所需的操作次数,是 (mathcal{O}(nB + qfrac{n}{B}))。显然取 (B = sqrt{q} = 2^{8}) 时是最优的。操作次数是 (mathcal{O}(nsqrt{q}))。
时间复杂度 (mathcal{O}(nB + qfrac{n}{B}log B) = mathcal{O}(nsqrt{q}log(sqrt{q})))。
总结:
上述做法的本质是:将一个序列按值域范围划分成若干子序列之后,原序列的每个连续区间都可以表示成划分成的子序列中的连续区间的并。
分块和分治都是对这样本质的具体实现(分出的块是这样的子序列,分治时处理的 (p) 数组也是这样的子序列)。将它们结合起来,就能得到最优的做法。
归类:分块,值域分块。
难度:难。
CF1364E X-OR
设 (s) 为可能涉及到的最大二进制位,即:(s = lfloorlog_2(n - 1) floorleq 10)。
解题的核心是:找出 (p_i = 0) 的位置 (i),然后把每个位置都和 (i) 问一遍,即可得到答案。
如何找到 (0) 所在的位置?有如下的一些方法:
法一:
考虑实现这样一个函数 (f),传入任意一个位置 (i),它能问出 (p_i) 的值。
为了实现这个函数 (f),我们构造一个序列 (z_0, z_1, dots, z_{s}),满足 (p_{z_j}) 的二进制第 (j) 位为 (0)。有了 (z) 序列后,函数 (f) 就很好实现了,它返回的结果就是 (q(i, z_0)operatorname{and}q(i, z_1)operatorname{and}dots operatorname{and}q(i, z_s)),其中 (q(x, y)) 表示用一次询问,问出的 (p_xoperatorname{or} p_y) 的值。这是因为,如果 (p_i) 的第 (j) 位为 (1),那么返回的结果第 (j) 位一定是 (1);如果 (p_i) 的第 (j) 位为 (0),那么 (q(i, z_j)) 的第 (j) 位是 (0),所以返回的结果第 (j) 位也是 (0)。调用一次函数 (f),需要 (s + 1) 次询问。
下面问题转化为如何构造 (z) 序列。可以每次随机两个位置 (x, y)((x,yin[0, n - 1], x eq y)),询问 (p_xoperatorname{or} p_y) 的值。对结果里所有为 (0) 的二进制位 (j),令 (z_jgets x)(或 (y))即可。因为任意二进制位 (j) 都有至少 (frac{n}{2}) 个数这位为 (0),所以期望 (4) 次就能随出 (z_j),总共 (40) 次询问就能得到 (z) 序列(实际是小于这个数的,因为每次询问可以更新多个二进制位)。
有了 (z) 序列后,如果暴力问出每个 (p_i),总询问次数是:(40 + ncdot(s + 1)),无法通过。
考虑找出 (p_i = 0) 的位置 (i)。初始时,先令 (i = 0),并且通过函数 (f) 问出 (p_0) 的值。然后依次考虑每个位置,设当前考虑到的位置为 (i')。用一次询问,查询 (p_{i}operatorname{or}p_{i'}),如果结果等于 (p_i),说明 (p_{i'}) 是 (p_i) 的子集。此时令 (igets i'),并通过函数 (f) 暴力问出新的 (p_i) 的值。扫描完所有位置后,最终的 (i) 就是我们要找的 (p_i = 0) 了。
因为每次 (i) 变化时,(p_{i'}) 都是 (p_i) 的子集且 (p_{i'} eq p_i),所以至少减少一个二进制位,也就是 (i) 最多变化 (s) 次。因此总共调用不超过 ((s + 1)) 次函数 (f)。另外,每次查询 (p_{i}operatorname{or}p_{i'}) 还需要 (n - 1) 次询问。
最后,让 (i) 和其他所有位置都问一遍((n - 1) 次询问),求出答案。
总共需要的询问次数是 (40 + (s + 1)^2 + (n - 1) + (n - 1) = 4257) 次。可以通过本题。
法二:
依次考虑所有位置。维护两个位置 (a, b),表示在当前扫描到的前缀里,(0) 只可能在位置 (a) 或位置 (b) 上。并且记录下 (p_a operatorname{or} p_b) 的值。设当前位置为 (c),则有如下情况:
- 若 (p_a operatorname{or} p_c > p_a operatorname{or} p_b),则 (p_c) 不可能是 (0)。
- 若 (p_a operatorname{or} p_c < p_a operatorname{or} p_b),则 (p_b) 不可能是 (0)。我们把 (b) 踢掉,把 (c) 加入。
- 若 (p_a operatorname{or} p_c = p_a operatorname{or} p_b),则 (p_a) 不可能是 (0)(否则 (p_b = p_c),不合题意)。我们把 (a) 踢掉,把 (c) 加入。
每次需要询问 (p_a operatorname{or} p_c)。另外,如果是情况 3(把 (a) 踢掉了),相当于把 (c) 作为新的 (a),所以还需要询问出新的 (p_a operatorname{or} p_b) 的值。所以最多可能要 (2n) 次询问。
求出最终的 (a, b) 后,随机一些位置 (t)。若 (p_a operatorname{or} p_t eq p_b operatorname{or} p_t),就能知道哪个位置是 (0) 了。也就是说,我们要随出一个位置 (t),使得 (p_t) 不是 (p_b) 的超集。那么考虑 (p_b) 里任意一个为 (1) 的二进制位,我们只需要随出一个 (p_t) 这一位上是 (0) 就可以了。因为每一位为 (0) 的数至少有 (frac{n}{2}) 个,所以期望只需要随机 (2) 次。
最后,知道了 (0) 的位置后,还要和每个数问一遍,求出答案。所以上述做法需要 (3n + 2) 次询问,无法通过。
不过,我们可以以随机顺序访问所有位置。这样每次加入 (c) 时,情况 3 发生的概率是很小的。设情况 3 发生的次数为 (k),则所需询问次数是 (2n + 2 + k)。可以通过本题(因为 (k) 是按我们随机的顺序来的,所以和题目的输入无关。因此你只需要自己随机几次,就可以验证了)。
暂无参考代码。
总结:
核心是要找出 (p_i = 0) 的位置 (i)。两种方法,分别是利用了 (operatorname{and}) 和 (operatorname{or}) 运算的特性。还是挺巧妙的。
归类:位运算,猜数列。
难度:中。
CF1365G Secure Password
每次询问,可以查询出一个集合里所有数的按位或。考虑通过某种构造方法,使得对每个位置 (i)((1leq ileq n)),都能选出若干(被询问过的)集合,满足:(i) 不在这些集合里,除 (i) 外的所有位置都被包含在至少一个集合里。如果能构造出一组满足上述条件的集合(不超过 (13) 个),那么本题就做完了。
考虑使用二进制编码。第一反应有一个较为简单的做法:
-
因为 (nleq 1000),那么每个位置 (i) 都可以对应一个 (10) 位二进制数(可以有前导零)。
-
对每个二进制位,我们把所有这位是 (0) 的位置的按位或值问出来;所有这位是 (1) 的位置的按位或值问出来。
-
查询位置 (i) 的答案,对每个二进制位 (j)((0leq j < 10)),设 (i) 的第 (j) 位为 (t),那么就把答案或上第 (j) 位与 (t) 相反的那个集合。
-
正确性:
- 显然位置 (i) 不会被选中的集合包含。因为是按每一位取反选的,所以所选集合里的数,至少有一位与 (i) 不同。
- 所有 ( eq i) 的位置都会被包含在至少一个集合里。因为其他位置至少会有一位与 (i) 不同。
-
上述做法所需的询问次数是 (20) 次。无法通过本题,需要进一步优化。
上述做法所需的询问次数太多了。下面有一个更妙的做法:
- 把所有 (13) 位的、恰好有 (6) 个 (1)的二进制数拿过来,作为编码。因为 ({13choose 6} > 1000geq n),因此一定可以使得:每个位置 (i) 唯一对应一个编码。
- 对每个二进制位 (j)((0leq j < 13)),把编码的第 (j) 位上是 (1) 的位置拿出来,询问他们的按位或,记为 (w_j)。
- 查询位置 (i) 的答案。对每个二进制位 (j)((0leq j < 13)),如果 (i) 的编码的第 (j) 位为 (0),那么就令答案或上 (w_j)。
- 正确性:
- 显然位置 (i) 不会被包含。
- 所有 ( eq i) 的位置,至少存在某一位,使得它的编码这一位上是 (1) 而 (i) 的编码这一位上是 (0)。这是因为我们保证了所有编码里 (1) 的个数相同。
- 需要 (13) 次询问,可以通过本题。
归类:二进制编码。
难度:中。
CF1290D Coffee Varieties (hard version)
以下仅讨论较为一般的情况((1 < k < n))。其他情况留给读者自行特判。
朴素暴力:
要对元素去重,可以考虑判断每一对元素是否相同。具体来说,对于所有二元组 ((i, j))((1leq i < jleq n)),先清空队列,然后把 (i) 加入队列,再把 (j) 加入队列。如果 (j) 和 (i) 两元素相同,就把 (j) 打上“死亡标记”。最后答案就是没有被打上死亡标记的元素数量。
上述做法太暴力了。考虑不清空队列。对所有二元组 ((i_1, j_1), (i_2, j_2), dots, (i_{frac{n(n - 1)}{2}}, j_{frac{n(n - 1)}{2}})),依次将元素:(i_1, j_1, i_2, j_2, dots, i_{frac{n(n - 1)}{2}}, j_{frac{n(n - 1)}{2}}) 加入队列。加入一个数时,若队列里存在和它相同的数,就把该数打上死亡标记。但这样(不清空队列)会带来一个问题:可能队列里和它相同的那个数就是它自己!那么我们可能就把某个数值的唯一一次出现给删了。
为了避免自己把自己删掉的情况,我们不得不使用一些清空操作。考虑这样一个问题:一张 (n) 个点的有向图,对所有 (i),从点 (i) 向点 (i + 1, i + 2, dots, n) 连边。求将图划分为尽量少的路径,使得每条边恰好出现在一条路径中。我的构造方法是:考虑枚举间隔 (d = 1, 2, dots, n - 1)。
- 所有【端点间隔为 (1) 的边】只需要 (1) 条路径就能串起来。
- 所有【端点间隔为 (2) 的边】需要划分为 (2) 条路径:起点为 (1) 的路径和起点为 (2) 的路径。
- ......
- 所有【端点间隔为 (frac{n}{2}) 的边】需要划分为 (frac{n}{2}) 条路径。
- 所有【端点间隔为 (frac{n}{2} + 1) 的边】需要划分为 (frac{n}{2} - 1) 条路径,因为起点为 (frac{n}{2}) 时就没有【端点间隔为 (frac{n}{2} + 1) 的边】了。
- ......
- 所有【端点间隔为 (n - 1) 的边】需要划分为 (1) 条路径。
总路径数是:(frac{(frac{n}{2} + 1)frac{n}{2}}{2} + frac{frac{n}{2}(frac{n}{2} - 1)}{2} = frac{n^2}{4})。
那么需要的清空次数也是 (frac{n^2}{4})。询问次数 = 总边数 + 划分出的路径数 = (frac{n(n - 1)}{2} + frac{n^2}{4})。无法通过本题。
分块暴力:
上述做法难以通过,是因为没有充分利用队列长度为 (k) 的特点。
我们考虑分块:每 (frac{k}{2}) 个数分为一块,分出 (frac{2n}{k}) 块。
把任意一个块里的所有元素加入队列(队列里有相同元素就打死亡标记),相当于实现了块内去重。下面考虑不同块之间的去重。暴力枚举一对块 ((i, j))((1leq i < jleq frac{2n}{k})),把队列清空,然后将两个块依次加入队列。
所需的清空次数,即块的无序对数,为 (frac{frac{2n}{k}(frac{2n}{k} - 1)}{2} = frac{2n^2}{k^2} - frac{n}{k} leq frac{2n^2}{k}leq 20000)。
所需的询问次数,是无序对数乘以 (k),即 (frac{frac{2n}{k}(frac{2n}{k} - 1)}{2}cdot k = frac{2n^2}{k} - n)。可以通过本题的 easy version。
结合一下:
我们发现,分块做法在处理二元组 ((i,i + 1), (i, i + 2), dots,(i, n)) 时,都要先清空队列,再重新加入第 (i) 块。这样是非常亏的。
考虑把分块和“朴素暴力”里的做法相结合。即,不用每次都只加入一个二元组,然后清空。我们把二元组看做边,那么可以每次加入一条路径,然后再清空。
在朴素暴力部分,我们知道,一张 (n) 个节点的图,有 (frac{n(n - 1)}{2}) 条边,可以划分成 (frac{n^2}{4}) 条路径。现在通过分块,我们把节点数压缩到了 (frac{2n}{k})。所以边数是 (frac{frac{2n}{k}(frac{2n}{k} - 1)}{2} = frac{2n^2}{k^2}-frac{n}{k}),划分出的路径数是 (frac{n^2}{k^2})。
每条边,以及每条路径的起点,都需要 (frac{k}{2}) 次询问操作(即把一个块加入队列)。所以需要的询问操作数是:(frac{k}{2}(frac{2n^2}{k^2}-frac{n}{k} + frac{n^2}{k^2}) = frac{3n^2}{2k} - frac{n}{2})。可以通过本题。
更牛一点:
发现在上述的,从 (i) 向 (i + 1, i + 2, dots ,n) 连边的有向图中,我们已经难以构造出更优的划分方案。
不妨退一步,把图扩充一下,变成完全图,即 (i) 向所有 (j eq i) 连边。另外注意,此时划分出的每条路径,一定不能多次经过同一个节点,否则可能出现“队列里和它相同的那个数就是它自己”(造成误删除)的情况。在原来的图上不会有这种情况,因为每个节点都无法走到自己。
完全图的性质更好,有更漂亮的划分方法:之字形划分(官方题解中称为 zig-zag pattern)。枚举所有起点 (s)((s = 1, 2, dots n))。走出一条不经过重复点的路径:(s o (s - 1) o (s + 1) o (s - 2) o (s + 2),dots)。可以理解为把点排成一圈。手动模拟一下,发现这样每条边恰好被覆盖一次。
并且由于扩充为了有向图,我们可以让块的大小从 (frac{k}{2}) 变成 (k)。因为任意两个块正着反着都会被拼一次,所以效果和原来是一样的。这样边数和路径数,分别被优化为了 (frac{n^2}{k^2} - frac{n}{k}) 和 (frac{n}{k})。总询问次数是:(k(frac{n^2}{k^2} - frac{n}{k} + frac{n}{k}) = frac{n^2}{k})。非常优秀。
总结:
首先,分块是一个常见的优化。然后要建立图论的模型,这需要对问题性质的深入分析。最后还要在图上构造出划分方案,比较考验构造能力。
归类:分块,图划分。
难度:难。
CF1292E Rin and The Unknown Flower
暴力做法:花费 (2) 问 ( exttt{C}, exttt{O}),剩下的位置就是 ( exttt{H})。
这种做法给了我们一些启发,事实上可以稍微加长一下询问的串长。
我们询问 ( exttt{CC}, exttt{CH}, exttt{CO})。这样就能以 (3 imes frac{1}{2^2} = 0.75) 的代价,知道(除最后一个位置外)所有 ( exttt{C}) 出现的位置。
同理,我们还想知道所有 ( exttt{O}) 出现的位置。不过考虑到前面已经问过一次 ( exttt{CO}),所以只需要再来两次询问 ( exttt{HO}, exttt{OO}),就能知道(除第一个位置外)所有 ( exttt{O}) 出现的位置。
至此一共使用了 (5) 次询问(( exttt{CC}, exttt{CH}, exttt{CO}, exttt{HO}, exttt{OO})),代价为 (5 imes frac{1}{2^2} = 1.25)。串里所有还不确定的位置,除第一个和最后一个位置外,它的字符一定是 ( exttt{H})。于是我们已知了除第一个和最后一个位置外的所有字符。
接下来处理第一个和最后一个位置(如果它们仍然未知的话)。第一个位置只可能是 ( exttt{O}, exttt{H}),最后一个位置只可能是 ( exttt{C}, exttt{H})。共有 (2 imes 2 = 4) 种可能。我们只需要进行 (3) 次长度为 (n) 的询问(如果都不成功,则必然是最后一种,不需要问了)。
至此花费的总代价为 (1.25 + frac{3}{n^2})。在 (n > 4) 时可以成功。
接下来单独处理 (n = 4) 的情况。
仍然先询问 ( exttt{CC}, exttt{CH}, exttt{CO})。如果至少有一次出现,那么至多只剩两位不确定。且前 (3) 位(如果还不确定的话)不可能是 ( exttt{C}),所以至多只有 (2 imes 3 = 6) 种可能,只需要 (5) 次询问。花费的总代价是:(3 imes frac{1}{2^2} + 5 imes frac{1}{4^2} = 1.0625)。
如果 ( exttt{CC}, exttt{CH}, exttt{CO}) 都没有出现过,再询问 ( exttt{HO})。如果它出现过,可以用类似的方法,以 (4 imes frac{1}{2^2} + 5 imes frac{1}{4^2} = 1.3125) 的代价求出答案。
如果 ( exttt{CC}, exttt{CH}, exttt{CO}, exttt{HO}) 都没有出现过,再问 ( exttt{OO})。如果 ( exttt{OO}) 出现过,那么目前已知的串,可能有如下三种情形:
- ( exttt{OOOO})。此时答案已知。
- ( exttt{OOO*})。即只有第 (4) 位未知,并且它只可能是 ( exttt{C}) 或 ( exttt{H})(是 ( exttt{O}) 的话已经被问出来了)。
- ( exttt{OO**})。即第 (3) 和第 (4) 位未知。此时第 (3) 位必是 ( exttt{H})(如果是 ( exttt{O}) 或 ( exttt{C}) 它一定已经被问出来了)。第 (4) 位只可能是 ( exttt{C}) 或 ( exttt{H})。
注意,不可能出现 ( exttt{**OO}) 或 ( exttt{*OOO}) 的情况。因为此时两个 ( exttt{O}) 前面,不可能是 ( exttt{C})(否则在 ( exttt{CO}) 就被问出来了),不可能是 ( exttt{H})(否则在 ( exttt{HO}) 就被问出来了),也不可能是 ( exttt{O})(否则在 ( exttt{OO}) 就被问出来了)。
综上所述,最多只有 (1) 个位还不确定,且它最多只有 (2) 种情况。所以只需要再来 (1) 次询问。总代价是:(5 imesfrac{1}{2^2} + 1 imesfrac{1}{4^2} = 1.3125)。
最后,如果 ( exttt{CC}, exttt{CH}, exttt{CO}, exttt{HO}, exttt{OO}) 都没有出现过,那么中间两位一定是 ( exttt{HH})。此时询问 ( exttt{HHH})。第一位如果是 ( exttt{H}),那么它会被问出来,否则它一定是 ( exttt{O})。同理,最后一位也可以确定。总代价是:(5 imes frac{1}{2^2} + 1 imesfrac{1}{3^2} = 1.3611)。
于是就做完了。实现的时候,可以把每个位置可能的选项,用一个 vector
存起来,然后 ( ext{dfs}) 一遍。这样可以避免复杂的讨论。详见参考代码。
总结:
要想到第一步:即用 (5) 次询问确定(除第一位和最后一位以外的)( exttt{C}) 和 ( exttt{H})。剩下的用耐心推一推,分类讨论一下即可。
归类:分类讨论。
难度:难。
CF1097E Egor and an RPG game
约定:以下称一个单调上升或单调下降的子序列,为“合法子序列”。称最长上升子序列为 ( ext{LIS}),最长下降子序列为 ( ext{LDS})。
考虑 (f(n)) 是多少。
我们想办法构造出一个“划分出的合法子序列数”很大的排列。考虑如下序列:
即,它是分层上升的,每层比上一层多一个数,且同一层内是严格递减的。
当存在某个正整数 (k),满足 (n = 1 + 2 + dots + k = frac{k(k + 1)}{2}) 时,这个序列无论划分成上升还是下降,都至少要分出 (k) 个合法子序列。更进一步,如果 (frac{k(k + 1)}{2}leq n < frac{(k + 1)(k + 2)}{2}),则用类似的构造方法,也可以使得一个长度为 (n) 的序列,至少分出 (k) 个合法子序列。换句话说,现在我们说明了:
记 (c(n) = maxleft{k ig | frac{k(k + 1)}{2} leq n ight})。
现在,如果我们能够想到一种方法,使得对任意长度为 (n) 的排列,都能将它划分为不超过 (c(n)) 个合法的子序列,我们的任务就完成了。因为:我们划分出的子序列数 (leq c(n)),而 (c(n)leq f(n))。
用归纳法。假设命题【对任意 (k geq c(m)),总能把一个长度为 (m) 的排列划分为不超过 (k) 个合法子序列】在 (1leq m < n) 时成立。接下来我们证明它在 (m = n) 时也是成立的。
求出当前序列的一个 ( ext{LIS}),记它的长度为 (l)。
情况一:若 (l > c(n)),则将它作为新划分出的一个子序列,加入答案。之后问题规模缩减为:
对原来的 (n, k),我们有:(n < frac{(k + 1)(k + 2)}{2})。发现删除后,(n - l < frac{(k + 1)(k + 2)}{2} - (k + 1)= frac{k(k + 1)}{2}),即 (n' < frac{(k' + 1)(k' + 2)}{2})。所以 (c(n')leq k')。此时根据归纳假设,可知原命题成立。
情况二:若 (l leq c(n))。发现我们总能将原序列划分为恰好 (l) 个下降子序列。其实就是在用二分求 ( ext{LIS}) 的算法中,每次二分出第一个大于当前数的位置后,不直接覆盖,而是把当前数加入到它代表的下降子序列中。具体可以见代码。
综上所述,原命题成立。因此用上述方法构造出的划分方案,就是符合要求的答案了。另外,上述的构造顺便还说明了 (f(n)leq c(n)),进而 (f(n) = c(n)),不过这已经不重要了。
每次找 ( ext{LIS}) 是 (mathcal{O}(nlog n)) 的(我用的树状数组)。因为 (c(n)) 是 (mathcal{O}(sqrt{n})) 级别的,所以总时间复杂度 (mathcal{O}(nsqrt{n}log n))。
总结:
我们先想方设法,构造出了一个“划分出的合法子序列数”很大的排列,从而确定了 (f(n)) 的一个下界 (c(n))。接下来我们通过归纳地构造,使得对任意长度为 (n) 的排列,都能划分出不超过 (c(n)) 个合法子序列。
构造的过程中,还用到了 ( ext{dilworth}) 定理的一个推论:如果原序列的 ( ext{LIS}) 长度为 (l),那么它能被划分为 (l) 个下降子序列(而且不能再少了)。
归类:归纳。
难度:难。
CF1261E Not Same
把问题稍微转化一下,变成要求构造一个 (n+1) 行 (n) 列的 (01) 矩阵,每一列的和给定,并且要求任意两行互不相同。
记 (b(i, j)) 表示答案矩阵第 (i) 行第 (j) 列上的数。
法一:
首先将所有数从大到小排好序。
对第 (i) 个数,相当于要在第 (i) 列填 (a_i) 个 (1)。我们从第 (i) 行开始向下走,依次把经过的前 (a_i) 个格子填上 (1)(如果到底了就返回第 (1) 行)。
下面证明这种做法的正确性。
首先,如果我们对排好序的 (a) 序列构造出答案后,再把答案的矩阵列按照原顺序交换,显然结果仍是正确的。故以下讨论的 (b) 矩阵,均为对排好序的 (a) 序列构造的答案。
我们要证明,按上述方法构造出的 (b) 矩阵,不存在完全相同的两行。
反证法:考虑两行 (i, j) ((i < j)),假设这两行相同。
简单分类讨论一下:
- 情况一:(1leq i < jleq n)。
因为 (a_{i + 1}leq n),所以 (b(i, i + 1) = 0)。又因为第 (i) 行与第 (j) 行相同,所以 (b(j, i + 1) = 0)。所以 (a_{i + 1}leq j - i - 1)。
如果 (i + 2leq j),考虑第 (i + 2) 列。要么 (b(i,i + 2) = b(j, i + 2) = 1),要么 (b(i, i + 2) = b(j, i + 2) = 0)。如果 (b(i,i + 2) = b(j, i + 2) = 1),那么 (a_{i + 2}geq j - i + 1)。然而,由于 (a_{i + 2}leq a_{i + 1}leq j - i - 1),所以这种情况不存在,所以只可能 (b(i, i + 2) = b(j, i + 2) = 0)。所以 (a_{i + 2}leq j - i - 2)。
同理,可以推出,(forall k in[1, j - i]:a_{i + k}leq j - i - k)。所以 (a_{j}leq 0)。与题意矛盾。
- 情况二:(1leq ileq n),(j = n + 1)。
因为 (a_{i}geq 1),所以 (b(i, i) = 1)。又因为第 (i) 行与第 (j = n + 1) 行相同,所以 (b(n + 1, i) = 1)。所以 (a_{i}geq n + 2 - igeq 2)。
如果 (i > 1),考虑第 (i - 1) 列。因为 (a_{i}geq 2),所以 (a_{i - 1}geq a_{i}geq 2)。所以 (b(i, i - 1) = 1)。所以 (b(n + 1, i - 1) = 1)。所以 (a_{i - 1}geq n + 3 - i)。
同理,可以推出,(forall k in[0, i - 1]: a_{i - k} geq n + k + 2 - i)。所以 (a_{1} geq n + 1),与题意矛盾。
顺便一提,我们原本的想法是把 (a) 序列按从小到大排序。这样也能得到一个“似乎正确”的做法。但是该做法可能导致第 (n) 行和第 (n + 1) 行相同。这给我们的启示是:
- 在证明时,注意考虑 (ileq n),(j = n + 1) 的特殊情况,是至关重要的。
- 如果从小到大排序不行,可以尝试反过来。
法二:
不需要排序。
我们从 (1) 到 (n),依次考虑每一列。
假设当前考虑到底 (i) 列,我们看看前 (i - 1) 列里(((n + 1) imes(i - 1)) 的矩阵里)有哪些相同的行(初始时所有行都是相同的)。
- 如果没有相同的行,那么后面无论怎么填,都不会再出现相同的行。所以可以随便填。
- 否则找出相同的两行 (r_1, r_2),令 (b(r_1, i) = 1),(b(r_2, i) = 0)。剩下的 (a_i - 1) 个 (1) 随便填。
下面证明这种做法的正确性。
把相同的行视为一组,记录每组的大小,得到一个可重集。
例如,初始时所有行都是相同的,那么可重集就是 ({n + 1})。加入了 (a_1) 后,可重集变成:({a_1, n + 1 - a_1})。
如果所有行都不同,那么可重集是 ({1, 1, dots, 1})((n + 1) 个 (1))。
否则我们每次操作的效果,相当于至少会拆掉可重集里的一个 (> 1) 的数。即,从可重集里选择一个 (x > 1),删掉它,再加入两个数 (x_1, x_2),满足 (x_1 + x_2 = x),(x_1, x_2 > 0)。
原可重集 ({n + 1}),在变为 ({1, 1, dots, 1}) 前,最多可以被操作 (n) 次。
所以经过 (n) 次操作后,可重集一定会变为 ({1, 1, dots, 1}),即所有行都不相同。
朴素实现是 (mathcal{O}(n^4)) 的(即每次暴力枚举两行,再暴力判断它们是否相同)。
对行哈希,用哈希来判断是否相同,可以做到 (mathcal{O}(n^2log n)) 或者 (mathcal{O}(n^2))。
总结:
第一种方法很牛逼,不知道怎么想到的,可能需要大量的尝试。第二种方法应该更容易想到。
难度:中。
Part3 jly 论文
注:本部分所有内容均参考了 jly 的论文。如有雷同,我抄他的。
知识点:鸽巢原理
鸽巢原理,或称为抽屉原理,是组合数学中一个非常重要的原理。通常的表述是,若将 (n) 件物品放入 (k) 个抽屉,则其中一定有一个抽屉包含至少 (lceilfrac{n}{k} ceil) 件物品,也一定有一个抽屉包含至多 (lfloorfrac{n}{k} floor) 件物品。
在一些构造题中,常常会要求构造一个权值至少为(或不超过)某一个数的方案。很多时候,可以考虑找出若干个可行的方案,使得它们的权值之和是定值。假设找出了 (k) 个可行 方案,其总权值和为 (n),由抽屉原理,这些方案中最小的权值一定不超过 (lfloorfrac{n}{k} floor),最大的权值至少为 (lceilfrac{n}{k} ceil)。
CF1450C2 Errich-Tac-Toe (Hard Version)
记第 (r) 行第 (c) 列的格子为 ((r, c))。
将所有格子,按照 ((r + c)mod 3) 的值,分为 (0, 1, 2) 三类。发现一个很好的性质:任意一行或一列上连续的三个格子,一定恰好包含 (0, 1, 2) 类格子各一个。
考虑通过操作,使得 (0, 1, 2) 三类里,有一类格子上全是 ( ext{X}),有一类格子上全是 ( ext{O})(如果格子非空的话),这样就一定满足题意了。
具体来说,我们有如下六种操作方案(以下讨论的均是非空的格子):
- 把第 (0) 类格子全改成 ( ext{X}),把第 (1) 类格子全改成 ( ext{O})。
- 把第 (1) 类格子全改成 ( ext{X}),把第 (0) 类格子全改成 ( ext{O})。
- 把第 (0) 类格子全改成 ( ext{X}),把第 (2) 类格子全改成 ( ext{O})。
- 把第 (2) 类格子全改成 ( ext{X}),把第 (0) 类格子全改成 ( ext{O})。
- 把第 (1) 类格子全改成 ( ext{X}),把第 (2) 类格子全改成 ( ext{O})。
- 把第 (2) 类格子全改成 ( ext{X}),把第 (1) 类格子全改成 ( ext{O})。
考虑初始时,每种格子上,每种字母的出现次数,如下表:
那么,六种方案所需的操作次数分别是:
- (o_0 + x_1)
- (o_1 + x_0)
- (o_0 + x_2)
- (o_2 + x_0)
- (o_1 + x_2)
- (o_2 + x_1)
它们的总和是 (2(x_0 + x_1 + x_2 + o_0 + o_1 + o_2) = 2k)((k) 的定义见题面)。
根据鸽巢原理,(6) 个数的总和为 (2k),必存在至少一个数小于等于 (lfloorfrac{2k}{6} floor = lfloorfrac{k}{3} floor)。也就是说,我们取其中操作次数最少的一种方案,一定是符合题意的。
另外,如果只考虑第 (2, 3, 6) 种方案,或只考虑第 (1, 4, 5) 种方案,它们的和都是 (k),所以最小值必不超过 (lfloorfrac{k}{3} floor)。跟考虑全部的 (6) 种方案本质是一样的。
时间复杂度 (mathcal{O}(n^2))。
总结:
看到矩阵上连续的 (m) 个位置,就想到把矩阵按 ((r + c)mod m) 染色,这是比较套路的。尤其是处理“矩阵上相邻两个位置”怎么怎么样时,我们会把矩阵按 ((r + c)mod 2) 染色。在本题里,我们把矩阵按 ((r + c)mod 3) 染色,这样带来的好处是:任意一组“连续的 (3) 个位置”,都会包含恰好每类格子各一个。
之后还需要熟练使用鸽巢原理。
归类:矩阵染色,鸽巢原理。
难度:易。
gym102900B Mine Sweeper II
称只含数字的 (0,1) 的矩阵为 (01) 矩阵。本文里,矩阵的行、列均从 (1) 开始编号。(A_{i,j}) 表示矩阵 (A) 第 (i) 行第 (j) 列上的数值。
可以用一个 (01) 矩阵 (A) 来描述一张地图。(A_{i, j} = 0) 当且仅当地图第 (i) 行第 (j) 列的位置上为空地,(A_{i,j} = 1) 当且仅当地图的第 (i) 行第 (j) 列的位置上为地雷。
设 (S(A)) 表示 (A) 所对应的地图里“所有空地上的数字之和”。形式化地:
设 (mathrm{dis}(A, B)) 表示 (A, B) 两矩阵里不同的位置数。形式化地:
原问题相当于,给定 (01) 矩阵 (A, B),要求构造一个 (01) 矩阵 (C),满足:
引理:一张地图所有空地上的数字之和等于相邻的 ( extbf{(空地, 地雷)}) 的对数。
形式化地说,定义函数 (f(r_1, c_1, r_2, c_2)) 表示 ((r_1, c_1)) 和 ((r_2, c_2)) 这两个位置是否相邻。那么:
根据上述引理,可以有一个推论:如果将一张地图的所有地雷改成空地,所有空地改成地雷,其所有空地上的数字和不变。形式化地:
定义 (T(A)) 为把矩阵 (A) 所有位置上的值分别取反得到的新矩阵。即:
则:
例如,在下图中,左右张地图所有空地上的数字之和相等。
上述推论启发我们,如果令 (C = B) 或令 (C = T(B)),都满足 (S(C) = S(B)) 的条件。接下来分析第二个条件。
注意到,对于任意两个大小为 (n imes m) 的矩阵 (A, B),显然有:
根据鸽巢原理,可知:
于是我们令 (C = B) 或 (C = T(B)),必有至少一种方案是合法的。
时间复杂度 (mathcal{O}(nm))。
代码略。
总结:
首先需要发现一个核心性质,即:一张地图所有空地上的数字之和等于相邻的 ( ext{(空地, 地雷)}) 的对数。然后可以进一步分析出:如果将一张地图的所有地雷改成空地,所有空地改成地雷,其所有空地上的数字和不变。据此可以想到两种构造方案,即 (C = B) 或 (C = T(B))。然后用鸽巢原理来分析这两种方案即可。
在构造题里,如果题目要求用不超过 (lfloorfrac{s}{k} floor) 次操作,完成一个任务,我们考虑使用鸽巢原理,做法是:找出 (k) 种构造方案,它们的操作次数之和为 (s)。如果存在这样的 (k) 种方案,那么其中必有至少一种方案,操作次数是不超过 (lfloorfrac{s}{k} floor) 的。
归类:鸽巢原理。
难度:易。
知识点:DFS 树
在解决一些图上的构造问题时,DFS 树往往有非常大的帮助。
一张图的 DFS 树是在对其进行深度优先遍历时,所形成的树结构。建立了 DFS 树后, 图上的边可以分成四类:
- 树边:即每个点到其所有孩子结点的边,也即每个点第一次被访问时经过的边。
- 前向边:是每个点到其后代的边,不包括树边。
- 后向边:是每个点到其祖先的边。
- 其余边称为横叉边。
其中,前向边、后向边、横叉边统称为非树边。
在构造题中,通常我们用到的是无向图的 DFS 树。如果我们将每条边按照第一次经过时的方向进行定向,则无向图的 DFS 树满足所有非树边都是后向边。这个性质在解题过程中有非常大的作用。
CF1364D Ehab's Last Corollary
见这篇文章。里面同时包含了多道相关的题目,供读者参考。
LOJ3176 「IOI2019」景点划分
对三个点集的大小,不妨设 (aleq bleq c)。
称一个“连通”的节点集合为“连通块”(关于“连通”的定义可以见题面)。那么问题相当于,在图上找出两个连通块,使得它们交为空,且大小分别为 (a, b) 或 (a, c) 或 (b, c)。
对于任意点数 $ > 1$ 的连通块,我们总是可以删去一个节点,使得剩下的节点仍然连通(找出任意一棵生成树,删掉一个叶子即可)。也就是说,我们总是可以把大的连通块变小。因此,如果存在一种方案,划分出了大小为 (a) 和 (c) 的连通块,那么也必存在方案能划分出大小为 (a) 和 (b) 的连通块。(b, c) 同理。于是我们只需要考虑,如何划分出两个大小分别为 (a, b) 的连通块即可。
考虑图是一棵树的情况。显然,此时有解的充分必要条件是:存在一条边,使得边两侧的子树大小分别不小于 (a) 和 (b)。可以枚举每条边并判断。
虽然树上的问题已经解决了,但为了给一般图上的做法做铺垫,我们进一步挖掘此时的性质。给出树的重心的定义及其基本性质:
(mathrm{son}_r(u)) 表示以 (r) 为根时 (u) 的儿子的集合。定义 (mathrm{size}_r(u)) 表示以 (r) 为根时,(u) 的子树大小。定义 (f(u) = max_{vinmathrm{son}_u(u)}{mathrm{size}_u(v)}),即以 (u) 为根时,(u) 的最大的儿子子树的大小。取 (f(u)) 最大的节点 (u),即为树的重心。如有多个(最多只有两个),可取任意一个。
性质:如果 (u) 是树的重心,那么 (forall v inmathrm{son}_u(u)),(mathrm{size}_u(v)leq lfloorfrac{n}{2} floor)。
考虑树的重心 (c)。发现有解的充分必要条件是,以 (c) 为根时,存在一个 (c) 的儿子子树大小 (geq a)。也就是说:只需要枚举和 (c) 相连的边即可!
证明:
充分性:因为 (bleq c) 且 (b + c = n - a leq n),所以 (bleq lfloorfrac{n}{2} floor)。又因为重心的所有儿子子树大小 (leq lfloorfrac{n}{2} floor),所以删去那个大小 (geq a) 的儿子子树后,剩余部分的大小 (geq n - lfloorfrac{n}{2} floor geq b)。
必要性:如果所有子树的大小都 (< a),意味着任意一个大小为 (a) 的连通块和大小为 (b) 的连通块,都必须包含重心。所以无法满足两连通块交为空这一条件,故无解。
回到一般图上的情况。建出 DFS 树。找到 DFS 树的重心 (c)。记 DFS 树上 (c) 上方的部分(除 (c) 子树外的部分)为 (T),(c) 的儿子子树分别为:(S_1, S_2, dots, S_k)。考虑几种情况:
- 如果 (T) 或某个 (S_i) 的大小 (geq a),那么和树的情况是一样的,可以构造出解。
- 如果 (T) 和所有 (S_i) 的大小都 (< a)。就需要考虑无向图 DFS 树的性质:不存在横叉边。所以不同的 (S_i) 之间是没有连边的。同时,可能有一些 (S_i) 会与 (T) 相连。
- 如果所有与 (T) 相连的 (S_i) 加上 (T) 的大小之和 (< a),那么一定无解:因为任意一个大小为 (a) 的连通块和大小为 (b) 的连通块,都必须包含重心,所以无法满足两连通块交为空这一条件。
- 否则考虑一个点集 (X),初始时为空。先把 (T) 加入 (X)。然后依次把和 (T) 相连的 (S_i) 加入 (X)(可以以任意顺序),直到 (X) 的大小不小于 (a) 为止。因为所有 (S_i) 大小都 (< a),所以最终 (X) 的大小一定 (< 2a)。又因为 (2a + bleq a + b + c = n),所以在删除 (X) 后,剩余节点数量 (geq n - 2ageq b)。此时,我们一定能在 (X) 里构造出连通块 (A),在剩余节点里构造出连通块 (B)。做法比较简单:
- 先将 (T) 加入 (A),然后在 (X) 中的每个 (S_i) 里,找一个与 (T) 相连的点加入 (A)(记为这个点为 (u_i))。把所有 (u_i) 加入后,如果大小还是不够,再将每个 (S_i) 里的其他点加入,为了保证联通,可以从每个 (u_i) 开始,在子树里 DFS。
- 对于 (B),先将重心 (c) 加入 (B),然后把不在 (X) 里的子树依次加入即可。同样,为了保证联通,可以从子树的根开始 DFS。
综上所述,我们证明了一种情况的无解性,并对除此之外的情况给出了构造方案。
时间复杂度 (mathcal{O}(n + m))。
总结:
从图是一棵树的特殊情况入手思考,发现和重心相关的性质。然后把思路迁移到 DFS 树中。在本题里,重心的性质带来的好处是:如果一个子树大小 (geq a),那么另一侧一定能构造出 (b)。然后只需要思考所有子树都 (< a) 的情况即可。
归类:DFS树。
难度:难。