2024.11 做题记录

一个月封闭集训,可能题会稍微多一点。

001. P8940 [DTOI 2023] C. 不见故人 提高+/省选-

先来分析一下题目:写的还是比较简洁的,就是把区间里的数都变为它们的 gcd。然后分析一下所谓代价,发现如果 k 小的时候会比较麻烦些,而 k 大起来反倒好办,就是直接全局改一下就好。

Key Observation 0 首先如果所有数都相等,那么答案一定为 0

Key Observation 1 所有数最终都会变成全局的 gcd。这就让我们可以对题目进行一步等价转化——将所有数除掉全局 gcd,最终所有的数就都可以变成 1

Key Observation 2 每个数最多会被包含在一次修改中。这也是显然的,考虑反证:如果一个数被包含在两次修改中,那么为什么我们不将两次修改合并呢?这样改两次代价只会更大,与题目要求的最小性矛盾,故证毕。这就启发我们进行阶段性动态规划。

考虑动态规划,定义 fi 代表前 i 个数字最少经过多少次修改才能全部变为 1。转移较为显然,即若 gcd(aj+1,...,ai)=1,那么 fi=min(fi,fj+ij+k),因为这个过程是倒着来的,所以我们也倒着枚举 j 方便快速递推 gcd

如若 n105,那么可以考虑二分和 ST 表优化掉找左界这一步,但 n4×106,导致我们我们必须想其他办法来优化这个动态规划。但这步启发我们可以把贡献拆为 fjji+k 两部分方便优化。

Key Observation 3 如若把 ai=1i 看做分界点,把序列分为 m 块,则在块的最右端点进行转移才是有意义的。这启发我们找到块,按照块进行动态规划。 Old Friend's Trick

找到块是简单的但细节较多,怎样处理看个人喜好。定义 gi 为到第 i 块最少要经过多少次修改才能全部变为 1。则有两种转移方式,第一种是块内转移,gi=gi1+rili+1+k+[gcd(ali,...,ari)1],其中 [p] 为艾弗森括号,p 是一个命题,若其为真,则 [p]1,否则为 0。第二种是隔块转移,这样就保证在转移区间内。一定存在 j 使得 aj=1。那么 gi=min{gj+rilj+1+k+1},向上面一样拆贡献维护最小值即可,甚至不用单调队列。注意第二种转移的 j 要保证 j<i1,处理时有些细节。答案即为 gm

002. CF1292B Aroma's Search 1700

题目选自:2020, 2021 年 CF 简单题精选

题意就是它给你生成了一个点列 (xi,yi)。这个点列是无穷的,给定了 (x0,y0),生成方式是 (xi,yi)=(ax×xi1+bx,ay×yi1+by)。现在给定起点 (xs,ys),询问 t 时间内最多能经过多少个点。

Key Observation 1 注意到 axay 都是大于等于 2 的,所以在值域内的点其实是 logV 量级的,这是一个非常非常关键的性质,它的出现让我们可以随便写都能过。

我们直接暴力生成这个点列,然后直接暴力枚举起点。这都是 logV 带给我们的自信。

Key Observation 2 xiyi 是单调上升的,所以一个一个走的曼哈顿距离等价于直接走过去的曼哈顿距离。发现就好,证明是显然的。

然后在枚举起点的同时,直接暴力枚举左右端点,因为注意到,我们最多最多就只能折返一次,然后枚举是先向右走还是先向左走即可。时间复杂度 O(log3V),做完了。

*003. CF1313D Happy New Year 2500

题目选自:2020, 2021 年 CF 简单题精选

题意很简洁,不需要我过多叙述。

Key Observation 0 数据范围 1k8。看到这么小的数据范围我们应该在能力范围内可以想到以下算法:暴搜,状压,容斥。当然我们目前并没有任何头绪。

Key Observation 1 差分,我们注意到区间加在差分意义下就是单点加减,又注意到数据范围,n 在可接受范围内而 m 则不可接受,印证了我们这一猜想。

Key Observation 2 一维扫描线,值域广而有用的点不多应让我们想到了 【模板】扫描线。我们尝试借鉴扫描线的思路进行某种操作。Happy New Year's Trick

我们先把有用的 2×n 个点加入一个数组里面。按照位置顺序排序,同时终点在前,起点在后,这是为了方便以后处理。并方便进行扫描线操作。并且存储时我们不妨给每个操作点一个权,起点为 i,终点为 i,这方便我们下面来对接,也方便终点在前的排序。

接下来进入主体部分。利用 k 的性质,我们进行状态压缩动态规划。

我们定义第 ii+1 这段区间为第 i 个操作段。i12×n 之间。特殊的,如果 i 的位置和 2×n 一样,那么这个操作段的长度就为 0,表示整体操作结束。

Key Observation 3 我们定义 fi,j 为第 i 到第 i+1 个操作段的 起点选择状态为 jj 是一个二进制串,因为保证了 1k8,所以 j 最多有 8 位。但因为我们不知道你这些操作具体是什么,所以我们开一个 vis 数组记录这些对你这个区间有影响的操作编号。 Happy New Year's Trick 2

接下来考虑怎么转移,我们先记第 i 个操作段的长度为 leni

讨论第 i 个操作点是起点还是终点:

  1. 若第 i 个操作点为起点,那么我们考虑怎么刻画这个事情。因为我们不考虑你起始操作点的顺序,因为你已经存了编号,所以你随便找一个没有被占用的位置占用上即可,即把 visj0 的一个点改为自己的 id,并用这个点的位置去进行更新。想到这个后转移就较为容易,枚举二进制串 02k。如果串 p 中存在 j 这一位,那么就是直接逆推,考虑前面没加上这一位时的答案,即 fi,p=fi1,p2j+leni×(popcount(p)mod2),如果没有 j 这一位,那么也是简单的,就是 fi,p=fi1,p+leni×(popcount(p)mod2)
  2. 若第 i 个操作点为终点,其实与起点是类似的。因为这是终点,所以前面必会有一个点对其有影响且是它对应的起点。我们找到这个点后,它就对这段区间没有了影响,把它取消标记,也就是将其 vis 值设置为 0。同时记录位置 j。那么转移也是非常简单的,如果串 p 中已经存在 j 这一位,那么就是不合法的,因为我们已经将第 j 位去除了,赋值为无穷大。否则 fi,p=max{fi1,p2j,fi1,p}+leni×(popcount(p)mod2)

最后一步是统计答案,就是 f2×n,0,就是最后一个操作段已经不能被任何操作点所影响。注意一下初始值是全部 f 数组赋值为无穷大,而 f0,0 单点为 0。所以这题我们就做完了。

004. CF1322B Present 2100

题目选自:2020, 2021 年 CF 简单题精选

题意简洁,不过多叙述。

非常妙的一道题。看到 ai107,我们可以考虑到二进制题目的一些经典操作。

Key Observation 1 按位统计答案,这看不出来的话你就没救了。

Key Observation 2k 位是不是 1 只与前 k 位有关,与其他的无关,所以可以将每个 aimod2k 放到 bi 里进行下一步操作。Present's Trick

Key Observation 3k 位是否是 1 要分两种情况讨论,第一种是两数之和不进位到上面,那么两数之和应该在 [2k,2k+1),第二种是进位到上面,那么两数之和应该在 [2k+1+2k,2k+22] 内。

有了第 3 个关键发现,我们就可以排序双指针解决这个问题,那么第 k 位对其的贡献就是 (cntmod2)×2k,最后都加起来即可。于是我们就做完了。

005. P5094 [USACO04OPEN] MooFest G 加强版 普及+/提高

题目摘自:我的《分治全家桶》博客

非常魔怔的一道题,反正分治就很人类智慧的呢。这题感觉树状数组做法是绿,分治做法得有蓝,因为不太好想。题目问的是 max(vi,vj)×|xixj|

看到绝对值我们考虑先把绝对值拆掉再说。按照键值 xi 从小到大排序。这样式子变成 max(vi,vj)×(xjxi)。为什么要分治呢?因为前面那个非常讨厌的 max(vi,vj)

我们继续考虑后序遍历 CDQ 分治(或者说序列分治)。在递归计算完左右两个子区间的答案后,左右区间内部天然的 x 键值顺序就显得没那么重要了。我们考虑打乱它,因为在整体合并中,我们只需要知道 右区间的所有 x 值都比左区间的 x 值大 就够了。这一点与第一题的归并排序有异曲同工之妙。

考虑我们合并要干什么东西。我们要处理的是 左右两边的 v 键值 对除自己外另一半区间的影响。这启示我们按照 v 进行排序。

合并时,每次加一个左区间的 v 键值对右半区间的贡献显然是:

vi×((k=mid+1j1xk)xi×(jmid1))

每次加一个右区间的 v 对左区间的贡献显然是:

vj×(xj×(il)(k=li1xk))

注意边界情况也要加上这两种贡献哦!不然你就会输出 0。也有更简洁的写法,不过自认为我的这个比较清晰易懂。

006. P3403 跳楼机 提高+/省选-

同余最短路模板题,但是真的好高妙啊,谁能想到要这么做啊?

我们发现如果直接连边 SPFA 或者直接连边 Dijkstra 都会直接炸的死死的。我们只能另辟蹊径。

Key Observation 1 我们发现虽然 h 大的离谱,但是 x,y,z 的值域都在可接受范围内。启发我们对这三个值下手。

Key Observation 2 我们不妨对 x 进行考虑,如果一个数 k 可行,那么 k+pxh 的这一系列数都是很可行的。所以我们不妨找到一些很小的数,然后让他们加上这些 px 得到最终的个数。

Key Observation 3 考虑如何不重不漏的计数,我们寻找不变量,在加 x 的过程中什么不变呢?对了,就是模 x 的余数不变!于是我们考虑按照余数分类。按照上面说的,我们对模 x 的每一个余数找一个最小可行的数,把它不断加上 x,计算不超过 h 的个数然后加起来即可。

Key Observation 4 如何实现这一点呢?我们考虑把模 x 完全剩余系中的 x 个数看做 x 个点,又因为我们还有 +y,+z 两种操作,所以我们连 同余边。将 i 连到 (i+y)modx,边权为 y+z 是同理的。这样我们跑一段最短路就可以找到模 x 的每一个余数的那个最小可行的数。Drop Tower's Trick

然后这道题我们就做完了。

007. P2365 任务安排弱化版 提高+/省选-

一眼能看出来是动态规划类型的题目。考虑朴素的动态规划算法。

这应该是简单的。定义 gi,j 代表前 i 个工作,分了 j 组做的最小费用。转移应该是 gi,j=gl1,j1+(j×s+sumti)×(sumfisumfl1),只要你没读错题就能想到这个 O(n3) 的动态规划。

Key Observation 1 本题最为神奇也是用到的唯一一个大 Trick —— 费用前置。在我们的推导中,我们发现复杂度的瓶颈在于分割的段数,但这又很难优化掉,因为我们需要知道花费了几倍的 s。但很遗憾,用普通的方法我们并不能解决或是规避这个问题。但是我们考虑一个 s 的影响,它能影响什么东西。很显然,这段时间可以影响在它后面的所有东西,又因为乘法是存在分配律的,所以我们就相当于把 s 这个费用前置,提前把它对后面的影响加上了,这样也就不必要知道具体有几个 s 了。费用前置 Trick/ Task Arranging Trick

008. [ABC379F] Buildings 2 1659 蓝牌题

感觉真的不好做啊?可能是我对这两种技巧都太生疏了吧。

你发现如果你单纯想预处理出所谓的“最左边的能看到你的”,那说明你跟我一样读错题了。因为题目中定义的“看见”这一动作是不完全具有单调性的。但这还是启示我们使用单调栈算法。

我们反过来考虑“你能看见啥”。那么我们考虑倒过来使用单调栈。

Key Observation 1 我们考虑信友队 NOIP 2024 联训 2024.11.13 的 T4 做法。在处理这样的多组询问问题时,我们考虑将询问离线。其实那题的做法也和这题差不太多。

我们考虑处理一组 (l,r) 询问。考虑到我们从后往前使用单调栈,所以我们的单调栈实际上是一个单调递减栈。考虑楼啥时候能被看见。首先它的位置得大于 r,其次它一定得比 [l,r] 内的最大值要大,再次在 [l,x] 中应该不存在比它高的建筑。

Key Observation 2 考虑在单调栈上二分。二分一个第一个位置比 r 大,那么从它一直到栈底的数都是可以的,那么这组询问的答案就是这段距离。

我们分别处理 m 组询问,这样我们这道题就做完了。

*009. [ABC379G] Count Grid 3-coloring 2304 黄牌题

打星是因为这题用到了一个状态压缩动态规划的经典优化 Trick——轮廓线状态压缩动态规划。

Key Observation 1 这其实是一个非常重要的发现,就是你发现如果 n×m200,那么 min(n,m)n×m 约等于 14。这就直接把做法指向了状态压缩动态规划。但考虑到朴素的按行转移状态压缩动态规划的时间显然是承受不住的,于是我们把做法指向 ——> 轮廓线状态压缩动态规划

我们钦定 m<n,并定义 fi,j,mask 为填完前 i 行,在 i+1 行填完 j 个,左边 j1 个和上一行的右边 jm 组成的一个三进制状态 mask 的合法组成个数。(这显然是一个三进制状压动态规划,实际上其状态约只有 2m 个,但也没有必要进行压缩因为时间上和空间上都可以通过)

首先我们先预处理出 f1,0,mask 这是可以直接 O(m×3m) 暴力枚举解决的。

考虑怎样转移,因为对于每一个格子,能影响它的只有上边和左边两个格子,先把这两个格子的状态按照某种方式快速求出来。然后枚举这一格的状态 0/1/2。然后你再以某种方式快速预处理出 newmask,直接暴力转移即可。注意碰到边界要转移到下一行。

最后答案即为 fn,0,mask。注意状态要使用 unordered_map 存储,以达到卡常,节约时间,节约空间的目的。

010. [ABC377E] Permute K times 2 1685 蓝牌题

一道与置换有关的趣题。不是很难,但很有趣就是了。

首先考虑我们的熟知结论 Exercise G's Trick,如果轮换数组为一个排列,那么所有数都必然会在自己的置换环上行走。

考虑显然的先把所有的环搜出来,记录一下每个数字在哪个环上,环长,环上位置实际对应的数字,这都是简单的,可以一并记录。

Key Observation 1 倍增置换。考虑第一次置换后 i 位置的情况,记为 bi,则 bi 显然等于 aai。考虑第二次置换,设 i 位置上的数被置换到了 ci,则 ci=bbi=aaaai。于是置换 k 次走 2k 步。 Permute K times' Trick

考虑到 k 非常大,于是我们可以使用快速幂,后面的问题就简单了。于是这题我们就做完了。

011. [ABC377G] Edit to Match 1782 蓝牌题

这场 G 是不是有点简单,这个字典树提示的其实有点明显了。首先数据范围给的是 |S|,说明与总长度有关,联想到 Trie 的节点数也与总长度有关。所以每次插入一个串到字典树中。

然后肯定是前缀能取就取,后面的话就取一个最短的后缀。最短后缀就记一个数组,最后随便统计一下就做完了,这题是真水题了。

*012. P5999 [CEOI2016] kangaroo 省选/NOI-

连续段动态规划 / 插入类型动态规划 / kangaroo's Trick

非常强的一个动态规划技巧。常见的类型如有一个波浪形的限制,就像这道题一样。还有排列计数和不能重复用一些数这种限制。还有 n 最好在 n3n2 范围内,这是比较可以接受的。

首先我们不考虑起点 s 和终点 t 的限制,只考虑求任意起点和任意终点的排列。

定义 fi,j 为用了前 i 个数,连续段个数为 j 个的排列方案数。我们一般情况下的转移为:

  1. 增加一个连续段:fi,j=fi,j+fi1,j1×j。因为原先有 j1 个连续段,所以有 j 个空格可以插入。
  2. 合并两个连续段:fi,j=fi,j+fi1,j+1。原先有 j+1 个连续段,有 j 个空格可以供插入合并。

需要注意的是,因为数组天然有序,所以我不管怎么合并都是满足条件的。

下面考虑 st 的限制。考虑如果 i=si=t。那么它们有两种转移。一个是插入到第一个/最后一个连续段的开头/末尾,一个是另起第一个连续段/最后一个连续段。就是 fi,j=fi1,j+fi1,j1

同时前面的另起连续段部分也要变一变。因为在插入 st 之后我们就已经钦定了所谓的“第一个”和“最后一个”连续段。所以在 i 足够大的时候我们就不能往序列两边插入连续段了,这样会破坏起点和终点的性质。最后转移变为 fi,j=fi,j+fi1,j1×(j[i>s][i>t]),其中 [p] 为艾弗森括号,p 为一个命题,若 p 为真,则 [p]1,否则 [p]0

然后这道题我们就做完了。

*013. P7967 [COCI2021-2022#2] Magneti 省选/NOI-

有了上一题的铺垫,我们这道题就是相对简单的。考虑 kangaroo's Trick,进行连续段动态规划。

考虑先按照 ri 排序,方便转移。记 fi,j,k 表示考虑了前 i 个磁铁,连续段个数为 j,占用空间为 k 的方案个数。这题我们有三种转移(有一种是在这题可行,而在 kangaroo 那题是不合法的)

  1. 增加一个连续段:没啥好说的,就是 fi,j,k=fi,j,k+fi1,j1,k1×j
  2. 合并两个连续段:就是 fi,j,k=fi,j,k+fi1,j+1,k2×ri+1×j,这也是简单的。
  3. 插入到已有连续段的两端:其实也不难,就是 fi,j,k=fi,j,k+fi1,j,kri×j×2×2 的原因是连续段的两边都可以插入。

最后答案就是 fn,1,i×(li+nn),后边那个组合系数是插板法得出的。

于是这道题我们就做完了。

014. P8865 [NOIP2022] 种花 普及+/提高

补了补 NOIP2022,发现第一题还是比较水的,就非常常规。

首先考虑 'C' 字型怎么做,非常简单,预处理出右边最长 0 串个数,记为 ri,j。然后你考虑怎么构成这个 'C',其实就是下面随便选一行,随意选一个可行长度,然后你自己这一行随意选一个可行长度。你考虑由乘法分配律对后面可行的状态做一个后缀和,记为 sumci,j,答案就是 sumci+2,j×ri,j

然后考虑 'F' 字型怎么做,稍微难一点点,考虑预处理出下面最长 0 的个数,记为 di,j。然后你考虑 'F' 和 'C' 有啥关系。无非就是 'C' 下面拼了一行,'F' 下面拼了一个 'L' 型的东西,这是一样的,记一下 sumfi,j=sumfi+1,j+di,j×ri,j,答案就是 sumfi+2,j×ri,j

然后我们就做完了,过于简单了。

*015. P3047 [USACO12FEB] Nearby Cows G 普及+/提高

历史遗留问题了属于是。大概是两年前剩的题了。现在依然不会,没有长进,恼!

考虑 fi,j 为树上第 i 个点,距离为 j 的点权和。考虑到其实 fi,j 是很不好转移的。所以我们设立另一个状态 gi,j,代表树上第 i 个点的子树内,与 i 距离为 j 的点的点权和。

首先我们可以一遍 DFS 求出 gi,j 这是简单的。就是 gu,0=augu,i=gv,i1

然后考虑怎样求出 fi,j,考虑子树内的贡献,就是 gi,j。子树外的贡献我们使用容斥原理,就是 fu,i1gv,i2 ,我觉得这个式子非常困难,最好画图理解一下。也有可能是我容斥学的过于的不好了。

小优化:注意到我们可以重复利用 fi,j 这个数组。但为了避免重复/缺少贡献,我们可以在换根的时候倒着枚举 k 来解决这一个问题。但其实不用这个优化也没什么事。

016. P3919 【模板】可持久化线段树 1(可持久化数组)

学了可持久化线段树,这是板子。注意 m 个版本,每次更新的时候需要动态开点。开新点的时候需要先继承历史版本这里所对应点的数据。其它就是实现问题了。我把代码贴上。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
int n, m, A[MAXN], root[MAXN], Top = 0;
struct Seg
{
	int L, R, val;
} Sgt[MAXN << 5];
int BuildTree(int u, int L, int R)
{
	u = ++ Top;
	if(L == R)
	{
		Sgt[u].val = A[L];
		return u;
	}
	int mid = (L + R) >> 1;
	Sgt[u].L = BuildTree(Sgt[u].L, L, mid);
	Sgt[u].R = BuildTree(Sgt[u].R, mid + 1, R);
	return u;
}
int Clone(int u)
{
	Top ++;
	Sgt[Top] = Sgt[u];
	return Top;
}
int Update(int u, int L, int R, int x, int val)
{
	u = Clone(u);
	if(L == R) 
	{
		Sgt[u].val = val;
		return u;
	}
	int mid = (L + R) >> 1;
	if(x <= mid) Sgt[u].L = Update(Sgt[u].L, L, mid, x, val);
	else Sgt[u].R = Update(Sgt[u].R, mid + 1, R, x, val);
	return u;
}
int Query(int u, int L, int R, int x)
{
	if(L == R) return Sgt[u].val;
	int mid = (L + R) >> 1;
	if(x <= mid) return Query(Sgt[u].L, L, mid, x);
	else return Query(Sgt[u].R, mid + 1, R, x);
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	cin >> n >> m;
	for(int i = 1; i <= n; i ++) cin >> A[i];
	root[0] = BuildTree(0, 1, n);
	for(int i = 1; i <= m; i ++) // 更新版本
	{
		int rt, opt, x, y;
		cin >> rt >> opt >> x;
		if(opt == 1)
		{
			cin >> y;
			root[i] = Update(root[rt], 1, n, x, y);
		}
		else
		{
			cout << Query(root[rt], 1, n, x) << '\n';
			root[i] = root[rt]; // 这里也要新建版本
		}
	}
	return 0;
}

017. P3834 【模板】可持久化线段树 2

权值可持久化线段树,先对权值进行离散化,也就相当于离线了。加入区间的每个点时对可持久化线段树建立一个新的版本。询问的时候线段树权值相减,然后线段树上二分板子即可。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
int n, m, A[MAXN], B[MAXN], tot = 0, Top = 0, root[MAXN];
struct Seg
{
	int L, R, val;
} Sgt[MAXN << 5];
int BuildTree(int u, int L, int R)
{
	u = ++ Top;
	if(L == R)
	{
		Sgt[u].val = 0;
		return u;
	}
	int mid = (L + R) >> 1;
	Sgt[u].L = BuildTree(Sgt[u].L, L, mid);
	Sgt[u].R = BuildTree(Sgt[u].R, mid + 1, R);
	return u;
}
int Clone(int u)
{
	Top ++;
	Sgt[Top] = Sgt[u];
	Sgt[Top].val ++;
	return Top; 
}
int Update(int u, int L, int R, int x)
{
	u = Clone(u);
	if(L == R) return u;
	int mid = (L + R) >> 1;
	if(x <= mid) Sgt[u].L = Update(Sgt[u].L, L, mid, x);
	else Sgt[u].R = Update(Sgt[u].R, mid + 1, R, x);
	return u;
}
int Query(int u, int v, int L, int R, int k)
{
	if(L == R) return L;
	int mid = (L + R) >> 1, xx = Sgt[Sgt[v].L].val - Sgt[Sgt[u].L].val, ans;
	if(k <= xx) ans = Query(Sgt[u].L, Sgt[v].L, L, mid, k);
	else ans = Query(Sgt[u].R, Sgt[v].R, mid + 1, R, k - xx);
	return ans;
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	cin >> n >> m;
	for(int i = 1; i <= n; i ++) 
	{
		cin >> A[i];
		B[i] = A[i];
	}
	sort(B + 1, B + n + 1);
	tot = unique(B + 1, B + n + 1) - B - 1;
	root[0] = BuildTree(0, 1, tot);
	for(int i = 1; i <= n; i ++) // 对可持久化线段树建立 n 个版本
	{
		int x = lower_bound(B + 1, B + tot + 1, A[i]) - B;
		root[i] = Update(root[i - 1], 1, tot, x);
	}
	while(m --)
	{
		int L, R, k;
		cin >> L >> R >> k;
		cout << B[Query(root[L - 1], root[R], 1, tot, k)] << '\n';
	}
	return 0;
}

018. P5829 【模板】失配树

笑点解析,我联考模拟赛 T2 没看出来这是失配树。

根据 KMP 数组的链式传递性(这是熟知结论)。我们可以对文本串 s 的任意前缀的 border 建立出一棵支配树。对于每个位置 i,它的父亲 fi 一定是 1i 这个前缀串的最长 border。而我们考虑位置 i1 形成的这一条链是什么东西,显然就是 1i 的这个前缀串的所有 border 了。

我们跑 KMP 的时候把失配树上每个节点的父亲建立出来。然后跑一遍树上倍增预处理。

对于前缀串 s1...p 和前缀串 s1...q,它们的最长公共 border 就是 pq 两个点在失配树上的 'LCA'。但其实这个 'LCA' 并不是真正意义上的 LCA,这种 LCA 的定义是:

  1. 如果 uv 有祖先后代关系,那么其 'LCA' 就是它们真正 LCA 的父亲。这是为了保证一个串的 border 不为另一个串本身,这不符合 border 的定义。
  2. 如果 uv 没有祖先后代关系,那么其 'LCA' 就是 uv 真正的 LCA。因为不会出现互相包含的情况。

然后这道题我们就做完了。顺便说一下,联考那个所谓广义 border 的求法就是把祖先后代关系的那个特判删掉,变成了真正的 LCA,因为广义 border 规定了一个串的 border 可以是它自己。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
string s;
int n, m, depth[MAXN], Parent[MAXN][25], lg[MAXN]; // 失配树 
int LCA(int u, int v)
{
	if(depth[u] < depth[v]) swap(u, v);
	while(depth[u] > depth[v])
		u = Parent[u][lg[depth[u] - depth[v]]];
	// if(u == v) return u;
	for(int i = lg[depth[u]]; i >= 0; i --)
	{
		if(Parent[u][i] == Parent[v][i]) continue;
		u = Parent[u][i]; v = Parent[v][i];
	}
	return Parent[u][0];
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	cin >> s >> m; n = s.length(); s = ' ' + s;
	depth[1] = 1; 
	for(int i = 2; i <= n; i ++) lg[i] = lg[i >> 1] + 1;
	for(int i = 2, j = 0; i <= n; i ++) 
	{
		while(j > 0 && s[j + 1] != s[i]) j = Parent[j][0];
		if(s[j + 1] == s[i]) j ++;
		Parent[i][0] = j; depth[i] = depth[j] + 1; 
	}
	for(int j = 1; j <= 24; j ++)
		for(int i = 1; i <= n; i ++)
			Parent[i][j] = Parent[Parent[i][j - 1]][j - 1];
	while(m --)
	{
		int u, v;
		cin >> u >> v;
		cout << LCA(u, v) << '\n';
	}	
	return 0;
} 

019. P5838 [USACO19DEC] Milk Visits G

我们考虑将每一类点的 dfn 值统统按有序形态压入 n 个 vector 里面。注意到一共只有 n 个点,所以空间开销就是 O(n)

那么现在问题变为:在 uv 的路径上,存不存在一个点 k,使得 k 的 dfn 在 c 类点的 vector 里面。这时候我们沿用 P4092 [HEOI2016/TJOI2016] 树 的操作,考虑在重链上做操作。已知我们重链的 dfs 序是连续的,所以问题变为这一段区间和 c 类点的 dfn 这两个集合有没有交。这可以使用重链上二分实现。然后处理 uv 路径上的每一条重链即可。时间复杂度 O(mlog2n)

这其实我还写了 10 道树剖,但是都太板子了,我不想放上来了,顶多还有一个边转点的 Trick 但那玩意儿是显然的。

020. P11233 [CSP-S 2024] 染色(非常重要!!!)

考场降智,连 O(n2) 做法都想不出来,我就是纯唐,唐到家了,我觉得我真没救了。这里只讲 100 pts 的做法。

考虑 fi 代表前 i 个数染色的最大答案。我们其实根本不关心你染了啥颜色,我们只关心你是从哪里转移过来的。

首先你可以和前面的和你相同的点染不同的颜色,但其实也不一定是不一样,反正你可以随便染,那么你的贡献起码是 fi1,那么我们的 fi=fi1,这是第一种转移。

第二种你前面从和你一样的点转移过来,那么我们可以证明你只有从离自己最近的相同点转移才是最优的。那么你的贡献就是 flasai+1+ai+p,其中 p 是一个量,代表 i 到离它最近的相同权值点的区间内的异色贡献。考虑异色怎么贡献。区间内的颜色都是一样的,所以就是相邻贡献,如果相邻位置一样,那么贡献就是 ai,否则就是 0。这显然可以用前缀和优化动态规划解决。然后做完了。

*020. [ARC058E] Iroha and Haiku *2473

过于神秘了,一眼丁真为我做不出来的题。

首先我们肯定知道如果正着做肯定是不好做的,一个很直接的原因就是贡献会算重复。然后就寄了。所以我们算有多少个序列是不好的。

然后我们考虑到它的 x,y,z 特别小,所以其实我们非常容易联想到状态压缩动态规划。事实证明我猜对了,但是我会不了一点。我们发现不仅 x,y,z 小,x+y+z 也在可接受范围内。于是我们考虑有没有关于 x+y+z 这个常数的时间复杂度。Iroha and Haiku's Trick

我们定义 fi,j 为考虑到第 i 个位置,其后缀和存在状态为 j 的方案数。 就非常神奇的一个状态定义。因为我们考虑到我们关心的后缀和情况最多只有 17 位,所以我们考虑设计这个状态。

什么情况下这个后缀和串是不好的呢?非常简单,有三种情况:

  1. 不存在一个后缀和等于 z
  2. 不存在一个后缀和等于 y+z
  3. 不存在一个后缀和等于 x+y+z

至于怎么计算后缀和串,这是简单的。我们考虑枚举 k110,那么加上这位后缀和就从 j 变为了 ((j << k) | (1 << (k - 1))) & ((1 << (X + Y + Z)) - 1)。这是容易理解的。然后就是普通的计数,后面是简单的。

021. [ARC060D] Digit Sum *2261

非常神的题,但是我自己想到了。

根号分治。首先如果 bn,那么直接暴力枚举求得数位和是否为 s

如果 b>n,我们注意到有一个结论,其最小的可能的进制就是 nsi+1。注意这里要保证 nsi 的倍数。记 p=nsi+1,最后还要判断一下 f(p,n) 是否等于 s。然后我们就做完了。注意要特判一下,如果这里的 p=1,那么我们直接扔掉。不扔掉的话会死循环寄掉。

然后做完了,代码不长但根号分治一般都很神。

022. [ARC060E] Tak and Hotels *2154

倍增优化动态规划,我应该还是第一次理解这个东西。朴素的暴力就是暴力枚举,这里不做过多讲解。

考虑正解,设 fi,j 为从 i2j 步能跳到的最远点。然后每次 Query 的时候你都往上调,基本就类似最近公共祖先 LCA 的那种求法随便倍增。然后就做完了。注意一下处理 fi,0 的细节。还有数组空间别开小(我就因为这个赛时没过 E 题)

然后就做完了。

023. [ARC060F] Best Representation *2804

Subtask 1:|s|4000

考虑一个 O(n2) 的做法。看到划分想到动态规划。这个应该是简单的,设 fi 表示前 i 个能划分的最少子串个数。gi 表示划分成最少子串个数的方案数。但是我们需要一个 Qi,j 表示 ij 的子串是否是好串。

注意到好串的定义就是之前题目里面的 Power Strings。然后就递推跑 KMP,应该是跑 n 遍 KMP,每次跑 in 这个子串,预处理出 Qi,j。动态规划的时间复杂度是 O(n2),跑 n 遍 KMP 的时间复杂度也是 O(n2),所以总复杂度就是 O(n2),然后就做完了。

Subtask 2:|s|5×105

首先特判:

  1. 如果字符串内的元素全部相等,那么必然要划分成 n 个串,个数就是 1 个。
  2. 如果字符串没有长度 >1 的周期或者 |s|=1,那么可以不用划分,最小划分个数就是 1,个数也是 1

后面有一个非常强的结论,我不想证。就是如果存在周期长度 >1,那么一定可以从第一个字符后面切开,使这个字符串变为两个串,那么这两个串一定不是 Power Strings。这点通过对于暴力 f 数组的打表也可以发现,所以这种情况的最小划分个数就是 2

然后怎么计数呢?因为是划分成两段,所以我们很自然的想到枚举分界点,然后看前面一段是不是 Power Strings,然后看后面一段是不是 Power Strings,因为 KMP 跑出的 border 具有对称性,所以可以通过正反串两次 KMP 得出答案。最后判断是 O(1) 的,上面讲过怎么判断 Power Strings。

024. CF1706E Qpwoeirut and Vertices 2300

连通性问题。考虑什么边才能对 “连通” 这一性质有贡献。非常简单,在无向图上只有树边有贡献。其他的返祖边都是无用的。这启发我们考虑最小生成树。

我们考虑把加入时间看做边权。对于询问 [L,R]。我们的答案就是将 [L,R] 连通的最小生成树上的最大边权。而 [L,R] 是难以维护的,于是我们考虑转化问题,将 [L,R] 两两连通转化为对于任意的在 LR1 中的 iii+1 连通。然后 [L,R] 的代价就是其所有的拆的这些区间的贡献的最大值。

考虑怎么维护 ii+1 连通的贡献。这其实用朴素最小生成树的话也很难维护。但是我们可以刻画维护最小生成树的过程,建出 Kruskal 重构树。而 ii+1 的贡献就是 LCA(i, i + 1) 的点权。

然后就是区间维护最大值问题,使用 ST 表解决即可。

然后这道题我们就做完了。

025. P9186 [USACO23OPEN] Milk Sum S

制杖题,细节这么多,害我写 40 min!

考虑先贪心。肯定小的排前面,大的排后面。修改就是把一个数抽走,再二分把这个数加进来。

然后你考虑把一个数抽走会发生什么。就是后面的贡献减去后面的后缀和,然后再减去自己的排名乘以自己的权值。

然后你考虑加入一个数会发生什么,反过来,就是贡献加上自己后面的后缀和,再加上自己的排名乘以自己的权值。注意这里有一个细节,我们统一把数插到第一个比自己大的位置,然后把比自己大的位置搞到后面。

注意判断两个位置之间的关系,这里还会有很多细节,不一一列举。

*026. CF1687C Sanae and Giant Robot 2500

题目大意非常清晰,这里不再复述。非常好的一道题。

Key Observation 1 你考虑一个转化,就是 ai=bi 意味着什么?是不是意味着 aibi=0?如果你认为单凭题目描述和这个无厘头的转化毫无连接的话,我推荐另一种思考方式,也就是 CF 的 Hint1。即我们考虑当 bi=0 的时候,a 数组应该是什么样的?答案就是全部为 0 对吧?那 a 数组全部为零让你想到了什么等价转化呢?a 数组的前缀和数组 sum 全部为 0 对吧?好那我们到此处转化完毕。考虑解决 bi0 的问题。其实是一样的,我们用 aibi,而 sumi=sumi1+(aibi),这样我们把这个复杂的问题化归成为了上面那个 bi=0 的简单问题,然后我们就可以处理了。Giant Robot's Trick 1

Key Observation 2 你考虑怎样去刻画区间的复制操作。在前缀和 aibi 意义下,其实这就等价于当 suml1=sumr 的时候,将 suml...r1 全部都赋值为 sumr,因为区间内和为 0。但是你又考虑到当 sumr 不为 0 的时候,区间推平是没有用的,于是我们可以考虑把 sumi=0 的位置 i 全部塞进一个队列里面,进行类广度优先搜索状物。而 sumi0 的塞入一个 set 里面,方便弹出。Giant Robot's Trick 2

然后我们进行暴力广度优先搜索,每次找到区间所在位置,暴力把 set 内在区间里的数的前缀和变为 0,再加入到队列里面。复杂度分析:每个区间只会考虑两次,即左右两端点进队,所以复杂度为 O((n+m)logn)

027. CF2038K Grid Walk 2100

(1,1)(n,n) 的最小权值路径,权值定义为 gcd(i,a)+gcd(j,b)

我们可以很容易的注意到同行权值一相同,同列权值二相同。注意到行列的格子必定会走一遍。所以我们尽量走权值一特别小和权值二特别小的格子。我们考虑先横着走,走到权值二为 1 的格子,然后一直向下走,走到权值一为 1 的格子。

然后有一个非常强的 Trick,这样找到两个位置 x,y 后,可以保证 nx25,ny25Grid Walk's Trick

后面的直接动态规划解决,做完了。

posted @   DataEraserQ  阅读(30)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示