• 构造题, 交互题选做


    更新中。

    欢迎催更。

    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) 次询问。

    参考代码-在CF查看


    总结

    核心是要想到对两个数互相询问。即发现 (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) 次询问。可以通过本题。

    参考代码-在CF查看


    总结

    本题里的这个“二分”非常巧妙。一般的二分,我们都试图寻找区间里第一个最后一个满足某条件的位置,每次我们会判断中点 (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) 个节点,可以通过本题。

    参考代码-在CF查看


    总结

    看到点数 (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))。则答案可以按如下方法构造:

    [mathrm{ans}(i,j) = egin{cases} 720720 && (i + j) mod 2 = 0\ 720720 + a(i,j)^4 && (i + j)mod 2 = 1 end{cases} ]

    容易发现,满足题目的所有要求。

    参考代码-在CF查看


    总结

    网格上,限制条件和“相邻位置”有关,一般可以考虑把格子按 (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)),可以通过。

    参考代码-在CF查看


    总结

    一开始要多观察和分析,不要被题面里的上取整、下取整搞晕,可以借助程序,就能发现整个过程的规律其实是简洁优美的,细节并不繁琐。

    然后分块和二分,都是交互题里比较经典的做法。

    归类:分块,二分。

    难度:中。

    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) 次操作,可以通过本题。

    参考代码-在CF查看


    总结

    首先分析:因为操作不还原,如果无法确定反转的是哪一边,那么是很难做的。于是我们试图发现一些性质,来判断它反转的是哪一边。进而我们分析出了奇偶性的这个性质。利用这个性质,不难构造出操作的方法。

    归类:奇偶性。

    难度:中。

    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))

    参考代码-在CF查看


    总结

    一开始不知道有解的条件,我们就先蒙一个显然的必要条件。然后在满足这个条件的前提下,尝试去构造解法。如果构造出来了,说明该条件也是充分的。

    首先构造出一个基本操作,它像是我们的砖头,我们用它来盖大房子。

    树上的问题,要注意到树本身特有的、“递归式”的结构,以子树作为天然的子问题,递归地解决。

    归类:树,递归。

    难度:中。

    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})))

    参考代码-在博客文章里查看参考代码-在CF查看


    总结

    上述做法的本质是:将一个序列按值域范围划分成若干子序列之后,原序列的每个连续区间都可以表示成划分成的子序列中的连续区间

    分块分治都是对这样本质的具体实现(分出的块是这样的子序列,分治时处理的 (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) 次。可以通过本题。

    参考代码-在CF查看


    法二

    依次考虑所有位置。维护两个位置 (a, b),表示在当前扫描到的前缀里,(0) 只可能在位置 (a) 或位置 (b) 上。并且记录下 (p_a operatorname{or} p_b) 的值。设当前位置为 (c),则有如下情况:

    1. (p_a operatorname{or} p_c > p_a operatorname{or} p_b),则 (p_c) 不可能是 (0)
    2. (p_a operatorname{or} p_c < p_a operatorname{or} p_b),则 (p_b) 不可能是 (0)。我们把 (b) 踢掉,把 (c) 加入。
    3. (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) 次询问,可以通过本题。

    参考代码-在CF查看


    归类:二进制编码。

    难度:中。

    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

    参考代码-在CF查看


    结合一下

    我们发现,分块做法在处理二元组 ((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})。可以通过本题。

    参考代码-在CF查看


    更牛一点

    发现在上述的,从 (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})。非常优秀。

    参考代码-在CF查看


    总结

    首先,分块是一个常见的优化。然后要建立图论的模型,这需要对问题性质的深入分析。最后还要在图上构造出划分方案,比较考验构造能力。

    归类:分块,图划分。

    难度:难。

    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}) 出现过,那么目前已知的串,可能有如下三种情形:

    1. ( exttt{OOOO})。此时答案已知。
    2. ( exttt{OOO*})。即只有第 (4) 位未知,并且它只可能是 ( exttt{C})( exttt{H})(是 ( exttt{O}) 的话已经被问出来了)。
    3. ( 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}) 一遍。这样可以避免复杂的讨论。详见参考代码。

    参考代码-在CF查看


    总结

    要想到第一步:即用 (5) 次询问确定(除第一位和最后一位以外的)( exttt{C})( exttt{H})。剩下的用耐心推一推,分类讨论一下即可。

    归类:分类讨论。

    难度:难。

    CF1097E Egor and an RPG game

    题目链接


    约定:以下称一个单调上升或单调下降的子序列,为“合法子序列”。称最长上升子序列为 ( ext{LIS}),最长下降子序列为 ( ext{LDS})


    考虑 (f(n)) 是多少。

    我们想办法构造出一个“划分出的合法子序列数”很大的排列。考虑如下序列:

    [{1, 3, 2, 6, 5, 4, 10, 9, 8, 7, dots } ]

    即,它是分层上升的,每层比上一层多一个数,且同一层内是严格递减的。

    当存在某个正整数 (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) 个合法子序列。换句话说,现在我们说明了:

    [f(n)geq maxleft{k igg | frac{k(k + 1)}{2} leq n ight} ]

    (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)),则将它作为新划分出的一个子序列,加入答案。之后问题规模缩减为:

    [k' gets k - 1\ n' gets n - l ]

    对原来的 (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))

    参考代码-在CF查看


    总结

    我们先想方设法,构造出了一个“划分出的合法子序列数”很大的排列,从而确定了 (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) 行相同。这给我们的启示是:

    1. 在证明时,注意考虑 (ileq n)(j = n + 1) 的特殊情况,是至关重要的。
    2. 如果从小到大排序不行,可以尝试反过来。

    参考代码-在CF查看


    法二

    不需要排序。

    我们从 (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))

    参考代码-在CF查看


    总结

    第一种方法很牛逼,不知道怎么想到的,可能需要大量的尝试。第二种方法应该更容易想到。

    难度:中。

    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})(如果格子非空的话),这样就一定满足题意了。

    具体来说,我们有如下六种操作方案(以下讨论的均是非空的格子):

    1. 把第 (0) 类格子全改成 ( ext{X}),把第 (1) 类格子全改成 ( ext{O})
    2. 把第 (1) 类格子全改成 ( ext{X}),把第 (0) 类格子全改成 ( ext{O})
    3. 把第 (0) 类格子全改成 ( ext{X}),把第 (2) 类格子全改成 ( ext{O})
    4. 把第 (2) 类格子全改成 ( ext{X}),把第 (0) 类格子全改成 ( ext{O})
    5. 把第 (1) 类格子全改成 ( ext{X}),把第 (2) 类格子全改成 ( ext{O})
    6. 把第 (2) 类格子全改成 ( ext{X}),把第 (1) 类格子全改成 ( ext{O})

    考虑初始时,每种格子上,每种字母的出现次数,如下表:

    [egin{array}{c|c|c|c} &0&1&2\hline ext{X}&x_0&x_1&x_2\hline ext{O}&o_0&o_1&o_2\ end{array} ]

    那么,六种方案所需的操作次数分别是:

    1. (o_0 + x_1)
    2. (o_1 + x_0)
    3. (o_0 + x_2)
    4. (o_2 + x_0)
    5. (o_1 + x_2)
    6. (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))

    参考代码-在CF查看


    总结

    看到矩阵上连续的 (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) 所对应的地图里“所有空地上的数字之和”。形式化地:

    [S(A) = sum_{i = 1}^{n}sum_{j = 1}^{m} [A_{i,j} = 0]sum_{k = max(1, i - 1)}^{min(n, i + 1)}sum_{l = max(1, j - 1)}^{min(n, j + 1)} A_{k, l} ]

    (mathrm{dis}(A, B)) 表示 (A, B) 两矩阵里不同的位置数。形式化地:

    [mathrm{dis}(A, B) = sum_{i = 1}^{n}sum_{j = 1}^{m}[A_{i,j} eq B_{i,j}] ]

    原问题相当于,给定 (01) 矩阵 (A, B),要求构造一个 (01) 矩阵 (C),满足:

    [egin{cases} S(C) = S(B)\ mathrm{dis}(A, C) leq lfloorfrac{nm}{2} floor end{cases} ]


    引理:一张地图所有空地上的数字之和等于相邻的 ( extbf{(空地, 地雷)}) 的对数

    形式化地说,定义函数 (f(r_1, c_1, r_2, c_2)) 表示 ((r_1, c_1))((r_2, c_2)) 这两个位置是否相邻。那么:

    [f(r_1, c_1, r_2, c_2) = [|r_1 - r_2| + |c_1 - c_2| = 1]\ S(A) = sum_{i = 1}^{n}sum_{j = 1}^{m}sum_{k = 1}^{n}sum_{l = 1}^{m}f(i, j, k, l)cdot [A_{i, j} = 0]cdot[A_{k, l} = 1] ]


    根据上述引理,可以有一个推论:如果将一张地图的所有地雷改成空地,所有空地改成地雷,其所有空地上的数字和不变。形式化地:

    定义 (T(A)) 为把矩阵 (A) 所有位置上的值分别取反得到的新矩阵。即:

    [forall iin[1, n], jin[1, m]: (T(A))_{i, j} = lnot A_{i,j} ]

    则:

    [S(A) = S(T(A)) ]

    例如,在下图中,左右张地图所有空地上的数字之和相等。


    上述推论启发我们,如果令 (C = B) 或令 (C = T(B)),都满足 (S(C) = S(B)) 的条件。接下来分析第二个条件。

    注意到,对于任意两个大小为 (n imes m) 的矩阵 (A, B),显然有:

    [mathrm{dis}(A, B) + mathrm{dis}(A, T(B)) = nm ]

    根据鸽巢原理,可知:

    [min{mathrm{dis}(A, B), mathrm{dis}(A, T(B))}leqlfloorfrac{nm}{2} floor ]

    于是我们令 (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))

    参考代码-在LOJ查看


    总结

    从图是一棵树的特殊情况入手思考,发现和重心相关的性质。然后把思路迁移到 DFS 树中。在本题里,重心的性质带来的好处是:如果一个子树大小 (geq a),那么另一侧一定能构造出 (b)。然后只需要思考所有子树都 (< a) 的情况即可。

    归类:DFS树。

    难度:难。

  • 相关阅读:
    MySQL 多列索引优化小记
    Spring MVC + Security 4 初体验(Java配置版)
    Java程序通过代理访问网络
    Spring RESTful + Redis全注解实现恶意登录保护机制
    WinSCP 中普通用户以 root 身份登录 Linux
    Linux下修改系统时区
    Git如何检出指定目录或文件
    朴素贝叶斯
    console.log 被重写覆盖以后如何恢复
    MongoDB 基础命令使用学习记录
  • 原文地址:https://www.cnblogs.com/dysyn1314/p/14364145.html
Copyright © 2020-2023  润新知