四月份训练手记
4.1 - 1 - Problem - H - Codeforces
思路
这道题主要是考察线段树的应用
不难发现,对于某一个字符串(不做修改
- 以
结尾的答案总贡献和子串数量 - 以
结尾的答案总贡献和字串数量
至于转移方法,我们只需要在新加入一个不同字符的时候更新贡献与子序列数量即可
那为了支持修改,我们需要使用线段树来维护以上信息,但线段树维护的是区间,而不是前缀
所以我们需要对子串的开头和结尾都进行分类
并且在合并的时候,考虑所有情况的贡献,减去那些中间字符相同(比如
收获 & 反思
线段树的又一应用方法
对于区间上贡献具有一定的 **“可加性” 或者 “分类可加性”**,的时候,就要考虑使用线段树来维护
对于 “线段树是在维护幺半群上的信息” 这句话的理解又加深了一点
4.1 - 2 - Problem - I - Codeforces
思路
我们先考虑,如果是朴素的贪心,怎么来解决问题:
- 首先对于每一个有序对
都可以用贪心统计答案:先遇到哪个字符,我就先加入序列 - 不难证明,这样找到的序列一定是最长的
思考一下,这样的贪心解法对于我们的解答有什么启发呢?
进一步地说,在这种贪心策略下,某一个位置的字符,在什么情况下才会被 “收录” 到某一个有序对
考虑到我们贪心的过程,答案也是显然的:
- 在某一轮贪心中,只有这个字符与前一个相同字符之间,有一个目标字符(有序对中的另一个字符
这个字符才会被 “收录”) , - 如果没有前一个字符(首次出现)则一定会被收录
那我们的贡献统计就变得简单起来了,对于每一个位置:
- 如果是第一次出现,一定会产生贡献,
之所以要减一是因为它不能和自己构成有序对(题目限制) - 否则,我们找它和前一个相同字符之间,统计其中不同字符的个数,就是它的贡献值
问题又转化为了:如何统计区间上的不同字符个数
对于普适的做法,我们可以使用主席树(可持久化权值线段树)来统计区间上不同数值的个数
但这道题不需要那么复杂,因为我们要求的区间具有 右节点严格非降的后缀区间 性质,在这个条件下,就可以记录每个字符最后一次出现的位置,并且标记为
收获 & 反思
思路相当值得学习!
这道题的几个关键点:
- 用贪心简化问题,便于考虑
- 用贡献法转化问题,使得枚举量变为
- 利用待统计区间的特殊性质,用树状数组完成区间字符种数的统计
4.2 - 1 - Problem - G2 - Codeforces
超级神秘好题
思路
我们考虑对于一组数
不难注意到,
于是题目要的条件就变成了
并且
一个很自然的想法就是,我们枚举所有可能的
但是这样子做有一个问题:我们前两层枚举的时间复杂度是
如何才能保证时间复杂度严格低于限制呢?
我们考虑上一个方案的缺点是什么,就是对于一个体量很大的
考虑使用预处理来提高效率
我们的检查是在做什么?是在检查每一个可能的
为什么这样时间复杂度是正确的?
首先每个数只会在初始阶段被枚举一次,对应的就是二层的枚举量严格小于
收获 & 反思
这道题做法难度不高,但是其中涉及到了较为复杂的时间复杂度优化想法与时间复杂度的正确性证明
解决的方法有很多,但是其中相当一部分可以被精心构造的数据卡到
4.3 - 1 - Problem - E3 - Codeforces
这次会用倍增了
思路
看到这道题就想到了应该可以倍增,因为:
的范围巨大,显然无法直接计算 - 题目中对于转移的限制较低,可能可以转化为统一计算的方案
但是倍增的对象还需要精心选择:
- 如果选择每个木屋的方案数,在统计的时候并不方便,而且时间会超出限制
- 如果选择第
天的方案数,则没有有效的转化手段
这个时候应该思考到:什么东西状态量少,可以倍增(可以转移
不难发现,如果抛开限制,我们既然每天都要前往湖心岛,那我们就可以把这个时候当作一个节点,从湖心岛出发之后再回来的方案数是可以计算的,还支持倍增
但是考虑到题目中的条件,那就要求我们前一次到达湖心岛与后一次离开必然有一次走短路,所以就可以分类维护出发与回来走的路的种类,然后进行条件转移
最后的答案就是考虑从
收获 & 反思
合理设计倍增的对象,就可以变魔法!
4.3 - 2 - Problem - B2 - Codeforces
思路
因为
所以也有
于是我们就可以枚举互素的
所以合法的
不难发现这里的
收获 & 反思
善于运用互素结论,可以更好的分析题目
4.4 - 1 - Problem - D - Codeforces
思路
赛时脑子抽了
其实就是一个背包
记每个字母的数量是
那转移方程就是:
需要注意其中一些项的转移条件
第二种方法就是,先用数学方法推导发现奇偶性排列的方案与最终字符串排列的方案无关,故可以直接先求出方案数,最后再计算组合数,本质上是一样的
收获 & 反思
需要针对题目选择不同的解决方案
4.4 - 2 - Problem - D - Codeforces
思路
不难注意到,对于最大值我们是一定要选取的,证明如下:
- 如果不选最大值,那么至少会带来一点减益
- 此时去选最大值,最小值一定非降,
至多减一,使用一定不会更劣
那么我们就可以考虑枚举我们要取到的最小值,我们可以把每一个数按照大小先排序,然后从大到小加入
我们可以用并查集来维护答案:
- 对于每一个连成一段的区间,我们最优就是每隔一个去选取,并且区间长度为奇数的时候,要从第一个开始选取
- 我们可以记录在这些区间里最大值出现了几次(偶数长度区间就取较大值)然后判断需不需要牺牲一个位置来取最大值
- 每一轮维护之后,答案就是
这里的 是各个区间统计的可以取到的最多格子数量,并且经过了判断是否需要减一(牺牲一个格子保证最大值)
收获 & 反思
维护最大值能不能被取到的时候,可以维护区间数量而不只是 “能或不能
对于这样取最值的题目,充分考虑性质之后,可以使用动态方法维护 + 枚举部分条件的方法求解
4.4 - 3 - Problem - H - Codeforces
思路
这是经典的 2-SAT
问题,每个元素选与不选就是每个
然后,对应的,2-SAT
问题的子句就是对应的 B 矩阵中的每一个纵列,任何两个元素不能同时为
收获 & 反思
学习了 2-SAT
使用 Tarjan-SCC 求解
4.5 - 1 - Problem - E - Codeforces
思路
很简单,一眼就是求
首先判断,对于两个点,如果他们之间的距离是奇数,一定无解
其次,如果两个点重合,则任意一个点都可行
再次就是,如果两个点深度相同,那么他们简单路径的中点(也就是
其他的点,也就是有解并且到
收获 & 反思
倍增求祖先的简单运用,没什么好说的
4.5 - 2 - Problem - E - Codeforces
思路
赛时没做出来纯糖哇
这是一个 “合理拆贡献” 的好例题,如果不能合理拆分贡献,就会使得这道题很难做(当然你会 NTT 大力出奇迹当我没说)
首先明确题意:对于所有可能的排列,求出所有区间
一个很自然的想法是,我们考虑对于每个区间,枚举其可能的
所有我们需要改变求解思路,考虑贡献法
这里有一个很巧妙的转化,我们把所有区间的
为什么这样是对的呢?不难发现,
这是贡献法中的一种 “差分思想
那我们该怎么统计这类集合呢?又该怎么求解呢?
首先我们注意到,如果要满足前
所以我们在考虑区间可行的最大
换而言之,如果出现在数组中小于
所以,我们就可以统计出
然后对这个数组进行第一维上的后缀求和,新数组
然后对于
就这样就做完了
4.6 - 1 - Problem - 1997E - Codeforces
思路
不妨把条件做一下转化:
- 对于第
只怪物,如果前面发生过至少 场战斗,那么怪物会逃跑,否则将发生战斗
那我们不难发现,对于任意时刻,
那么我们只需要计算对于某个位置,$ans [i] $ 记录当
也就是说,对于
对于每一个位置,我们可以用树状数组维护并实现查询
那么就做完了
收获 & 反思
很巧妙的转化思路,一些可能的思考启发是:
- 对于此类问题,考虑 “什么东西具有单调性
在此处是每个点处的战斗力与” , 的关系,进而是每个怪物是否会发生战斗与 的关系 - 然后就是树状数组的应用,应该考虑到答案可以以什么形式来存储并维护
4.7 - 1 - Problem - D - Codeforces
思路
很巧妙的思路
对于
对于剩余情形:
- 若
为偶数: - 我们令前
个数与后 $\frac n2-1 $ 个数为 'a'
,这样保证了所有仅由'a'
组成的子串出现的次数均为奇数(前后出现次数相差一) - 然后令中间为其他任意字母即可
- 我们令前
- 若
为偶数 - 与奇数处理方法类似,只不过中间两个字母为任意两个不同的其他字母
构造完毕
收获 & 反思
题目不难,但是思路很优美
考虑构造一个东西是奇数,就可考虑构造两个部分,使得这两个部分一个是奇数,一个是偶数(特别地,可以使得两个部分相差一)即可
4.7 - 2 - Problem - G - Codeforces
思路
很自然的想法是,我们无法确定什么时候可以出发,所以不确定通话在出发后多久开始,所以不妨将整个过程逆转,考虑从活动地点到家里的最短耗时,这样通话区间与出发时间的关系就是确定的了
我们考虑一个最快的流程应该是怎样的:
- 在通话开始前就可以到家
- 在通话开始前下车,在通话结束前步行到家
- 在通话开始前下车,在通话结束后步行或坐车到家
那我们就可以考虑分几个部分来解决问题:
- 先考虑在
之前,坐车能到哪些位置,此时如果能直接到家则得到答案 - 然后新建一个图作为通话时的情形,建上所有步行边,同时从
点向上述坐车能到达的点连一条有向边,长度即为花费的时间 - 再求一轮单源最短路,看看能不能步行到家
- 如果不能,我们就枚举打完电话后上车的位置,在打电话前花费的时间就是
,打电话后花费的时间就是坐车到家的最短开销
做完了
启发
对每个过程都做贪心处理,用有限的枚举充分考虑所有情况
4.7 - 3 - Problem - H - Codeforces
思路
用前缀和转化
一个是原矩阵的前缀和,一个是给所有数都乘上顺序权值的前缀和,还有一个给所有数按行乘上权值的前缀和
用这三个前缀和合理组合就可以计算出答案,没什么难的
收获 & 反思
没什么好说的(才不会告诉你 for 循环写锅了一次)
4.8 - 1 - Problem - 765D - Codeforces
思路
不难发现,如果有
然后考虑
那我们从必要条件入手,对于每一个
那就考虑第一个条件,除此之外,每一个
- 对应的位置如果还没有确定映射关系:
- 有没有可供映射的
,如果没有就 Fail
- 有没有可供映射的
- 如果已经有映射关系:
- 映射的值是否正确,如果不正确就 Fail
如果都满足,那么就是一组合法的解
收获 & 反思
不错的一道思维题,有点绕绕的,最重要的是抓住 “
4.8 - 2 - Problem - 2075F - Codeforces
思路
首先转化题意,即求最大的一个子序列,使得左端点唯一严格最小,右端点唯一严格最大
然后我们将数组转化为二维点
也就是说,对于这样的
然后我们考虑一个点可以在取那些左右端点组合的时候产生贡献:
- 由于所有可能作为左端点(右端点)的点是单调的,所以这个贡献区间一定是形如
这样的连续区间 - 那我们统计贡献就可以转化为将其中一维化为坐标轴,对另一维扫描线计数,线段树维护最值即可
做完了,思路
收获 & 反思
很优美的思路,我可能还没办法总结出什么,但是有以下几点:
- 考虑可能的贪心,他可能带来让问题变得可解的关键性质
- 善于应用单调栈
- 善于考虑贡献的转化,能否用贡献求解
- 对于形如在两个维度上的连续区间的限制,可以转移到 “对一个矩阵区域有贡献” 上,进而可以使用扫描线处理(以其中一个维度作为坐标轴)
4.9 - 1 - Problem - 1666E - Codeforces
思路
看到求可行的最值,考虑二分
首先是二分路段的最大差值
显然我们可以枚举这个差值下,路段长度的上下界
然后对于这个上下界,我们显然是可以判断出可行性,并且若可行,我们可以逆过来求出一组合法的解的
同时不难发现,如果不可行,必然是因为太长 / 太短,这是什么?这是宝贵的单调性啊!
也就是说,我们可以通过二分
就做完了
收获 & 反思
"Stop learning useless algorithms, go and solve some problems, learn how to use binary search."
4.10 - 1 - Problem - 1458C - Codeforces
思路
这个考虑非常巧妙
我们将每一个矩阵中的元素记作一个三元组
那么就不难发现,每一个元素在对应操作下的 “去向
: : : :
于是我们记录这些变化的复合,最后对于每一个元素应用变化,就可以得到目标矩阵
收获 & 反思
如果发现对于一个矩阵上的元素很难维护 “每一个位置上的数
这对于一些有固定变化方式的运算有奇效
值得好好学习的一道题!
4.11 - 1 - Problem - B3 - Codeforces
思路
关注到一个很重要的点:每一列有且只有一个房子,并且距离是在
那么我们从第一列开始考虑,把第一个数放在
这样子我们就可以把之后的所有房子都成功匹配,但是问题来了,第一个房子怎么办?
很容易想到的是,如果数列中有某一个
如果没有
最后,如果没有
- 如果
那么无解 - 否则,可以让
分别位于 ,这样使得第一个可以合法匹配
到这里就做完了
收获 & 反思
需要注意条件的特殊性质,比如:
- 每一列有且仅有一个
- 根据
的大小一定能找到解 - 构造特殊的开头使得第一个房子得以匹配
4.11 - 2 - Problem - D - Codeforces
思路
一个很自然的思路是,如果
此外,如果
所以除了这两种情况就是无解
收获 & 反思
又是一个充分条件入手证明必要性的构造题
4.12 - 1 - Problem - E - Codeforces
思路
其实很简单:
- 不难注意到,其中的最大值一定同时作为(最后一个)前缀最大和(第一个)后缀最大出现
- 同时,第一个和最后一个也一定是前 / 后缀最大值
- 也就是说,不满足上面两个条件的,就一定是不合法的条件组
那我们考虑,其余情况都有合法的解吗?如果是,我们怎么统计解的总数呢?
我们首先明确,最大值左边显然不会有后缀最大值,同样的右边不会有前缀最大值
那就是把左右两边分开求解了,这种情况下,由于没有重复元素,所以无论怎么分配都是等价的
那我们只考虑其中一边,以前缀最大值为例:
- 既然已经确定了后一个前缀最大值在位置
(最开始的时候就是最大值的位置 那也就说明前面还有) , 个数,再考虑前一个前缀最大值 - 因为前一个前缀最大值是往前首个前面没有任何数比他大的数,所以前面所有数中的最大值不能在他前面,也不能在他后面,所以他就是前面所有数中的最大值
- 那么还剩下
个数可以被支配,且他们都比选出的数小,那方案数就是从中任选 个数随机排列放在前后两个前缀最大值中间 - 再往前也是一样的子问题形式,只需要将左右方案数都求出来,最后乘上最开始分配两边数的方案数即可
至此问题得解。
收获 & 反思
考虑把问题分解为子问题,不重复不遗漏,最简化问题建模,这是需要大量训练的
手动取模又出锅了调了快一个小时,我是傻逼