IOI2025 集训队互测记录

Round 1

87.5+0+25,垫底了,我怎么能这么唐的。

飞带长队怎么这么牛,三个题都好厉害!点赞了。

开场开 T1,做了一会之后发现会了 40 分的判定。于是逮住这个判定研究了大半天,手动模拟之后猜到了划分出来的每个串的数量的条件大概是 \(\ge \max b_i-a_i\) 这种形式的,发现可以很容易 \(O(n^5)\) DP 这个东西。然后这个题采取了一种奇妙的回答方式,你只需要在一个测试点的不同的测试组中选一个作答就行了,但是我以为你需要回答一个前缀,于是我的算法就变成 \(O(n^5 T)\) 的了,只拿了 87.5 分。考完问 zhy 他是怎么切的,他告诉他也是五方,原来我已经想到正解了,气急败坏……气急败坏……

中途去看 T2,写了几个判定都假了,没有想到直接取序列的前若干项暴力就可以拿很多分了。T2 做了许久还是 0 分,发现时间只剩下半个小时了,最后疯狂 rush 完了 T3 的 25 分暴力。

这场比赛寄就寄在一直在纠结是继续做 T2 的判定还是去想 T1 那根本不存在的更低复杂度做法,从而整场考试都有点心神不宁,没有冷静下来复盘。如果能仔细冷静的观察局势,无论是 T1 理解错出题人的意思还是 T2 部分分一分没拿都是可以避免的。

《基础 ABC 练习题》 from 方心童

AGC055D 加强,查了一下自己水表发现自己某天心血来潮做了 AGC055 两个题就是没做这个题。

考虑一下判定一对 \((x,y)\) 是否合法,记录你目前拥有的 ABC,BCA,ACB,AB,BC,CA,A,B,C 个数,初始 ABC \(x\) 个,BCA \(y\) 个,CAB \(n-x-y\) 个,你发现假设现在你想填入一个 A,那么从 ABC,AB,A 中选一个去掉开头的字母,这样永远只可能会增加以 B 开头的串,于是不难证明先取长的更优,写一写这个判定发现可以拿 40 分。

更仔细地考虑这个过程。在这个过程中,为了保证碰到对应的字母时串的数量是够的,也就是你之前遇到的 C 的数量加上 \(x\) 不少于 A 的数量。这个过程中遇到的这些 C 可以认为他们都解放了一个 A,如果没有,说明这些 C 是从 ABC 转化成 BC 再转化成 C 的,那么在此之前,一定有个 BC 转化成了 C,也就是说所有的 BCA 已经消耗完了,那么所有的 CA 已经完全转化出来了,因此你需要优先消耗 CA 中的 C。综上所述你可以认为消耗单独一个 C 的时刻一定是一段后缀,那么在转化单独的 C 之前,所有的以 A 开头的字符串都已经被转化出来了,不需要继续考虑判定条件了。同理,对于 B 的数量和 C 的数量也是差不多的要求,可以发现这三个条件合起来是充要的。

接下来 DP 就很简单了,记 \(a_i,b_i,c_i\) 表示前 \(i\) 个字符中 A、B、C 的个数,考虑记录 \(\max(a_i-c_i),\max(b_i-a_i),\max(c_i-b_i)\) 是复杂度高的,我们考虑计算 \(a_i-c_i\le X,b_i-a_i\le Y,c_i-b_i\le Z\) 方案数是容易的,简单差反演一下就得到了最大值的方案数了,\(O(n^5)\)

Code

《基础 01? 练习题》 from 郑钧

首先题目中给出的序列就是 Thue-Morse 序列,也就是常见的 \(\operatorname{popcount}(i) \bmod 2\) 构成的分形序列。在连续子序列这道题中,zj 给出了一个刻画 TM 序列所有可能的子串结构的方式。

具体地,如果一个串是 TM 序列的前缀或者是其取补的前缀,那么称这个串是好的,一个串的 TM 拆分即将一个串拆分成两个非空串 \(A,B\),使得 \(A\) 的翻转 \(\operatorname{rev}(A)\) 以及 \(B\) 串都是好的。

我们断言,所有 TM 序列的子串只有三种情况,且满足这三种情况之一的一定是 TM 序列的子串:

  • 单个字符构成的串 \(0/1\)

  • 具有唯一的 TM 拆分点。

  • 具有两个 TM 拆分点,满足将串拆分成了三个子串 \(A,B,C\),使得 \(|A|,|C|\le |B|\)\(B\) 的长度是二的次幂,\(\operatorname{rev}(A+B)\)\(B+C\) 都是好串。

可以看到 TM 拆分的本质是用 \(\operatorname{popcount}\) 奇偶性来刻画 TM 序列时,找到 \(\operatorname{lowbit}\) 最大的那个点拆分序列。上面的性质详细的证明可以看 zj 题解。

如何寻找 TM 拆分呢?发现你只需要求出每个位置最多向左/向右多少位依然是好串。这似乎是带通配符的 LCP 问题,怎么做?

不要被字符串思维束缚了!考虑 TM 序列的性质,其匹配结构是可以倍增求出的。预处理每一个长度为 \(2^k\) 的串是否与 TM 序列或其取反后的前缀相同。

于是先枚举 TM 拆分点往左右扩展,然后容斥掉拥有两个拆分点的情况,这个可以枚举中间 \(B\) 串长度往两边扩展。

接下来计数还是比较 trivial 的。经典的套路,考虑求所有的子串的答案和,相当于若干次区间加之后求历史和,线段树维护即可。

Code

《基础 01 练习题》 from 刘海峰

很有想法的题!可以看作对于二分竞赛图性质深入研究的例题。

题意大概是说每一行 \(A\) 矩阵中等于 \(0\) 的集合包含了 \(B\) 矩阵中等于 \(0\) 的集合,或是 \(A\) 矩阵中等于 \(1\) 的集合包含了 \(B\) 矩阵中等于 \(1\) 的集合。列的话 \(B\) 矩阵中的 01 得反过来。

我们不妨转化一下题意,由于我们每次是将一些位置变成 \(1\),考虑一个位置变成 \(1\) 之后还会有哪些新的要求,如果 \(A\) 矩阵中为 \(1\) 的位置在 \(B\) 矩阵中变成了 \(1\),那么 \(A\) 矩阵同一列的 \(0\)\(B\) 矩阵中一定也要同时变成 \(1\) 才行;如果是 \(A\) 矩阵中为 \(0\) 的位置在 \(B\) 矩阵中变成了 \(1\),那么就对 \(A\) 矩阵同一行中的 \(1\) 作出同样的要求。

这样就转化成了一个图论问题。优化建图之后,相当于每个 \(1\) 连出到列点,被行点连入,每个 \(0\) 连出到行点,被列点连入。题目就是让我们求有多少个包含非行列点的 SCC,暴力 tarjan 获得 25 分。

考虑到每个非行列点只有一个入度一个出度,可以直接看成一条边,那么原图就变成了左右各 \(n\) 个点,中间 \(n^2\) 条不重也不相对的有向边的二分竞赛图。

对于普通竞赛图,SCC 问题是老生常谈的,我们知道竞赛图 SCC 缩点之后的形态是一条链,而且根据兰道定理,我们将出度排序可以构建出一条哈密顿链,每一个强连通分量是一段区间,通过判断一段长为 \(i\) 前缀的度数和是否是 \({i\choose 2}\) 可以判断其是否是 SCC 的右边界。

对于二分竞赛图,是否有同样优美的性质呢?首先考虑缩点之后,很明显缩出来的很有可能不是一条链,但是这种情况之发生在有两个同侧且出邻域一模一样的点时才有可能出现。如果我们不断删去出邻域相同的其中的一个点,将删去的那个视为没删去的点的附属点,它们在图中一定保持相同的可达性地位,然后 SCC 缩点,可以保证缩出来一条链。

证明 copy from sol:

考虑删点求拓扑序的过程中,如果存在某一时刻有两个强连通分量入度为 \(0\),首先这两个强连通分量一定是同侧单点。如果不是单点,就存在至少两种颜色,那么这两个强连通分量之间就有连边了,此时这两个点都连向了没被删的异侧,所以这两个点完全相等,于是矛盾。

接下来我们考虑同样地通过度数序列刻画可达性性质。对于度数相同的所有点,它们之间有可能是之前去重时删去的点,恰有一个留在原图中的点单独构成 SCC,这样它们之间一定互相不可达;也有可能一部分留在了原图中,这样所有度数相同的点一定在同一个 SCC 中,一定互相可达。因此,我们给度数相同的点添加一条有向链一定不影响 SCC 的形态。

对于度数不同的点,由于删点后 SCC 成一条链的状态,所以度数大的点一定能到度数小的点。

考虑如何解决本题。对于两行,从左往右扫描列,找到它们第一个不同的位置,此时该位置为 \(1\) 的行(设为 \(x\))出度大于该位置为 \(0\) 的行(设为 \(y\)),那么有可达性 \(x\to y\)。由于往右扫描列的过程不会破坏原有可达性(这个观察非常关键!),所以你把所有行按照字典序排序之后,所有包含行点的 SCC 在之上构成若干个区间!主席树维护哈希直接排序就可以得到一个 \(O(n\log^2 n)\) 的做法,常数不大。

接下来的事情比较好处理了,考虑得到字典序顺序之后,每一次加入一个列点,观察其对可达性的影响。求出能直接到达其在序列中最靠前的行点,以及其能直接到达在序列中最靠后的行点,如果这个东西构成了区间,那么中间所有点一定在同一个 SCC 里,用并查集并起来;否则利用可并堆维护一下这个列点处于哪两个 SCC 之间,如果这两个 SCC 并起来了,这个列点也要加入这个新的 SCC。

注意算答案是把边看成点算包含边的 SCC 个数,所以设一个 SCC 中有 \(x\) 个行点 \(y\) 个列点,对答案的贡献是 \(\min(1-xy,0)\)

Code

Round 2

我自己的场,感觉对题目已经没什么可说的了,题目是按照出题时的题目编号排序的,预估的难度顺序是 T2、T1、T3。来聊聊后台看比赛的感受吧。

首先我的题数据似乎是被爆了的,zhy 验题的时候提出了随机抽样法,但是没有通过我当时的数据,让我误认为完全标搭不上关系的抽样法正确率比较低。但是似乎场上过了 T1 的人绝大多数写的是抽一个前缀,这样就不用高精之类的东西。考前的时候没有仔细考虑到每一种可能的乱搞比较失职,我自己也应该仔细对待 zhy 提出的乱搞的,但是我脑子里的想法已经被标算定型了,导致我认为思考路径接近标的做法正确率才比较高(即至少要发现“去除无效位”这个结论才能做这个题)。

我造数据的方式大概是提出一些错解,比如没有去除无效位的、没有处理有倍数关系的等等,然后与标算对拍。这样的一个我忽略了的坏处是同余方程分布太均匀了!导致抽样法很容易得到一个能够反映全局的抽样的块。

cxy 老师太牛了,拿下了 T2 一血,随后用一些奇怪的方法过了 T1。蒯他的代码下来对拍,发现将数据生成方式修改一下是可以很快拍出来的。后面几位通过的选手都可以被拍出来的数据 hack 掉。

fxt 老师似乎卡了很久 T1,之后把抽样次数改大直接通过了该题。测了一下发现他能通过刚刚造出来的所有 hack 数据,但很遗憾把他的代码拿下来对拍,依然是可以很快拍出来,跟其它六名通过此题的选手错在不同的地方啊……作为出题人对于到底发生了什么已经不懂一点了……

原本看到这里心态比较崩溃,以为大家会介意我造水了数据要扣我出题分,不过我的题似乎得票率甚至是最高的?可能是因为这个 idea 我觉得还是挺有新意的,而且这个题数据也确实挺难造的;也有可能是叉老师帮我吸引火力了,大家又不愿意去做 zhy 的题。

接下来说一下 T2,这个题是叉老师的题,被喷的点大概在于放过了 \(O(n\sqrt n \log n)\) 但卡掉了 \(O(n\sqrt n)\)

第一次听说这题竟然放过了挂 \(\log\) 的做法感觉还是很惊奇的,确认了半天到底是不是那几个根号半 \(\log\) 的提交。后来发现确实有一个树剖分块的做法,跑得甚至比许多根号做法还快。作为赛前看过这个题的人属实震惊了。

这个 6s 其实是放宽了时间限制之后的结果,zhy 和 xde 写的两种做法不同的标都可以通过。对于某些根号被卡常赛前可能有心理预备,分块算法的常数确实极度依赖空间访问连续性等等不可预见的问题。解决方法只是说下次别出卡 \(\log\) 类型的分块题了,除非学 Ynoi 强行把数据范围、时间限制开到很大,否则造成不良好体验的风险太大了。

还有 T3,这个题做法还是十分有水平的,但是开场两个小时没有一个提交。终于有了第一个提交的时候,我们开心地跑过去一看,发现是有人交 T1 代码交到 T3 上了,更乐了。

本场 T1、T3 zak 都验了题,但值得声明的是并不是群里有人说的按 zak 1ppm 给分的。因为如果按不写代码的纯思考时间给分的话,两个题总分都不超过 30pts。

我大概是写了这个题的由 zak 提出消元做法,只需要求一系列多项式组成的矩阵的连乘然后求逆,能拿 85 分。zhy 给的标则是利用特征多项式迭代的一个做法,不会特征多项式没有写,但是消元做法确实简单又优美,zak 的功力深不可测!

zhy 觉得消元做法没有达到他毒瘤的本意,于是把模数改成了非质数,zak 在得知之后又很快提出了另一个支持非质数模数的做法。

赛时 cxy 找到了一个可以不需要求逆元的跟 zak 做法类似的想法,仅仅需要求多项式矩阵连乘不需要求逆,算是把 zhy 最后一档分的设计爆了。

两个人的题都被一个人爆了,于是那天晚上我和 zhy 都挺沮丧的,我无论如何都不想写一道题,于是在和 zhy 聊了许多关于训练、做题、灵感等等的话题之后就回家了。

最后 zhy 这个题除了 cxy 老师,做的人实在是太少了,让人感叹多项式已经成为了昨日幻影了。大家似乎都不太喜欢给 poly 题投票。

总而言之,虽然大家有可能不喜欢这一场中的某些题,也许甚至三个题都不喜欢。但是我见证着这场是如何诞生的,认为至少三位出题人都是较为认真地对待了这些题从 idea 诞生到造数据的全过程。我认为 T1 真的是我觉得我出过的最好的、最有新意的 idea 了,虽然最终我造的数据可能完全浪费了这个 idea,让大家认为这个题是猜结论或者是一道乱搞题,感到了极其遗憾。在这里自己向自己疯狂谢罪了。

不过既然已经在自己出的题目中认认真真下了功夫,也没有必要再多去懊恼了,尽力去做就是最好。

接下来是做法的一些讨论。

《生命的循环》 from 杨鑫和

本场互测最“神经”的题目,评判标准是题面中包含神经作为子串的次数最多。

Q:《生命的循环》的含义是什么?

A:没有任何含义,单纯只是想跟《矩阵的周期》起个差不多的标题,题目背景也是脑内乱 roll 出来的。

咕了,反正是自己的题,我的想法大多写在题解里了(

如果需要人话版的可以在直接摇我写一个。

Code

《树上简单求和》 from 肖岱恩

zhy 给出了一个很牛的一句话做法,考虑一条路径在括号序意义下是一段区间,转化成经典序列分块问题。做完了……分块的空间可以做到线性。

代码没写。

《路径计数》 from 周桓毅

讲讲我唯一会的 zak 提出的消元做法,能拿 85 分。

注意到本题看起来最奇怪的、唯一一个跟下标 \(i\) 有关而不是 \(j\) 有关的系数 \(B_i\),考虑先消掉这个系数。对于所有的平着走一步(\((i,j) \to (i+1,j)\)),用乘法分配律拆开 \(\prod (B_i + C_j)\),考虑哪些位置取了 \(B_i\),把这些步直接扣掉不会改变任何其它步的系数,只有终点左移了。扣掉这些位置之后,我们就能把 \(x\) 当作时间维,需要解决的问题就变成了一个一维数轴左右游走,即你初始站在 \(0\),往右走一步系数 \(A_i\),往左走一步系数 \(D_i\),原地不动系数 \(C_i\),分别求出行动 \(0\sim n\) 次之后终点对应的 \(F_i\) 和路径系数的乘积之和。

这个问题可以列出答案的生成函数消元做,即与期望类型的随机游走类似,设 \(G_i\) 表示从位置 \(i\) 开始随机游走若干步的生成函数,把每一项表示成前两项的线性组合,手动消元之后最后只需要求逆就可以还原出 \(G_0\) 了。

接下来考虑拼回去 \(B\) 的系数,这个问题的形式是有一个低次多项式序列,你需要对于每一个前缀求前缀积与一个给定多项式点乘后的结果,这个可以分治做,每次把给定多项式与左边的前缀积减法卷积一下递归到右边,截断一下无用的位就行了。复杂度 \(O(n\log^2 n)\)

这个做法的巧思在于意识到处理期望类型题目的常见手段也可以套用到多项式题目上来,我们可以利用生成函数刻画随机游走过程。

Round 3

最自大闭的一集。

自从组 Round 2 的时候知道了题目不是按难度顺序排就给自己心态上了巨大 debuff。开 T1,看起来好神秘;开 T2,是数据结构!想 114514 年,发现不是签;开 T3,打表刻画了许久的模式,这一点都不好拿分啊!仔细思考之后终于在 T3 拿了 17 分。只剩下一个半小时了,回去把 T1、T2 暴力补了,最后疯狂 rush T2 sub2 的 30 分,67 分签到分遗憾离场。

下午在几个月不想熬夜打 CF 之后终于有了阳间场,叉老师飞快过了 D,而我 B 罚时两发,C 罚时四发,D 做法假了乱搞交了十三发还没过。

《环上排序信息最优分割》 from 仲煦北

场上以为很难,但是后来发现是环上邮局板子,决策单调性分治的时候用链表只删除地维护排序信息就行了。

明明看过 cmd 的博客,我为什么不多开开这个题呢?怎么四边形不等式都没想到?

Code

《研心》from 李静榕

看了半天题解被绕晕了。疑似巨大难写题,暂时咕了,以后有时间再写。

《无限地狱》from 陈旭磊

别急,应该会有的。

Round 4

比赛结束时:78+5+24,我已经全力以赴了……果然还是不够吗……

发榜时:啥?这也能前二十?

zyb 发消息时:啥?前二十的线低于签到线😯……

打互测就像在抽奖,考的我的印象是 T1 是最可做的题,T2 是非常强的 01 背包问题不好做,T3 是 ylx 出的大时限抽象题。赛后一看,发现大家用各种各样的乱搞水过了 T2,T1 一个人都没过,而 T3 相比之下显得十分具象,像道正常题。原先以为自己是不会做 T1 要菜输了,后来发现确实输了,输在不会 T2 乱搞。

现在互测最难的地方在于对形势建立清晰的认知,只要赛前知道榜,你的分数水平就会提高一大截!

upd: 这 T3 也不是一道正常题啊,场上过的人好像也不是标。

《Désive》from 陈诺

蛮不错的一个题,至少是这一场最像题的题了。但是考完破防之后点了个无,忘了点赞了。

场上我写了一个排列特殊性质下是对的乱搞做法,发现直接过了 o=1 的特殊性质拿了全场最高分,也就是说这个题虽然最后几个 sub 比较强但是没往前面的 sub 放 hack 数据。这个乱搞也就是扫描线扫描右端点,然后维护 trie 树上关于左端点下标的 DP 值。发现右端点右移的时候左端点变化期望不是很多,所以直接不扫描线了整体维护 DP 值连续段的归并再二维数点就行了。

讲点正经做法。

考虑 xormex 怎么求。一个很直接的想法是在 trie 树上 DP,也就是我上面乱搞用到的方法。但是 trie 树上 DP 不容易刻画其连续段变化的性质。

另一个想法是不难证明 trie 树上 DP 的本质是求一条链,使得这条链上所有点的兄弟节点代表的子树中,所有满子树的子树大小之和最大化。我们从这个角度入手本题。

依然是扫描线,当右端点移动的时候,考虑每棵子树变成满子树的最大左端点 \(mn_p\),只有 \(O(n)\) 个位置变化了。显然只有选择的满子树包含了至少一个 \(mn_p\) 变化了的子树的方案才会更新答案。

一个方案的更新形如对于当前所有左端点小于某个值的位置答案与一个值取 \(\max\)

如何处理出这些方案的更新呢?一个朴素的想法是暴力扫描 \(mn_p\) 变化了的子树的兄弟子树中的叶子。这些叶子更新的方案相当于其到根的链上的点的兄弟节点的 \(mn_p\) 拿出来排个序后缀和一下,这样在修改是排列的时候依然是 \(O(2^n\operatorname{poly}(n))\) 的,但是在修改不是排列时一个大子树被反复更新就会爆炸。

你发现处理兄弟子树的所有叶子,重复处理的部分太多了!考虑将兄弟子树当成一个整体 DP,DP 出其取每一种可能的最小左端点的 xormex 是多少,再作为一个整体去和到根的链上兄弟的 \(mn_p\) 排序后缀和。但是这样复杂度没什么根本性变化,需要更好的处理。

注意到只有包含了你当前更新了 \(mn_p\) 的子树的方案才需要考虑,还注意到如果你的方案能够直接包含没有更新 \(mn_p\) 之前的 \(p\) 子树也是不需要更新的,这说明对于一次更新将 \(mn_p\)\(v\) 更新到 \(v'\),你只关心 \((v,v')\) (开闭没关系,因为你的时间本来就是序列下标不会重)之间的那些分界点的更新。也就是说对于 \(p\) 的兄弟子树,你只需要求出左端点介于 \((v,v')\) 间的所有可能的 xormex 及其对应区间就行了,这一部分注意到同一个子树所有的 \((v,v')\) 区间不交,所以拿一个 trie 树双指针式地维护一下就行了;对于 \(p\) 的祖先的兄弟节点,可以暴力扫描出所有 \(mn_p\) 位于 \((v,v')\) 之间的位置处理。

这样每个修改了 \(mn_p\) 的子树贡献的修改数是 \(O(2^n n)\),看起来是三只 \(\log\),如何优化?注意到修改了 \(mn_p\) 的子树具有祖先关系,所以其实后半部分扫描到根的链的兄弟,总共只可能修改 \(n\) 个不同位置,修改又是取 \(\max\) 的形式,所以说给当前时间的所有修改位置去个重复杂度就少了一个 \(n\) 了。

后半部分当然可以线段树历史和。但是由于场上已经写了二维数点了,就直接蒯了过来用 map + 树状数组实现了同样的功能。似乎跑得比线段树快点?

Code

《分道扬镳》from 葛致远

彻底咕咕算了,不想改了。

《观虫我》from 叶李蹊

idea 感觉很牛!但是实现了一下标算,发现怎么卡常都卡不过去,怎么回事?

一个经典的想法是折半,也就是修改时贡献前一半的超集,查询时查询后一半的子集,复杂度 \(O(q(\sqrt 2)^n)\)

考虑这个做法的本质,实际上相当于每一位的贡献方式对应一个矩阵 \(\begin{pmatrix}1 & 1 \\ 0 & 1\end{pmatrix}\),我们可以把它拆成两步分别做:\(\begin{pmatrix}1 & 0 \\ 0 & 1\end{pmatrix}\begin{pmatrix}1 & 1 \\ 0 & 1\end{pmatrix}\) 或者 \(\begin{pmatrix}1 & 1 \\ 0 & 1\end{pmatrix}\begin{pmatrix}1 & 0 \\ 0 & 1\end{pmatrix}\),前者在修改 1 多的时候表现差,后者在询问 0 多的时候表现差。如果询问 0 多、修改 1 多,复杂度就被卡满了。

注意到还有一种拆分方式能在询问 0 多、修改 1 多的时候表现好: \(\begin{pmatrix}1 & 0 \\ 1 & 1\end{pmatrix}\begin{pmatrix}1 & 1 \\ 1 & 0\end{pmatrix}\)。我们如果每一位随机取它是用哪种方式贡献的,那么一位期望会使当前操作的枚举空间扩大 \(\frac{4}{3}\) 倍。复杂度期望 \(O(q(\frac{4}{3})^n)\)

但是由于这个题的对于复杂度影响是乘上什么倍率而不是加上什么东西,这也就意味这虽然期望是 \((\frac{4}{3})^n\),但是其方差往往难以接受,很容易复杂度爆炸。解决方法是随机若干个分配拆分矩阵的方式预先取最优的。

最后,使用 ull 64 位压位,相等于将后 6 位独立出来。这一部分可以直接通过预处理一些系数加使用 __builtin_parityll 做到 \(O(1)\),复杂度 \(O(q(\frac{4}{3})^{n-\log w})\)。可以接受。

没卡过常,代码不放了。

Round 5

100+40+20

想了一整场没有切 T2,小小的挫败。

还是进前 20 了,果然是集训队比烂大赛。

T1 反复确认题意之后很快想出来了,经常见的想法。每次取平均值最大的连通块跟父亲并一下就可以了。

T2 写完平方做法之后,想要用点分治直接优化这个做法。我就一直在考虑刻画这个连通块的形态:容易注意到,在这个连通块的边界,所有点都要么 \(<l\),要么 \(>r\),于是考虑点分治之后考虑边界点扫描线,可以得到一个做法?假了。因为连通块的边界点不一定在当前分治块内,无法挽救正确性。

最后认为 T2 不太好做了,之后一直在写部分分,最后在想 T2 链的部分分怎么写,意识到可以把 size 的贡献拆到每个点上去,强硬分治计数左端点、右端点、size 拆出的点这个三元组,大力分讨这些点在左右两边的哪边然后巨大麻烦数点。发现只剩一点点时间完全写不了了。最后想了想,认为这扩展不了正解。(其实可以,就是叉老师的做法)。

叉老师切掉了 T2。然后我才知道点分也是可以做两只 \(\log\) 的。我对点分治的理解太狭隘了!原来点分治不一定要把答案在第一次劈开连通块的地方统计!

原来以为是 DS 题练的不够多,后来才发现自己输在思维,不要被对特定算法的理解固定死了!

《长野原龙势流星群》from 孙培轩

考虑单个子树怎么求:二分答案,每个点的权值减去二分的答案之后直接贪心,如果这个子树当前的和 \(\ge 0\) 就保留,否则扔了。

考虑如何扩展到全局:直接在时间轴上扫描你二分的答案,你发现只有当一个子树是否保留的状态发生改变之后你才需要考虑这个时间。所以说拿一个堆维护状态发生改变的最近时间,然后找到这个子树把它跟父亲用并查集合并一下就行了。

复杂度 \(O(n\log n)\)

Code

《Classical Counting Problem》from 翟鹏昊

出题人的做法三个 \(\log\) 怎么好意思放出来,至少也应该把时限放大一点吧!害得我们 zhy 的 \(O(n\sqrt n)\) 做法被卡常成 55 分了 QwQ!

首先讲讲这个题的平方复杂度的刻画,每一个合法的连通块都可以通过如下方式生成,对于一个区间 \([l,r]\),满足只保留编号在 \([l,r]\) 之间的点之后 \(l,r\) 两点联通,那么 \(l,r\) 所在的联通块就是我们想要的连通块。

出题人做法:

考虑对最小值分治建 kruskal 重构树 \(T_1\),最大值分治建 kruskal 重构树 \(T_2\),那么合法连通块的刻画是对于区间 \(l,r\),满足 \(l \in T_2(r)\)\(r \in T_1(l)\)\(T_1(l)\cap T_2(r)\) 就是合法连通块(其中 \(T(x)\) 表示该树上以 \(x\) 为根的子树)。

考虑对第一颗树静态链分治(即树上启发式合并),以标记出 \(T_2\) 中在 \(T_1(l)\) 子树中的节点。然后 \(l\in T_2(r)\) 的条件相当于限制 \(r\)\(l\) 的祖先,\(r \in T_1(l)\) 的条件相当于限制 \(r\) 是被标记的点。对第二颗树树剖线段树(也可以全局平衡二叉树降一个 \(\log\)),设计一下线段树标记就可以求出这条树链上所有被标记的点的子树中被标记的点的个数和了。

总结一下就是 DSU 降一维,树剖降一维,线段树统计,还是三只 \(\log\) 的,这样搞完全没有任何意思啊?

来讲讲叉教给我的有意思做法:

注意到我场上的做法 naive 在我过度关注了本题中连通块的具体形态,而事实上,在将 size 拆贡献之后,本题可以转化成一个三元组统计问题,求有多少个合法的三元组 \((l,r,x)\),满足区间 \([l,r]\),张成的联通块包含了点 \(x\)

这样看起来什么都没有改变?考虑点分治,但是一个三元组不是在分治中心第一次劈开 \([l,r]\) 张成的连通块被统计,而是在分治中心第一次将 \(l,r,x\) 劈到不同的分治块,也就是说分治中心位于 \(l,r,x\) 构成的虚树上时统计。

这样统计的好处是什么?这样的话,\([l,r]\) 区间张成的连通块包含点 \(x\),可以等价转化成张成的连通块包含 \(l \to rt\) 的路径、\(r \to rt\) 的路径、\(x \to rt\) 的路径(\(rt\) 是分治中心),变成了一个纯粹的数点问题。

\(l\to rt\)\(r \to rt\) 的路径判掉自己跟自己冲突的情况之后,只提供了一维偏序,而 \(x\to rt\) 的路径要被 \([l,r]\) 这个区间完全包含,实际上可以把 \(x\to rt\) 的路径看成一种对权值的修改,二维数点的时候如果 \(l,r\) 的区间包含了这个区间权值就 +1。

这个做法只需要在点分治的每一层跑二维数点就行了,注意要容斥掉三个点在同一个子树内的情况,复杂度 \(O(n\log^2 n)\)

三只 \(\log\) 运行效率吊打根号和两 \(\log\) 了。

Code

《运筹帷幄》from 赵海鲲

咕会。

Round 6

稍微有点状态的一场。开场开 T1,想了一会之后会了一个判定,写了写 DP 发现假了,仔细研究了一下就修好了,直接 DP 可以四方,容斥+前缀和优化一下转移就可以做到三方。

接下来看 T2,这场 T2 其实是可做题,但是场上我没有很多思路,于是写了一个压缩运算表相同行列的做法,发现直接草过了 60 分,非常开心啊!

看 T3,发现二分一下可以枚举中间点贪心,原来以为得到了一个双 \(\log\) 做法,但是发现需要决策中间点的方向假了,中间的部分仍然需要平方合并。没办法写平方对数暴力跑路了。

终于进前十了……

《树数叔术》from 苟竞予

我有 \(O(n^2V)\) 爆标做法!

首先注意到 \(V\) 很大是假的,取题面中的集合为全集,发现这要求所有数的 \(\operatorname{mex}\) 等于 \(V+1\),也就是说 \(0\sim V\) 的所有数都必须至少出现一次。

然后考虑这个题的判定条件。这个题好做就好在你将必要条件一个一个分析过去发现它就充分了。首先我们可以得到一个必要条件: \(0,1\) 都只能出现一次,中间链上的节点都强制被选了。我们继续往后加入颜色,每加入一种颜色都只有两种情况,第一种是其所有出现都在之前的颜色的虚树中,第二种是其恰好在这棵虚树外出现一次。

所以我们依次枚举所有颜色,DP 它是属于第一种情况还是第二种情况,注意到如果属于第一种情况,还要强制它至少出现一次。这一部分可以考虑容斥,强制其不出现相当于就是乘上一个 -1 的系数,在 DP 中顺带统计一下就行了。

题目要求有标号树,可是我们刚才那个过程没有分配标号怎么办?注意到刚才给树染色的过程天然就为数钦定了一个顺序:每次加入一个虚树外的点时,依次给新加入的链编号就行了。那么对于这种染色生成的编号,与这棵树原来的编号恰有 \(n!\) 种对应方式,所以我们 DP 的时候完全不用考虑分配标号的事情,然后最后乘上一个阶乘就得到答案了。

前缀和优化,得到复杂度 \(O(n^2V)\)

Code

《欧伊昔》from 魏忠浩

加深对于 FWT 理解的一道题目,zhy 教我了一个不需要数据随机性质的做法。我们要求一个定义在任意运算表 \(op\) 下的三进制高维卷积 \(f\circ g = h\)(笔者不懂数学,符号都是乱写的,希望大家看得懂)。

首先根据 cmd 博客里指出的,FWT 的要点在于对于高维的卷积问题,每一位其实都是在线性变换意义下独立的,于是我们就是要设计三个三阶线性变换矩阵 \(A,B,C\),使得 \(C(A(f)B(g))=h\)

现在,我们能否有一种方法使得任意的运算表都能构造出这种线性变换呢?

我们考虑形式化 FWT 的构造要求。实际上,对于一个 \(3\times 3 \times 3\) 的三阶张量(这里可以理解成立体的矩阵) \(G_{i,j,k} = [op_{i,j}=k]\),我们就是要对其进行三阶张量的 CP 分解,也就是将其分解乘若干个秩一张量。秩一张量指得是可以将这个张量分解成三个向量的外积,用形式化一点的语言来说,对 \(G\) 的 CP 分解,就是求出三个 \(r\times 3\) 的矩阵 \(A,B,C\),使得 \(G_{i,j,k}=\sum_{t=1}^r A_{t,i} B_{t,j} C_{t,k}\)

我们 FWT 的过程就可以写成 \(h_k=\sum_i \sum_j G_{i,j,k} f_i g_j=\sum_{t=1}^r C_{t,k}(\sum_i f_i A_{t,i}) (\sum_j g_j B_{t,j})\)

对于一些特殊的张量,其 CP 分解是熟知的。比如说一张拉丁方运算表生成的矩阵,由于所有的三阶拉丁方都可以通过模 3 意义下的加法生成,给行和列分别赋一个 3 阶排列 \(ap,bp\),那么 \(op_{i,j}=(ap_i+bp_j)\bmod 3\) 就是一个拉丁方。根据三进制 FWT 我们可以用三阶单位根来分解这个张量。

这启发我们当我们知道一个 \(G_{i,j,k}\) 的分解方式之后,可以通过一定的手段把它变换乘其它矩阵的分解方式。

我们枚举三个 \(\{0,1,2\}\to \{0,1,2\}\) 映射 \(u,v,w\)(一开始我是只枚举置换做的,这样虽然方便构造,但是不能保证能够造出 \(r=5\) 的 CP 分解;精细实现枚举置换的做法应该还是能过数据随机的版本的),求出运算表 \(w_{(u_i+v_j)\mod 3}\),这张运算表的 CP 分解可以很容易从普通的模 3 意义下加法的分解中变形得到,考虑当前运算表与这张运算表不同的位置的个数,你可以惊奇地发现不同的位置个数的最小值有极大概率不超过 2。

进一步地,你构造更多已知 CP 分解方式的张量,比如 \(w_{\max(u_i,v_j)}\)。zhy 告诉我写个暴搜你就可以发现此时所有的运算表都满足与其中至少一个运算表只相差两个位置。也就是说,通过这种方式你对于所有的运算表都构造出了一个 \(r=5\) 的 CP 分解,这是一个确定性做法,复杂度严格 \(O(5^n)\)

注意到这种 CP 分解包含了虚数,也就是三阶单位根,你需要选一个模数来计算取值。可是本题的答案有可能爆 int,选取 long long 范围内的模数乘法代价又更高,于是你可以选一个 int 范围内的模数跑两次来卡常。

另一个从这个题中学到的 FWT 技巧,也就是说 FWT 可以类似 Karatsuba 算法一样分治实现,这样相比于传统写法有几个好处:第一如果 FWT 后数组的长度比原数组长(换句话说就是本题中 CP 分解出来的 \(r>3\) 的时候),那么这种方式只需要开 \(O(3^n)\) 的空间而不是 \(O(5^n)\) 节省了空间常数;第二是可以通过递归到低层就改跑暴力这样常数更小。

其实本题还是非常好写的,但是为了卡常不知不觉写到了 8k,经过卡常的丑陋代码:

Code

《人间应又雪》from 黄佳旭

我是鸽子。

Round 7

打得最抽象的一场。开场开 T1,发现这不是我们的动态笛卡尔树吗?搜这个算法第一篇就是我自己的博客欸?这不是优势在我?

然后开始写,写着些着意识到这个题需要维护子树 size,然后意识到在原树上维护会破坏直链的势能,所以得又开一颗 LCT,再一看数据范围 \(10^6\) 两秒 LCT 真的有希望吗?

花在了这题 3h 之后意识到这个题的正解可能完全不是 LCT。于是再花了一些时间拼完暴力之后只好去看 T2 了。

T2 看起来 Trie 树合并维护 SG 值直接做完了,但是空间带了 \(\log\),出题人卡空间!!!于是你需要牺牲时间换空间,改成 dsu on tree 维护空间复杂度成线性了,时间只多了一个非常小的 \(\log\),可以跑过去。

但是!!!编译器不知道把什么东西压到系统栈里导致空间炸了,所以你必须要手写栈+疯狂共用数组卡空间才能通过这个题!!最后没有调出来,45+50 遗憾离场。

最后集训队 rk21,这也能 rk21???就差一名就可以拿满互测高分的分了!!!

《Cyberangel》from 陈哲章

原来不是动态笛卡尔树,而是大海的深度!

动态笛卡尔树单 \(\log\) 做法:动态笛卡尔树维护直链剖分,发现树形态的变化由 \(O(n)\)linkcut 组成,然后你需要维护左儿子大小乘右儿子大小乘值的和,可以类似 DDP 思想维护虚子树标记和,显然常数是过不去的。

考虑类似大海的深度,直接上分治统计过分治中心的区间的贡献,这样将 \(\max\) 的贡献转化为右边前缀 \(\max\) 和左边后缀 \(\max\)\(\max\)。依然考虑依次加入最大值,考虑当前的贡献和。以左边为例,假设当前的后缀最大值取到了贡献,那么它的贡献只与它的位置、它下一个后缀最大值的位置和右边第一个比它大的数的位置有关。右边第一个比它大的位置可以用并查集维护,后缀最大值用一个栈维护就行了。

复杂度可以做到 \(O(n\log n\alpha(n))\)

Code

《新生舞会》from 蒋承佑

直接 DP SG 值就行了,设 \(f_u\) 为子树 SG 值,\(g_u\)\(f_u\) 异或上所有儿子的 \(f\) 值,用 trie 树维护出子树内到子树的根这一条链的 \(g\) 异或和,在 trie 树上二分做完了。

剩下的是卡空间,把 trie 树合并改成启发式合并,然后再改成非递归式 dfs

这个题有任何意义在吗?

Code

怎么在互测 OJ 上可以过,在 QOJ 上你就又卡爆我空间了?

真不想卡空间了。

《PM 大师》from 屠佳铭

甚至还没来得及看题,之后有时间会改。

Round 8

越来越烂了。

开局看 T1,发现这不是我们的弦图问题吗?本题子树图的特例,子树图的 PEO 是按照最浅点的深度排序。然后这个问题就转化成了链加颜色,求单点 \(\operatorname{mex}\) 的问题。这个问题一开始我把它转换成了单点加颜色子树 \(\operatorname{mex}\),发现这就是一个动态二维数点问题:给每一个颜色用 set 维护虚树转变成纯粹的链加,外面套一个线段树二分就可以做了。但是 \(5\times 10^5\) 的数据范围两只 \(\log\) 的大常数算法动态二维数点,让我不相信这能过。纠结了很久要不要写这个看起来没前途的算法。

这个时候 lbp 已经开始交 T1 了,看起来是会了,听说了他的复杂度也是两 \(\log\) 的。于是我才意识到这个题标算是不是就是两只 \(\log\) 啊?然后我仔细想了想,由于 PEO 路径 LCA 深度不降,可以直接树剖 + 堆同样是两只 \(\log\) 维护这个东西,只不过两个都从跑得满的 \(\log\) 变成跑不满的 \(\log\)。赶紧开始狂写,写完之后发现都要跑 1.4s,要是写刚才那个算法绝对过不去了。

可是此时已经过去两个多小时了。于是看了看后面两个题,T2 发现题目名称中包含多项式,发现本题严格强于树上链分治求多项式乘积,看起来不好搞,先跳了;看 T3,这不是我们的 Cardboard Box 吗?这个问题有无数种搞法,原题题解区占大部分的按 \(b-a\) 排序考虑分界点的做法和五个堆的反悔贪心做法看起来都不太有前途。这个时候我想起这个题 zhy 魔怔的想法:对于这种背包问题,其答案在模 2 意义下有凸性,这个时候我们可以闵可夫斯基和维护,这就很类似于动态凸包问题了!看起来就是这个题想考的!

同时动态凸包问题的查询需要二分,在这个题上的表现就是 wqs 二分,我们对线段树每一个节点维护出其闵可夫斯基和之后将查询区间的线段树一起拿出来 wqs 二分,就可以做没有修改的情况了!

于是我打算先写一个 wqs 二分试试水。没想到这一试水直接淹死了。样例全过了交上去一直是 WA,验证了一下凸性,发现没问题啊?

最后发现是 wqs 二分输出答案那里应该是 \(K\times w-ans\) 而不是 \(cnt \times w - ans\),也就是你应该拿你询问的物品个数而不是你二分求出来的最优物品个数,好难受啊!!!

之后只剩下一个小时不到了,想要赶紧冲下正解,然后发现动态修改你可以直接维护 wqs 二分的加法,开始写!!!写这些着发现不对,这玩意没有可减性啊?需要上一个线段树分治!把纬度颠倒重新来!!!写着写着发现还是不对劲,这样是不是多了一个 \(\log\) 了?这下寄了。

只剩下十分钟了,干啥都来不及了,这个时候看 ckx,发现他已经切了 T2 了。我刚刚是不是扔掉了一个其实更加可做的题?于是在机房里怒吼红温😡。

发现群里讨论 T1 的人都没用弦图,唯一一个在问弦图的老哥没有做出来。感情是我会得更多做这个题想法更慢啊……

感觉这场明明全程都在干事,但是无效思考、无效调试时间太多了,感觉大部分时间浪费在了想做法 \(\to\) 假了,写代码 \(\to\) 写挂了 \(\to\) 调试的疯狂循环中。可能是考试精力不太足导致+考场策略不好导致的。

正好周六休息,想着下一场一定要打好,考前早点睡觉养足精神,不要乱开题(伏笔)。

《熟练》from 宋浩然

考虑弦图理论,按路径 LCA 深度从小到大排序之后可以求出来 PEO。然后如何构造方案呢?注意到由于路径 LCA 深度不降,所以说你可以把路径染色视为路径的两个端点到根的染色。就变成了链加颜色,求单点 \(\operatorname{mex}\)

然后考虑使用树剖 + 堆维护这个东西。所有链在树剖意义下都是一个前缀,由于查询点深度不降,我们在其深度深于这个前缀的端点的时候删去这个前缀的信息。问题被完全转化成了 \(O(n\log n)\) 次插入、删除求 \(\operatorname{mex}\),直接用堆就可以维护了。

Code

《皮鞋的多项式》from 鲜博宇

\(O(\sqrt {n \log n})\) 级别的 \(B\),考虑对每一个点维护一个多项式对 \((F_i,G_i)\),求子树乘积的时候,如果 \(G_i\) 的大小比 \(B\) 大了就把它乘到 \(F_i\) 上,这样 \(F_i\) 至多会发生 \(O(\frac{n}{B})\) 次变化或者与其它 \(F_i\) 合并。记录每一种不同的 \(F_i\) 就可以存储下来整颗树的子树乘积结构。

这个东西实际上可以看作某种类型的 top cluster 树分块,每一个过程里可能出现的 \((F_i,G_i)\) 都代表了一个边簇内的信息。每一次合并两个边簇,如果大小过大就新开一个簇。跟其它的树分块做法本质上十分相同,实现上更加清晰明了。

Code

《幽默还是梦》from 张力玺

wqs 二分做法考场上的想法已经假了,但是我感觉能修。

答案在模 2 意义下具有凸性,考虑 wqs 之后变成这样一个问题:限定奇偶形,求所有位置价值和的最大值,在此基础上最大化选择的体积大小。

可以对于每一个位置,求出其价值和最大的拿一个,以及最大的那一个减去次大的那一个,前者加起来,后者求最小值。(比较时价值相同按照体积比较)

如果最后最大的加起来奇偶性不对,就靠着后一个选项将奇偶性恰好调整 1。

考虑最大、次大二元组的情况只有 \(O(1)\) 种,我们将所有可能成为答案斜率的点离散化(也就是 \(2a_i,b_i,2(b_i-a_i)\)),建线段树,把所有位置和修改的每一种最大、次大二元组的情况挂在线段树对应的区间上去。

我们考虑模仿标算,可以在这棵线段树上整体 wqs 二分,递归到一侧的时候把已经确定的贡献直接贡献到询问上,注意到这个时候取 \(\min\) 时你不确定哪个体积更优(不确定当前二分的 \(w\)),但是 \(w\) 前面的系数值域 \(O(1)\),对于每一种值记录最小的截距。这样在一个线段树节点上你就可以直接查询每个询问在哪边了。

冇得代码。免责声明:上述算法纯属口胡,假了属于正常现象。这个算法想成这样已经不太可写了。写了常数也大概过不去(因为每个询问你要拆成四段放到线段树上,线段树长度还是三倍 \(n\)+修改数 级别的)。

标算的话大致看了前面的部分,感觉是按性价比选了之后调整 \(O(1)\) 个元素,看起来文明很多。

Round 9

吸取上一场的教训赛前睡足了八个半小时,感觉精力很不错。开 T1,做了做发现自己会了 \(k=0\),抓住两个对称的点算贡献就行了;继续想了想,发现抓住两个关于起点对称的点很有前途,应该可以直接拓展到 \(k=1\),于是仔细分讨了一下情况,写了些暴力,然后再对拍验证了一下,仔细修了修结论,到 2h 左右的时候发现自己 \(k=1\) 的刻画是完全正确的!剩下的只需要逐情况处理就行了。然后考虑 \(k=0\) 怎么写,发现这样子只需要一个区间加二次函数就行了。抓住对称的两个端点,考虑内部的奇偶性,然后对中点进行区间加,需要推一个等差数列求和。

然后我发现,这样需要对每一种情况的四个区间,都推一个等差数列求和,工作量爆炸!想要硬着头皮完成,尝试实现 \(k=0\) 的过程中,思路越理越乱。转变策略想打点暴力,发现这个题暴力也很难写,部分分在思维上确实有梯度,但是在实现复杂性上却几乎一样。对我这种写不出来代码的堪称疯狂折磨。

直到快 4h 了,我才发现我在这个题的沉没成本彻底收不回来了。于是赶紧跑去看 T2。发现直接对着这个东西 DP 一下就做完了,怎么签到在 T2 啊!!!狂写写了 \(O(3^q\operatorname{poly}(n))\),最后优化到 \(2^q\) 级别我已经会怎么搞了,但没来得及写完。

赛后群里有人说 T3 是 DIVCNT1,我知道这个东西的原题啊!如果赛场开了这个题大不了就去题解区现学。zhy 跳了 T1 然后就这样 200 分赢了。感觉原本重大利好我们的场次,能被我打得这么抽象也是没谁了,唯一一次得分发生在考试的最后十分钟。

结果来打的人好少,又是高分低于签到线的一场。

《游戏》from 章弥炫

好难写。

《木桶效应》from 周泽坤

先不细讲做法了。大概就是注意到给排列的每个位置一个下界,问有多少个排列,这个东西的方法是对下界排个序之后依次填数。对这个东西 DP 就行了。

但是我的复杂度似乎是 \(O(2^q n^3)\) 的,ckx 只多一个 \(q\),恰好是慢 \(q\) 倍左右,题解里的 \(O(2^q n^3 q^2)\) 多两个 \(q\) 真能过吗🤔?

Code

《月亮的背面是粉红色的》from 焦思源

DIVCNT1+类欧科技题。

最重要的是会 DIVCNT1。

Round 10

100+40+100 好起来了一点,rk5 刷新互测排名记录了。

开场选择先看三个题,发现 T1 怎么是数据随机题?T2 怎么是奇怪数数?看到 T3,感觉像个猜结论题,于是花了一些时间刻画了答案数组的奇偶性特征,写了写发现只判奇偶性特征大样例只 WA 了两组询问。然后仔细考虑数值特征,玩了几组之后大胆才结论直接按照这个栈的弹出序列贪心构造,无论如何都有一种合法方式放置骨牌,所以只要求一个点的高度大于等于其在栈中的深度。发现过了 45 分,发现这个 DP 可以轻松用 deque 优化,复杂度做到 \(O(n)\)

开 T1,有了一个大概率正确的想法,但是阈值如果取前 \(K\) 小点对的距离之类的很麻烦,于是再次充分利用数据随机的性质,考虑抽样!抽到的样种距离最小的点对就是阈值。

这样子抽样总会 T 几个点,因为抽出来的阈值很不稳定,但是多交几遍由于测试点取 \(\max\) 就直接过了。

T2 看起来不会一点,ckx 后面跟我讲答案的差分甚至可以 OEIS?

《计算几何》from 李青阳

这个题不带数据随机后来听 zhy 说是有做法的。但是还好我不会这个做法,否则我就过不了这个题了。

把这个题当作纯乱搞题做,考虑设一个阈值 \(lim\),点对距离 \(\le lim\) 的点全部暴力扣出来二维数点,如果一个询问区间不包含任何这样的点对,那么这个询问区间一定很小,跑平方暴力都行。

如何取这个 \(lim\) 呢?直接抽 \(O(n)\) 个点对,其距离最小值当作 \(lim\),这样就不用每个测试点自己调阈值了,这个设法期望正确的概率很高,由于测试点分数取 \(\max\),把代码多交几遍就过了。

差不多的 CodeCode 交两遍,拼起来就有满分了。

《分治》from 徐恺

好题嘞,但是你可以通过 OEIS 法来获得许多分数。

考虑如何暴力 DP,一个点的贡献可以这样确定,一个点的子树中分为三类叶子:贡献当前其子树 \(\operatorname{mex}\) 的点、已经用完了的点、自由点。DP 状态记录三类点各有多少个,即记录 \(\operatorname{mex}\) 和自由点的数量。转移时把 \(\operatorname{mex}\)\(\max\),然后可以拿出若干个自由点增大这个 \(\operatorname{mex}\)

考虑性质 A 答案为什么是这样的。注意到你 DP 优秀的转移点其实极其固定。由于性质 A 左右儿子同构,不妨设左儿子的 \(\operatorname{mex}\) 比右儿子多。这个时候贡献右儿子 \(\operatorname{mex}\) 的所有点都会变成已经用完了的点。然后你再从当前子树中选若干个自由点变成贡献当前子树 \(\operatorname{mex}\) 的点。

考虑对每个叶子算贡献,一个叶子一旦从自由点变成贡献点,接下来只有当它第一次成为右子树中上来的点才会停止贡献。所以说将从根走到一个叶子的方式写成 01 串,代表走左儿子(1)还是右儿子(0),这个点最大贡献次数就是最长的 \(1\) 连续段长度+1。

对于任意的情况你发现也是完全类似的,由于分治中点上取整,所以依然是左儿子稍重,我们依然可以只保留左儿子的贡献。所以答案依旧是根到每个叶子的走法 01 串最长 1 连续短+1 之和。

\(n\) 的二进制最高位为 \(l\),一个问题是此时有长度为 \(l\) 的 01 串,也有长度为 \(l+1\) 的 01 串。如何计数这种结构?

发现对于所有长度为 \(l+1\) 的 01 串其成对出现,是一个长度为 \(l\) 的 01 串后面跟个 \(0/1\),我们考虑所有后面跟个 \(0\) 的串,把这个 \(0\) 去掉又不影响答案,然后我们发现问题就变成了一个深度为 \(l\) 的性质 A 的情况,还有一些零散的最后一位一定是 1 的 01 串。

考虑哪些 01 串长度为 \(l+1\) 且最后一位是 1。这个条件等价于按照这个 01 串的指示依次对 \(n\) 进行除以二上取整/除以二下取整之后,其对最高位产生了进位。我们研究这个进位的条件,发现如果最接近最高位的连续一段一中,对应的操作有除以二上取整,这个时候最高位无论如何都会进位,这个情况判定为合法;然后递归分析下去,现在你不希望最高位所在的连续一段被进位,那么考虑下一段连续一段 0,如果其中发生了除以二下取整的操作对应位是 0 的情况,这个时候所有的进位到这里都戛然而止,进位不到更高位了,判定为非法;接下来对下面一段 1 进行同理分析……

问题的结构就是对于你检查没出问题的前缀,每一个除以二下取整操作都对应 1,每一个处以二上取整操作都对应 0,这种时候低位是否有进位直接影响高位。你枚举你检查没出问题的前缀,然后考虑你判定位合法的第一个 1 连续段,其必须包含一个 1。相当于对于当前串的最长 1 连续段,你知道已经有的 1 连续段的最长长度 \(mx\)、第一段的长度下界 \(now\),还剩下未决定 01 的串的长度 \(m\),在此基础上求最长 1 连续段的长度和。

考虑二项式反演出至少有一段 \(\le x\) 的方案数。进行一些平凡的处理之后(包括第一段的长度下界,这只是相当于对于 \(m\) 大小的改变,包括你需要重新计算答案在 \(\max(mx,now)\) 之下的贡献,包括 \(x\) 实际上要从 \(\max(mx,now)\) 开始枚举,但是这也可以转化成对于 \(m\) 大小的调整),你发现你唯一不好做的事情仅仅形如 \(O(l)\) 次查询:\(\sum_{x>0,y>0,(x+1)y\le m} {m-xy\choose y} 2^{m-(x+1)y}(-1)^{y-1}\)。这里记 \(h_{x,y,m}={m-xy\choose y} 2^{m-(x+1)y}(-1)^{y-1}\)

考虑根号分治这个东西,对于固定的 \(x\)\(f_m=\sum_{(x+1)y\le m} h_{x,y,m}\) 可以关于 \(m\) \(O(l)\) 递推出来;对于固定的 \(y\)\(g_m=\sum_{(x+1)y\le m} h_{x,y,m}\) 也可以关于 \(m\) \(O(l)\) 递推出来。(可以利用拆分组合数建立递推)

\(x,y\) 总有一个 \(\le \sqrt l\)。所以可以 \(O(l\sqrt l)\) 预处理 \(x,y\le \sqrt l\) 的时候的 \(f,g\) 数组。

这时候,怎么考虑 \(x,y\le \sqrt l\) 部分的贡献呢?我们上面提到,\(x\) 的枚举下界可以转化成对 \(m\) 的修改,即 \(h_{x,y,m}=h_{x-1,y,m-y}\),所以你枚举 \(y\le m\) 计算 \(\sum_x h_{x,y,m}\) 的时候,为 \(x\) 添加一个 \(>\sqrt l\) 的下界就行了。

Code

《骨牌覆盖》from 陈凯丰

不想写多了,进行一个速通。

结论:考虑维护一个奇偶性的栈,栈中元素符合奇偶奇偶奇……排列,即与其下标奇偶性相同。如果当前要加的数和栈顶奇偶性相同那么 pop,否则 push(注意如果栈为空认为栈顶是偶数,即栈必须要从奇数开始)。

称一个点的深度为其入栈后的栈大小/弹栈前的栈的大小,如果一个序列,其所有元素都不小于其在栈中的深度,且栈最终为空,则这个序列合法。这是充要的。

对这个东西计数,发现栈的形态唯一,所以只需要记录栈的大小,直接平方 DP,可以整体 DP 优化。拿两个 deque 分别维护所有的奇数位置/偶数位置,那么操作只剩下交换两个 deque/整体平移/头尾插入删除。复杂度 \(O(n)\)

Code

Round 11

咕咕咕就是为了在 11 月 11 日更新第 11 场。其实就是刚刚才改完题啦。

这场先开了 T1,发现树圆覆盖一个连通块就是覆盖直径,我就做了很蠢的一步操作,将一个直径劈成两半统计,然后疯狂写写写,最终写出了 7.9K 代码,最后上了拍调了出来感觉神清气爽,虽然写得很畅快,但是浪费了我互测一场超过一半的时间。接下来的时间看 T3,发现像是楼房重建(单侧递归)线段树板子,仔细想了想之后发现可以双 \(\log\) 打打标记,需要设计三个标记,其中一个专门用来贡献右儿子。题目本身标记的设计挺 tricky 不过还算有趣,但是出题人数据造挂了,没开 __int128 爆了 long long 出现了负数,用 long long 自然溢出就可以过。第一次见输出取模值在补码意义下结果的题,到最后还没修好,稍稍谴责一下。

最后的时间想冲个 T2 暴力,读完长长的题目感觉就像一个强行加了很多东西的题,显然这个信息时可以半群合并的,那个 \((i+d)\bmod n\) 就直接让人想到万欧类题目。好巧不巧,昨天刚刚做了 tham 让 VP 的 Ucup Stage Harbin 的一道万欧题,zhy 也往联考里搬了一道 CTT 的万欧题《随机数据》,跑到那题的题解区那边一看发现只有 xxy 的题解,然后就理解这个题是咋来的了。如果 T1 能做快一点是不是能 AK 啊,悲痛。

最后暴力也挺麻烦的,时间只剩下不到十分钟没有冲完。100+0+100 遗憾离场。

代码还是等 QOJ 传题,先放这了。

《连通块》from 金点

考虑树上的圆覆盖一个连通块的充要条件就是覆盖其直径,证明可以考虑距离一个点最远的点一定是直径端点。如何求出每一个联通块的直径呢?建虚树太麻烦,而且也不好拿出直径。考虑点集的直径信息具有可合并性,用线段树维护 dfs 序上的区间直径信息。每次求一个子树扣掉若干颗子树后的直径信息即可。

接下来如果你不像我一样犯蠢的话,你就会发现树圆覆盖其直径等价于其到直径中心的距离限制,直接上点分治模板就做完了。

但是我很唐,我是这样处理的:考虑按照直径将连通块劈成两半,其中一半到另一半中的直径端点距离最远。所以多了一条限制形如树圆的圆心在以 \(r\) 为根时 \(x\) 的子树中。

这个东西同样可以做到双 \(\log\) 直接点分治统计。发现唯一不同于一般点分治的贡献形式形如 \(r\) 在一个子连通块中,\(x\) 在一个子联通块中,这个时候只需要统计 \(x\) 子树中的所有圆心,通过树上 dfs+回溯二维数点就可以做了。

最后就是这里调了巨久直接导致遗憾离场了。

#include <algorithm>
#include <cstdio>
#include <set>
#include <vector>
#define lc (p << 1)
#define rc (p << 1 | 1)
using namespace std;
int read() {
    char c = getchar();
    int x = 0;
    while (c < 48 or c > 57)
        c = getchar();
    do
        x = (x * 10) + (c ^ 48), c = getchar();
    while (c >= 48 and c <= 57);
    return x;
}
const int N = 100003, M = 300003, Lg = 17, INF = 0x3f3f3f3f;
int hd[N], ver[N << 1], nxt[N << 1], tot = 1;
void add(int u, int v) {
    nxt[++tot] = hd[u], hd[u] = tot, ver[tot] = v;
}
int sub, n, m, q;
int de[N], ft[N], anc[N][Lg];
int dfn[N], sz[N], od[N], num;
int f[Lg][N], e[N], ps[N];
inline int cmp(int x, int y) {
    return dfn[x] < dfn[y] ? x : y;
}
struct cmpdfn {
    bool operator()(const int x, const int y) const {
        return dfn[x] < dfn[y];
    }
};
inline int lca(int u, int v) {
    if (u == v)
        return u;
    u = dfn[u], v = dfn[v];
    if (u > v)
        swap(u, v);
    int k = __lg(v - u++);
    return cmp(f[k][u], f[k][v - (1 << k) + 1]);
}
inline int dis(int u, int v) {
    return de[u] + de[v] - 2 * de[lca(u, v)];
}
void dfs(int u, int fa) {
    anc[u][0] = fa;
    for (int t = 1; t < Lg; ++t)
        anc[u][t] = anc[anc[u][t - 1]][t - 1];
    sz[u] = 1;
    f[0][dfn[u] = ++num] = fa;
    od[dfn[u]] = u;
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (v == fa)
            continue;
        de[v] = de[u] + 1;
        dfs(v, u);
        ps[i >> 1] = v;
        sz[u] += sz[v];
    }
}
struct Info {
    int u, v, d;
    Info() : u(0), v(0), d(-INF) {
    }
    Info(int U, int V) : u(U), v(V), d(dis(U, V)) {
    }
    Info(int U, int V, int D) : u(U), v(V), d(D) {
    }
    friend Info operator+(const Info x, const Info y) {
        if (x.d < 0)
            return y;
        if (y.d < 0)
            return x;
        Info z = x.d > y.d ? x : y;
        int td;
        td = dis(x.u, y.u);
        if (td > z.d)
            z = Info(x.u, y.u, td);
        td = dis(x.u, y.v);
        if (td > z.d)
            z = Info(x.u, y.v, td);
        td = dis(x.v, y.u);
        if (td > z.d)
            z = Info(x.v, y.u, td);
        td = dis(x.v, y.v);
        if (td > z.d)
            z = Info(x.v, y.v, td);
        return z;
    }
} sm[N << 2];
void build(int p = 1, int l = 1, int r = n) {
    if (l == r) {
        sm[p] = Info(od[l], od[r], 0);
        return;
    }
    int mid = (l + r) >> 1;
    build(lc, l, mid);
    build(rc, mid + 1, r);
    sm[p] = sm[lc] + sm[rc];
}
Info query(int sl, int sr, int p = 1, int l = 1, int r = n) {
    if (sl > sr)
        return Info();
    if (sl <= l and r <= sr)
        return sm[p];
    int mid = (l + r) >> 1;
    if (sr <= mid)
        return query(sl, sr, lc, l, mid);
    if (sl > mid)
        return query(sl, sr, rc, mid + 1, r);
    return query(sl, sr, lc, l, mid) + query(sl, sr, rc, mid + 1, r);
}
int jump(int u, int k) {
    for (int i = 0; i < Lg; ++i)
        if (k >> i & 1)
            u = anc[u][i];
    return u;
}
int jumpto(int u, int v, int k) {
    int t = lca(u, v);
    int len = de[u] + de[v] - 2 * de[t];
    if (k <= de[u] - de[t])
        return jump(u, k);
    else
        return jump(v, len - k);
}
int mnw, all, rt;
bool del[N];
void findrt(int u, int fa) {
    sz[u] = 1;
    int mxw = 0;
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (v == fa or del[v])
            continue;
        findrt(v, u);
        sz[u] += sz[v];
        if (sz[v] > mxw)
            mxw = sz[v];
    }
    if (all - sz[u] > mxw)
        mxw = all - sz[u];
    if (mnw > mxw)
        mnw = mxw, rt = u;
}
int qr[M], res[M];
vector<int> vec[N], lis[N], qv[N];
int tr[N], lim;
int pre[N], up;
bool ins[N];
int bel[N], col;
void upd(int x, int v) {
    ++x;
    while (x <= lim)
        tr[x] += v, x += (x & -x);
}
int qry(int x) {
    ++x;
    if (x > lim)
        x = lim;
    int res = 0;
    while (x)
        res += tr[x], x ^= (x & -x);
    return res;
}
void trav(int u, int fa, int d) {
    ins[u] = 1;
    vector<int> tmp;
    for (int x : vec[u]) {
        if (fa and bel[x] == bel[u])
            tmp.emplace_back(x);
        if (ins[x])
            ++tr[d + 1];
        else if (bel[x] != bel[u])
            lis[x].emplace_back(d);
    }
    vec[u].swap(tmp);
    sz[u] = 1;
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (v == fa or del[v])
            continue;
        trav(v, u, d + 1);
        sz[u] += sz[v];
    }
    ins[u] = 0;
}
void go(int u, int fa, int d) {
    for (int x : lis[u])
        upd(x, 1);
    for (int x : qv[u])
        if (d <= qr[x])
            res[x] += qry(qr[x] - d);
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (v == fa or del[v])
            continue;
        go(v, u, d + 1);
    }
    for (int x : lis[u])
        upd(x, -1);
    lis[u].clear();
}
void gotrav(int u, int fa, int d) {
    bel[u] = col, ins[u] = 1;
    if (d >= lim)
        lim = d + 1;
    if (d > up)
        up = d;
    for (int x : vec[u])
        if (ins[x])
            ++pre[d];
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (v == fa or del[v])
            continue;
        gotrav(v, u, d + 1);
    }
    ins[u] = 0;
}
void mygo(int u, int fa, int d) {
    for (int x : qv[u])
        if (d <= qr[x])
            res[x] -= pre[min(qr[x] - d, up)];
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (v == fa or del[v])
            continue;
        mygo(v, u, d + 1);
    }
}
void solve(int u) {
    all = sz[u], mnw = INF;
    findrt(u, 0);
    del[u = rt] = 1;
    lim = 1;
    bel[u] = u, ins[u] = 1;
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (del[v])
            continue;
        col = v, up = 0;
        gotrav(v, u, 1);
        for (int i = 1; i <= up; ++i)
            pre[i] += pre[i - 1];
        mygo(v, u, 1);
        for (int i = 0; i <= up; ++i)
            pre[i] = 0;
    }
    ins[u] = 0;
    trav(u, 0, 0);
    for (int i = 1; i <= lim; ++i)
        if (i + (i & -i) <= lim)
            tr[i + (i & -i)] += tr[i];
    go(u, 0, 0);
    for (int i = 1; i <= lim; ++i)
        tr[i] = 0;
    vec[u].clear();
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (del[v])
            continue;
        solve(v);
    }
}
int main() {
    sub = read(), n = read(), m = read(), q = read();
    for (int i = 1; i < n; ++i) {
        int u = read(), v = read();
        add(u, v), add(v, u);
    }
    dfs(1, 0);
    for (int t = 1; t < Lg; ++t)
        for (int i = 1; i + (1 << t) - 1 <= n; ++i)
            f[t][i] = cmp(f[t - 1][i], f[t - 1][i + (1 << t - 1)]);
    build();
    while (m--) {
        int k = read();
        for (int i = 1; i <= k; ++i)
            e[i] = read();
        sort(e + 1, e + k + 1, [](int x, int y) {
            return dfn[ps[x]] > dfn[ps[y]];
        });
        set<int, cmpdfn> st;
        for (int i = 1; i <= k + 1; ++i) {
            int x = i <= k ? ps[e[i]] : 1;
            auto it = st.lower_bound(x);
            int las = dfn[x];
            Info res;
            while (it != st.end())
                if (dfn[*it] < dfn[x] + sz[x]) {
                    res = res + query(las, dfn[*it] - 1);
                    las = dfn[*it] + sz[*it];
                    it = st.erase(it);
                } else
                    break;
            res = res + query(las, dfn[x] + sz[x] - 1);
            if (res.d) {
                vec[res.u].emplace_back(jumpto(res.u, res.v, (res.d + 1) >> 1));
                vec[res.v].emplace_back(jumpto(res.v, res.u, (res.d + 2) >> 1));
            } else
                vec[res.u].emplace_back(res.v);
            st.emplace(x);
        }
    }
    for (int i = 1; i <= q; ++i) {
        qv[read()].emplace_back(i);
        qr[i] = read();
    }
    solve(1);
    for (int i = 1; i <= q; ++i)
        printf("%d\n", res[i]);
    return 0;
}

《轮盘赌游戏》from 徐骁扬

感觉跟《随机数据》差别很小啊!核心想法依然是万欧区间查询。

首先先剥开这个题套的壳,题目中的期望问题转化成 \(\sum_i P(x\ge i)\) 这种形式之后,发现去掉一些无用的贡献之后其就是求环上所有区间的元素乘积和。(特别地全集要算 \(n\) 次,因为有多种起点)。这个东西可以用四个变量组成的信息进行半群合并(区间乘积,所有靠着左端点/右端点的区间乘积之和,所有区间乘积之和,注意不用维护环上跨过左端点右端点分界线的区间乘积值和,因为这可以通过前四个信息直接计算出来)。

先不考虑修改,全局询问怎么做。可以直接万欧,题目相当于是让我们按 \((id \bmod n) \bmod m\) 的顺序合并,由于 \(m\) 很小,考虑对 \(n\) 万欧,维护 \(m\) 所有起点的答案,万欧的信息中存储对于当前 \(\bmod m\) 的每一种值最后合并出来的信息是什么,然后 \(U\) 令坐标 \(-n\)\(R\) 合并当前位置之后令坐标 \(+d\)。这样就可以 \(O(m\log n)\) 做完全局询问了。

考虑万欧区间查询怎么做。注意到万欧对于半群信息的合并构成了一张 DAG,每个点只有两条出边,且信息合并只发生了 \(\log n\) 次,那么本题中的半群信息也至多合并了 \(\log n\) 层。你可以把这个当成线段树结构,容易发现在上面区间查询的复杂度也是 \(O(\log n)\) 的。

这里当然可以直接把所有没被修改过的区间查询出来,然后用树上启发式合并维护做到两只 \(\log\),一种经典的牺牲空间换时间的方法是改成直接在这个结构上线段树合并,复杂度做到了 \(O(q\log n)\)

#include <algorithm>
#include <cassert>
#include <cstdio>
using namespace std;
template <typename T = int> T read() {
    char c = getchar();
    T x = 0;
    while (c < 48 or c > 57)
        c = getchar();
    do
        x = (x * 10) + (c ^ 48), c = getchar();
    while (c >= 48 and c <= 57);
    return x;
}
const int P = 998244353;
const int N = 100003, T = 5000;
const int M = 20000003;
typedef long long ll;
typedef __int128 lll;
ll n, d;
int m, q, t, cnt, lim;
int qx[N], qy[N];
int a[N], b[N], rt[N << 1];
int qp(int a, int b = P - 2) {
    int res = 1;
    while (b) {
        if (b & 1)
            res = (ll)res * a % P;
        a = (ll)a * a % P;
        b >>= 1;
    }
    return res;
}
struct Info {
    int s, ls, rs, is;
    Info(int X = 0) : s(X), ls(X), rs(X), is(X) {
    }
    Info(int S, int L, int R, int I) : s(S), ls(L), rs(R), is(I) {
    }
    friend Info operator*(const Info a, const Info b) {
        return Info((ll)a.s * b.s % P, (a.ls + (ll)a.s * b.ls) % P,
                    (b.rs + (ll)b.s * a.rs) % P,
                    (a.is + b.is + (ll)a.rs * b.ls) % P);
    }
    int calc() {
        return ((ll)ls * rs % P * qp(P + 1 - s) + is + n) % P;
    }
} e[M];
ll len[M];
int ex[M], ey[M];
int mul(int x, int y) {
    if (x < 0)
        return y;
    if (y < 0)
        return x;
    int p = cnt++;
    ex[p] = x, ey[p] = y;
    len[p] = len[x] + len[y];
    e[p] = e[x] * e[y];
    return p;
}
inline int sub(int x) {
    return x >= m ? x - m : x;
}
struct Data {
    int f[T], d;
    friend Data operator*(const Data &a, const Data &b) {
        Data c;
        c.d = sub(a.d + b.d);
        for (int i = 0; i < m; ++i)
            c.f[i] = mul(a.f[i], b.f[sub(i + a.d)]);
        return c;
    }
    Data gp(ll x) {
        Data cur, res;
        cur.d = d, res.d = 0;
        for (int i = 0; i < m; ++i)
            cur.f[i] = f[i], res.f[i] = -1;
        if (!x)
            return res;
        while (x > 1) {
            if (x & 1)
                res = res * cur;
            cur = cur * cur;
            x >>= 1;
        }
        return res * cur;
    }
};
Data go(ll w, ll a, ll b, ll c, Data U, Data R) {
    if (!w) {
        Data init;
        init.d = 0;
        for (int i = 0; i < m; ++i)
            init.f[i] = -1;
        return init;
    }
    if (a >= c)
        R = U.gp(a / c) * R, a %= c;
    ll nw = ((lll)a * w + b) / c;
    if (!nw)
        return R.gp(w);
    ll o = w - ((lll)nw * c - b - 1) / a, g = c - b - 1;
    return R.gp(g / a) * U * go(nw - 1, c, g % a, a, R, U) * R.gp(o);
}
int upd(int p, ll x, int v) {
    int q = cnt++;
    len[q] = len[p], ex[q] = ex[p], ey[q] = ey[p];
    if (len[p] == 1)
        e[q] = Info(v);
    else {
        if (x <= len[ex[p]])
            ex[q] = upd(ex[p], x, v);
        else
            ey[q] = upd(ey[p], x - len[ex[p]], v);
        e[q] = e[ex[q]] * e[ey[q]];
    }
    return q;
}
int merge(int x, int y) {
    if (x < lim or y < lim)
        return max(x, y);
    int p = cnt++;
    ex[p] = merge(ex[x], ex[y]);
    ey[p] = merge(ey[x], ey[y]);
    e[p] = e[ex[p]] * e[ey[p]];
    return p;
}
void exgcd(ll a, ll b, ll &x, ll &y) {
    if (!b) {
        x = 1, y = 0;
        return;
    }
    exgcd(b, a % b, y, x);
    y -= (a / b) * x;
}
ll iv, tmp;
int main() {
    n = read<ll>();
    cnt = m = read<int>();
    d = read<ll>();
    q = read<int>();
    t = read<int>();
    exgcd(d, n, iv, tmp);
    iv %= n;
    if (iv < 0)
        iv += n;
    for (int i = 0; i < m; ++i)
        e[i] = read(), len[i] = 1;
    Data U, R;
    U.d = sub(m - n % m);
    R.d = d % m;
    for (int i = 0; i < m; ++i)
        U.f[i] = -1, R.f[i] = i;
    rt[0] = go(n, d, 0, n, U, R).f[R.d];
    lim = cnt;
    printf("%d\n", e[rt[0]].calc());
    for (int i = 1; i <= q; ++i) {
        ll x = (lll)read<ll>() * iv % n;
        if (!x)
            x = n;
        int v = read<int>();
        rt[i] = upd(rt[0], x, v);
        printf("%d\n", e[rt[i]].calc());
    }
    for (int i = q + 1; i <= q + t; ++i) {
        rt[i] = merge(rt[read()], rt[read()]);
        printf("%d\n", e[rt[i]].calc());
    }
    return 0;
}

《序列》from 徐哲晨

可以当作楼房重建的板子进阶!考验你对于单侧递归线段树的理解!

如何推导标记的设计感觉很难用语言表达出来,但是我隐隐感觉这之中是有一些方法的。

这里只讲结果,设计三种标记:

  • 普通区间加标记 ta,含义是区间每个数加上 ta

  • 区间加前缀 \(\max\) 标记 tg,含义是区间每个数按位加上当前区间的前缀 \(\max\) 数组。

  • 右儿子区间加前缀 \(\max\) 标记 tt,含义是区间每个数中位于右儿子的安按位加上当前区间的前缀 \(\max\) 数组。

然后用一个 proc 函数单侧递归打标记,打标记的时候显然不能下传标记,这样复杂度多带个 \(\log\),不过据 zhy 说他这样写了也直接过了。替代下传的方式是直接计算好当前 proc 函数对于当前区间的和的增量是多少。

复杂度 \(O(n\log^2 n)\)

#include <algorithm>
#include <cstdio>
#define lc (p << 1)
#define rc (p << 1 | 1)
using namespace std;
int read() {
    char c = getchar();
    int x = 0;
    while (c < 48 or c > 57)
        c = getchar();
    do
        x = x * 10 + (c ^ 48), c = getchar();
    while (c >= 48 and c <= 57);
    return x;
}
typedef __int128 ll;
const int N = 500003;
int n, m;
int a[N];
int mn[N << 2], mx[N << 2], tg[N << 2], tt[N << 2], len[N << 2];
ll sm[N << 2], sg[N << 2], ta[N << 2];
ll ask(int p, int v) {
    if (mx[p] <= v)
        return (ll)len[p] * v;
    if (mn[p] >= v)
        return sg[p];
    if (mx[lc] > v)
        return ask(lc, v) + sg[p] - sg[lc];
    else
        return (ll)len[lc] * v + ask(rc, v);
}
void pushup(int p) {
    mn[p] = min(mn[lc], mn[rc]);
    mx[p] = max(mx[lc], mx[rc]);
    sg[p] = sg[lc] + ask(rc, mx[lc]);
}
ll padd(int p, ll v) {
    ll dlt = (ll)len[p] * v;
    sm[p] += dlt, ta[p] += v;
    return dlt;
}
ll pall(int p, int v) {
    ll dlt = (ll)sg[p] * v;
    sm[p] += dlt, tg[p] += v;
    return dlt;
}
ll prig(int p, int v) {
    ll dlt = (ll)(sg[p] - sg[lc]) * v;
    sm[p] += dlt, tt[p] += v;
    return dlt;
}
ll proc(int p, int v, int c) {
    if (mx[p] <= v)
        return padd(p, (ll)v * c);
    if (mn[p] >= v)
        return pall(p, c);
    ll cur = 0;
    if (mx[lc] > v) {
        cur += prig(p, c);
        ll tmp = proc(lc, v, c);
        cur += tmp;
        sm[p] += tmp;
    } else {
        cur += padd(lc, (ll)v * c);
        cur += proc(rc, v, c);
        sm[p] += cur;
    }
    return cur;
}
void pushdown(int p) {
    if (ta[p])
        padd(lc, ta[p]), padd(rc, ta[p]), ta[p] = 0;
    if (tg[p])
        pall(lc, tg[p]), proc(rc, mx[lc], tg[p]), tg[p] = 0;
    if (tt[p])
        proc(rc, mx[lc], tt[p]), tt[p] = 0;
}
void build(int p = 1, int l = 1, int r = n) {
    tg[p] = 0, ta[p] = 0, tt[p] = 0, sm[p] = 0, len[p] = r - l + 1;
    if (l == r) {
        sg[p] = mn[p] = mx[p] = a[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(lc, l, mid);
    build(rc, mid + 1, r);
    pushup(p);
}
void update(int x, int v, int p = 1, int l = 1, int r = n) {
    if (l == r) {
        sg[p] = mn[p] = mx[p] = v;
        return;
    }
    pushdown(p);
    int mid = (l + r) >> 1;
    if (x <= mid)
        update(x, v, lc, l, mid);
    else
        update(x, v, rc, mid + 1, r);
    pushup(p);
}
int now;
void modify(int sl, int sr, int p = 1, int l = 1, int r = n) {
    if (sl > sr)
        return;
    if (sl <= l and r <= sr) {
        proc(p, now, 1);
        now = max(now, mx[p]);
        return;
    }
    pushdown(p);
    int mid = (l + r) >> 1;
    if (sl <= mid)
        modify(sl, sr, lc, l, mid);
    if (sr > mid)
        modify(sl, sr, rc, mid + 1, r);
    sm[p] = sm[lc] + sm[rc];
}
ll query(int sl, int sr, int p = 1, int l = 1, int r = n) {
    if (sl <= l and r <= sr)
        return sm[p];
    pushdown(p);
    int mid = (l + r) >> 1;
    ll res = 0;
    if (sl <= mid)
        res += query(sl, sr, lc, l, mid);
    if (sr > mid)
        res += query(sl, sr, rc, mid + 1, r);
    return res;
}
void write(ll x) {
    if (x > 9)
        write(x / 10);
    putchar((x % 10) ^ 48);
}
int main() {
    n = read(), m = read();
    for (int i = 1; i <= n; ++i)
        a[i] = read();
    build();
    while (m--) {
        int op = read(), x = read(), y = read();
        if (op == 1)
            update(x, y);
        if (op == 2)
            now = 0, modify(x, y);
        if (op == 3)
            write(query(x, y)), putchar('\n');
    }
    return 0;
}

Round 12

怎么互测没人打了?6+100+0 都能前 10,这样不就看不出我有多菜了吗?

做了许久仙人掌题,然后头脑不清醒疯狂玄学 RE,而且是单测不 RE,多测就 RE,于是我疯狂检查数组有没有清空之后无果,最后发现是自己有一个数组名打错了引发了一些奇妙连锁反应。还省略了一大堆由于奇怪原因引起的 bug。

为什么有的码题调起来很顺,有的码题就对着我干啊?看起来是我对仙人掌这个东西不太熟练了。

原来以为这是唯一签到,后来发现大家好多人是过了 T3 却没有人过两个题,2024 年了是不是熟练掌握仙人掌这东西的人越来越少了?

T2 出题人的 std 挂掉了,不过最后还是及时修了。我们的互测正在蒸蒸日上!

《(LIS, LDS) - Coordinates》from 刘恒熙

似乎是厉害题,orz 刘恒熙。不过先咕了。

《签到题》from 李枨夏

感觉不难仙人掌题。对于一个根和一个点考虑最优的树上这个点到根的距离。首先这两个点圆方树上的路径中所有的圆点一定都会被经过,这个可以通过树上差分简单打打标记解决。接下来只需要考虑算剩余点的贡献,考虑对于一个环来说,你肯定是断开与根所在的那一个子树相邻的一条边。那么你比较一下哪一种断开方式更优就行了,前缀和之后也可以简单通过树上差分计算贡献。

讲个逆天的事情,一开始没有写前缀和想要验证一下正确性发现直接过了出题人的数据,这是一个环长的平方之和的算法,也就是说出题人 std 挂了重造数据时没有造任何大环!

#include <algorithm>
#include <cassert>
#include <cstdio>
#include <functional>
#include <vector>
using namespace std;
typedef long long ll;
template <typename T = int> T read() {
    char c = getchar();
    T x = 0;
    while (c < 48 or c > 57)
        c = getchar();
    do
        x = (x * 10) + (c ^ 48), c = getchar();
    while (c >= 48 and c <= 57);
    return x;
}
typedef pair<int, int> pii;
const int V = 1000000;
const int N = 200003;
int n, m, q, cnt;
int a[N];
vector<int> va[V + 1], nd[N], all;
vector<pii> ve[V + 1];
int len[N];
ll ans, o[V + 1];
namespace bct {
const int M = N << 1;
int hd[M], ver[M << 1], nxt[M << 1], tot;
void add(int u, int v) {
    nxt[++tot] = hd[u], hd[u] = tot, ver[tot] = v;
    nxt[++tot] = hd[v], hd[v] = tot, ver[tot] = u;
}
int sz[M], cur;
ll res[M], dlt;
bool vis[M];
int arr[M];
void getsiz(int u, int fa) {
    cur += (u <= n);
    vis[u] = 1;
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (v == fa)
            continue;
        getsiz(v, u);
    }
}
void dfs(int u, int fa) {
    sz[u] = (u <= n);
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (v == fa)
            continue;
        dfs(v, u);
        sz[u] += sz[v];
    }
    if (u <= n) {
        dlt += sz[u], res[u] -= sz[u];
        for (int i = hd[u]; i; i = nxt[i]) {
            int v = ver[i];
            if (v == fa)
                continue;
            res[v] += cur - sz[v];
        }
    } else {
        int x = u - n;
        for (int i = 0; i < len[x]; ++i)
            if (nd[x][i] == fa)
                arr[i] = cur - sz[u];
            else
                arr[i] = sz[nd[x][i]];
        ll t0 = 0, t1 = 0, sum = arr[0];
        for (int i = 1; i < len[x]; ++i) {
            sum += arr[i];
            t0 += (ll)(i - 1) * arr[i];
            t1 += (ll)(len[x] - i - 1) * arr[i];
        }
        for (int i = 0; i < len[x]; ++i) {
            ll t = max(t0, t1);
            if (nd[x][i] == fa)
                dlt += t, res[u] -= t;
            else
                res[nd[x][i]] += t;
            if (i < len[x] - 1) {
                t0 += (ll)(len[x] - 1) * arr[i];
                t0 -= sum - arr[i + 1];
                t1 += sum - arr[i];
                t1 -= (ll)(len[x] - 1) * arr[i + 1];
            }
        }
    }
}
void trav(int u, int fa) {
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (v == fa)
            continue;
        res[v] += res[u];
        trav(v, u);
    }
    res[u] += dlt;
}
void solve() {
    for (int x : all)
        if (!vis[x])
            cur = 0, dlt = 0, getsiz(x, 0), dfs(x, 0), trav(x, 0);
    for (int x : all) {
        if (ans < res[x])
            ans = res[x];
        res[x] = 0;
    }
}
void clear() {
    for (int x : all)
        hd[x] = 0, res[x] = 0, vis[x] = 0;
    for (int i = 1; i <= cnt; ++i)
        hd[i + n] = 0, res[i + n] = 0, vis[i + n] = 0;
    while (tot)
        ver[tot] = nxt[tot] = 0, --tot;
}
} // namespace bct
namespace sol {
int hd[N], ver[N << 1], nxt[N << 1], tot;
void add(int u, int v) {
    nxt[++tot] = hd[u], hd[u] = tot, ver[tot] = v;
    nxt[++tot] = hd[v], hd[v] = tot, ver[tot] = u;
}
int de[N], las[N];
bool vis[N], cov[N];
void dfs(int u, int fa) {
    vis[u] = 1;
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (v == fa)
            continue;
        if (vis[v]) {
            if (de[v] < de[u]) {
                nd[++cnt].resize(de[u] - de[v] + 1);
                for (int x = u;; x = las[x]) {
                    bct::add(n + cnt, x);
                    nd[cnt][len[cnt]++] = x;
                    if (x == v)
                        break;
                    cov[x] = 1;
                }
            }
        } else {
            las[v] = u;
            de[v] = de[u] + 1;
            dfs(v, u);
        }
    }
}
void solve() {
    for (int x : all)
        if (!vis[x])
            las[x] = de[x] = 0, cov[x] = 1, dfs(x, 0);
    for (int x : all)
        if (cov[x])
            cov[x] = 0;
        else
            bct::add(las[x], x);
    bct::solve();
}
void clear() {
    bct::clear();
    for (int x : all)
        hd[x] = 0, vis[x] = 0, las[x] = 0;
    while (tot)
        ver[tot] = nxt[tot] = 0, --tot;
    all.clear();
    for (int i = 1; i <= cnt; ++i)
        len[i] = 0, nd[i].clear();
    cnt = 0;
}
} // namespace sol
int main() {
    read(), n = read(), m = read(), q = read();
    for (int i = 1; i <= n; ++i)
        va[a[i] = read()].emplace_back(i);
    for (int i = 1; i <= m; ++i) {
        int u = read(), v = read();
        ve[__gcd(a[u], a[v])].emplace_back(u, v);
    }
    for (int x = V; x; --x) {
        for (int i = x; i <= V; i += x) {
            for (int t : va[i])
                all.emplace_back(t);
            for (auto [u, v] : ve[i])
                sol::add(u, v);
        }
        sol::solve();
        sol::clear();
        o[x] = ans;
    }
    while (q--) {
        ll k = read<ll>();
        int p = upper_bound(o + 1, o + V + 1, k, greater<ll>()) - o - 1;
        if (p)
            printf("%d\n", p);
        else
            puts("-1");
    }
    return 0;
}

《SaM》from 朱汶宣

赛时想的判定都是从前往后的赋 \(2^i\) 的位权的,全假了,然后喜提 0 分!

你想到的判定越准确,你越会认为这个题简单。如果想不到判定你就寄了。

你只会做将一个数变成 0 或者将一个前缀转化成其下一个数这样的操作。

考虑枚举一个前缀,钦定你要填满这个前缀,这个前缀往后的所有数每种只保留一个,剩下的都转化成 0。

接下来你记录你转化出了几个多余的 0,记为 \(t\),然后倒着考虑,对于前缀 \(i\) 你当前需要 \(x\)\([0,i]\) 这样的前缀。如果当前位置个数 \(c\) 足够,那么把多余的转化成 0;如果不够,那么你就需要多转化出一些出来,将 \(x\) 变成 \(2x-c\) 即可。最后比较一下 0 的个数与要求的个数就行了。

那么你直接 DP 这个东西,算 \(=k\) 的方案数不好算,但是计算 \(\ge k\) 的方案数是简单的,你找到最长的前缀,使得多填满这个前缀之后恰好有 \(k\) 个数。你对前缀按照刚刚那个判定方式倒过来 DP 一下,对于后缀直接 DP 出其不同位置的个数,然后在你枚举的 \(m\) 位置合并一下就行了。转移需要用前缀和优化做到单次 \(O(1)\)

由于 \(di\le n\),所以总状态数不超过 \(O(n^3\ln n)\),时间复杂度也就是 \(O(n^3\ln n)\) 的了。

#include <algorithm>
#include <cassert>
#include <cstdio>
using namespace std;
int read() {
    char c = getchar();
    int x = 0;
    while (c < 48 or c > 57)
        c = getchar();
    do
        x = (x * 10) + (c ^ 48), c = getchar();
    while (c >= 48 and c <= 57);
    return x;
}
const int N = 203, P = 1000000007;
int n, m;
int f[N][N][N], g[N][N][N], h[N][N][N], lim[N], res[N];
int s1[N][N][N], s2[N][N][N];
inline void inc(int &x, int v) {
    if ((x += v) >= P)
        x -= P;
}
inline void dec(int &x, int v) {
    if ((x -= v) < 0)
        x += P;
}
int main() {
    n = read(), m = read();
    for (int i = 1; i <= m; ++i)
        ++lim[read()];
    if (!lim[0])
        ++lim[0];
    f[n + 1][0][0] = 1;
    for (int i = n; ~i; --i) {
        for (int s = 0; s <= n; ++s)
            for (int x = 0; x <= n - i and x <= s; ++x)
                inc(f[i][s + max(lim[i], 1)][x + 1], f[i + 1][s][x]);
        for (int x = 0; x <= n - i + 1; ++x)
            for (int s = x + 1; s <= n; ++s)
                inc(f[i][s][x], f[i][s - 1][x]);
        if (!lim[i]) {
            for (int s = 0; s <= n; ++s)
                for (int x = 0; x <= n - i and x <= s; ++x)
                    inc(f[i][s][x], f[i + 1][s][x]);
        }
    }
    for (int x = 1; x <= n; ++x)
        for (int y = 1; y < x; ++y)
            inc(res[y], f[0][n][x]);
    for (int s = lim[0]; s <= n; ++s)
        for (int t = 0; s + t <= n; ++t)
            for (int x = 1; x <= s + t; ++x)
                g[s][t][x] = 1;
    for (int i = 1; i <= n; ++i) {
        if (!lim[i]) {
            for (int s = 0; s <= n; ++s)
                for (int x = 0; x <= n - i and x <= s; ++x)
                    if (f[i + 1][s][x] and g[n - s][s - x][1])
                        inc(res[i + x],
                            1ll * f[i + 1][s][x] * g[n - s][s - x][1] % P);
        }
        for (int s = n; ~s; --s)
            for (int t = 0; s + t <= n; ++t)
                for (int x = 1; x * i <= s + t; ++x) {
                    s1[s][t][x] = g[s][t][x];
                    if (t)
                        inc(s1[s][t][x], s1[s + 1][t - 1][x]);
                }
        for (int s = 0; s <= n; ++s)
            for (int t = 0; s + t <= n; ++t)
                for (int x = 1; x * i <= s + t; ++x) {
                    s2[s][t][x] = g[s][t][x];
                    if (s)
                        inc(s2[s][t][x], s2[s - 1][t][x - 1]);
                }
        for (int s = lim[i]; s <= n; ++s)
            for (int t = 0; s + t <= n; ++t)
                for (int x = 1; x * (i + 1) <= s + t; ++x) {
                    int p = max(lim[i] - 1, x);
                    if (p <= s) {
                        dec(h[s][t][x], s1[s - p][t + p - x][x]);
                        inc(h[s][t][x], s1[0][t + s - x][x]);
                    }
                    p = lim[i];
                    if (i > 1) {
                        if (2 * i * x >= s + t)
                            p = max((2 * i * x - s - t + i - 2) / (i - 1), p);
                    } else if (2 * x > s + t)
                        p = 0x3f3f3f3f;
                    if (p <= x and p <= s) {
                        inc(h[s][t][x], s2[s - p][t][2 * x - p]);
                        if (x < s)
                            dec(h[s][t][x], s2[s - x - 1][t][x - 1]);
                    }
                }
        for (int s = 0; s <= n; ++s)
            for (int t = 0; s + t <= n; ++t)
                for (int x = 1; x * i <= s + t; ++x)
                    s1[s][t][x] = s2[s][t][x] = g[s][t][x] = 0;
        for (int s = 0; s <= n; ++s)
            for (int t = 0; s + t <= n; ++t)
                for (int x = 1; x * (i + 1) <= s + t; ++x)
                    g[s][t][x] = h[s][t][x], h[s][t][x] = 0;
    }
    for (int i = 1; i <= n; ++i) {
        dec(res[i], res[i + 1]);
        printf("%d ", res[i]);
    }
    putchar('\n');
    return 0;
}

Round 13

怎么打成🤡了。

开场看 T1,研究了半天发现其实它根本不用维护 sum,只需要维护懒标记。欸我这不是会了吗?接下来拿树剖疯狂维护一下懒标记看起来就行了?

发现你需要先处理特殊性质定位到一整个区间的情况,相当于对一条链调用 pushdown,这个拿个树状数组 + 栈就可以直接维护。写写写,拍拍拍,调调调,终于在半场之后搞完了。

然后要考虑分离前缀后缀的情况,再拿两个树状数组分别维护左儿子/右儿子的情况,终于在 4.5h 时搞完了。

接下来花了 0.5h 在后两题拼一些部分分。100+20+32.8。

出榜一看,怎么 T2 才是签到。哇趣,垫底了!

最近几场越来越喜欢肝题了,因为总感觉一道会做的题过了这个村就没这个店了。如果先开了 T2 写完之后写其它题会更有信心一些吧。

还有这个 T1 其实根本不复杂,我到底为啥需要写四个小时?yxh 的代碼能力確有問題!

有一些实力强劲的选手都能同时过 T1、T2,区分国家队的果然就是代码能力?

感觉自己最近训的也不算少,能不能不带磕磕绊绊地写出一道题却需要看运气。说是代码能力的问题,但我觉得代码能力体现在的不是你现在会不会写大码题(如果是这样,我最近还是写了较长代码的),而是体现在写一个题之前你是否已经充分理解这个题的做法的方方面面,也就是说,你做法是不是想麻烦了,你对于这个题的实现难度是否有错误预估,写代码之前你对于这个题的实现细节是不是想憨了。开始写代码之后,也有许多能帮我们减少思考的方式。写代码本身敲键盘的时间很少,但停一停思考这个地方这么写到底优不优秀的时间花去了很多。

启示是以后写代码有思考放到写代码之前思考!代码洁癖别太严重了,要养成模块化思维,这一段代码要写什么功能就要以最快的速度写完,不要写一点就去想这一部分跟其它代码合并一下是不是常数更优秀、更加美观之类的。

我突然意识到我有个啥习惯呢?比如说你写一个树状数组,如果默写板子一下子就写完了,我的手速也不算慢!但是每次我都要想一些奇怪的事情,比如说如果修改函数全部都是单点 +1,那么我的函数原型就一定要是 upd(int x) 而不是 upd(int x, int v),这是奥卡姆剃刀;再比如说差分树状数组,查前缀的正向树状数组和查后缀的反向树状数组实现同样的功能,但是一个是需要 upd(l - 1),另一个是 upd(l),那么我会仔细思考哪一个不用写 -1 就选哪个,因为这样看起来更加美观;再比如说还是差分树状数组,假设前面出现了 upd(x, v) 后面又出现了 upd(x, -v),那我一定会在写出第二个语句的时候回过头检查把两个语句一起删了,因为这样更加简洁。

这虽说是我坚持了很久的好习惯,确实显著减小了我的平均代码常数;由于反复的思考代码的架构是否合适,也节省了我写完代码之后的调试时间成本。但是对于一些更加强调速度的 IOI 赛制竞赛来说却不一定。像 zyf 一样一下 rush 出一大块具备特定功能的代码然后再去做适配,虽然常数更大,但是需要的总用时却更少了。高手也不一定是码风好的选手啊!

《线段树与区间加》from 张定江

首先你要知道 \(va\) 是诈骗用的,线段树有一个性质:区间和就等于其子树中所有懒标记的和。那么我们将 \(va\) 在到根的链上做一个前缀和,把它加到 \(vb\) 上就变成只需要维护懒标记的问题。

考虑特殊性质怎么做,相当于要快速做根到一个点的标记下传。先对广义线段树树剖,然后用一个栈维护重链上所有的懒标记。每次相当于 pop 出重链上的一段前缀的懒标记,把它们推到同一个位置,在这个过程中相当于对该重链所有的轻儿子的懒标记区间加了。所以拿树状数组维护区间加,由于轻儿子一定是链顶,每次访问一条重链的时候把链顶被它父亲 pushdown 下来的懒标记下放下来。

接下来考虑如果不是特殊性质,那么你找出第一次劈开查询区间的线段树节点,接下来相当于操作一段线段树区间的前缀/后缀,发现这相当于对树链上所有为左儿子/右儿子的点进行区间加,开两颗树状数组维护就行了。

正如上面总结所说的,我先写了特殊性质,开了一颗树状数组,接下来写非特殊性质时又多写了两个树状数组,但是我发现第一个树状数组可以不用开,于是删掉了第一颗树状数组,把它改成了对后两颗树状数组同时操作,有点多此一举了。

这份代码是不是写成 shit mountain 了。

#include <algorithm>
#include <cassert>
#include <cstdio>
#include <functional>
#include <queue>
#include <set>
using namespace std;
typedef unsigned ui;
typedef pair<ui, ui> puu;
ui read() {
    char c = getchar();
    ui x = 0;
    while (c < 48 or c > 57)
        c = getchar();
    do
        x = (x * 10u) + (c ^ 48), c = getchar();
    while (c >= 48 and c <= 57);
    return x;
}
const ui N = 400000;
ui n, m, rt, res;
ui l[N], r[N], vt[N], val[N], sg[N];
ui lc[N], rc[N], ft[N], tp[N], de[N], arr[N];
ui *nd[N], tmp[N];
inline ui len(ui p) {
    return r[p] - l[p] + 1;
}
void go(ui p, ui las) {
    val[p] += vt[p] * (r[p] - l[p] + 1);
    tp[p] = las;
    arr[las] = de[p] - de[las] + 1;
    if (l[p] == r[p])
        return;
    if (len(lc[p]) < len(rc[p]))
        swap(lc[p], rc[p]);
    de[lc[p]] = de[p] + 1, ft[lc[p]] = p, vt[lc[p]] += vt[p], go(lc[p], las);
    de[rc[p]] = de[p] + 1, ft[rc[p]] = p, vt[rc[p]] += vt[p], go(rc[p], rc[p]);
}
priority_queue<puu, vector<puu>, greater<puu>> que[N];
struct BIT {
    ui *tr[N], *pre[N];
    void upd(ui p, ui x, ui v, bool t = 1) {
        if (t)
            res += pre[p][x] * v;
        while (x)
            tr[p][x] += v, x ^= (x & -x);
    }
    ui qry(ui p, ui x) {
        ui res = 0;
        while (x < arr[p])
            res += tr[p][x], x += (x & -x);
        return res;
    }
} Ld, Rd;
void proc(ui p, ui x, ui v) {
    ui now = v;
    while (!que[p].empty()) {
        auto [X, V] = que[p].top();
        if (X < x) {
            que[p].pop();
            res -= V * val[nd[p][X]];
            Ld.upd(p, X, -V), Ld.upd(p, x, V);
            Rd.upd(p, X, -V), Rd.upd(p, x, V);
            now += V;
        } else
            break;
    }
    res += now * val[nd[p][x]];
    if (now)
        que[p].emplace(x, now);
}
void build(ui p) {
    nd[p] = new ui[arr[p]];
    Ld.tr[p] = new ui[arr[p]]();
    Rd.tr[p] = new ui[arr[p]]();
    Ld.pre[p] = new ui[arr[p]]();
    Rd.pre[p] = new ui[arr[p]]();
    for (ui i = p; i; i = lc[i])
        nd[p][de[i] - de[p]] = i;
    for (ui i = 0; i + 1 < arr[p]; ++i) {
        Ld.pre[p][i + 1] = Ld.pre[p][i];
        Rd.pre[p][i + 1] = Rd.pre[p][i];
        ui pp = nd[p][i];
        ui lp = rc[pp];
        if (l[lp] == l[pp])
            Ld.pre[p][i + 1] += val[lp];
        if (r[lp] == r[pp])
            Rd.pre[p][i + 1] += val[lp];
        build(lp);
    }
}
void push(ui p) {
    assert(tp[p] == p);
    if (p == rt)
        return;
    ui q = tp[ft[p]];
    ui x = de[p] - de[q];
    ui cnt = 0;
    if (l[ft[p]] == l[p]) {
        ui v = Ld.qry(q, x);
        if (v) {
            Ld.upd(q, x, -v, 0), Ld.upd(q, x - 1, v, 0);
            que[p].emplace(0, v);
        }
    }
    if (r[ft[p]] == r[p]) {
        ui v = Rd.qry(q, x);
        if (v) {
            Rd.upd(q, x, -v, 0), Rd.upd(q, x - 1, v, 0);
            que[p].emplace(0, v);
        }
    }
}
void updL(ui p, ui x, ui v, ui lim = 0) {
    assert(l[nd[p][lim]] <= x and x <= r[nd[p][lim]]);
    push(p);
    ui L = lim, R = arr[p];
    while (L < R) {
        ui mid = (L + R) >> 1;
        if (x >= l[nd[p][mid]] and x < r[nd[p][mid]])
            L = mid + 1;
        else
            R = mid;
    }
    Ld.upd(p, L - (x < l[nd[p][L]]), v);
    if (lim)
        Ld.upd(p, lim, -v);
    if (x >= r[nd[p][L]]) {
        proc(p, L, v);
        if (x > r[nd[p][L]])
            updL(rc[nd[p][L - 1]], x, v);
    } else {
        proc(p, L, 0);
        updL(rc[nd[p][L - 1]], x, v);
    }
}
void updR(ui p, ui x, ui v, ui lim = 0) {
    assert(l[nd[p][lim]] <= x and x <= r[nd[p][lim]]);
    push(p);
    ui L = lim, R = arr[p];
    while (L < R) {
        ui mid = (L + R) >> 1;
        if (x > l[nd[p][mid]] and x <= r[nd[p][mid]])
            L = mid + 1;
        else
            R = mid;
    }
    Rd.upd(p, L - (x > r[nd[p][L]]), v);
    if (lim)
        Rd.upd(p, lim, -v);
    if (x <= l[nd[p][L]]) {
        proc(p, L, v);
        if (x < l[nd[p][L]])
            updR(rc[nd[p][L - 1]], x, v);
    } else {
        proc(p, L, 0);
        updR(rc[nd[p][L - 1]], x, v);
    }
}
void upd(ui p, ui sl, ui sr, ui v) {
    assert(l[p] <= sl and sr <= r[p]);
    push(p);
    ui L = 0, R = arr[p];
    while (L < R) {
        ui mid = (L + R) >> 1;
        if (sl >= l[nd[p][mid]] and sr <= r[nd[p][mid]])
            L = mid + 1;
        else
            R = mid;
    }
    if (l[nd[p][L - 1]] == sl and r[nd[p][L - 1]] == sr)
        return proc(p, L - 1, v);
    if (r[nd[p][L]] < sl or l[nd[p][L]] > sr) {
        proc(p, L, 0);
        return upd(rc[nd[p][L - 1]], sl, sr, v);
    }
    if (l[nd[p][L]] == l[nd[p][L - 1]]) {
        updR(p, sl, v, L);
        updL(rc[nd[p][L - 1]], sr, v);
    }
    if (r[nd[p][L]] == r[nd[p][L - 1]]) {
        updL(p, sr, v, L);
        updR(rc[nd[p][L - 1]], sl, v);
    }
}
int main() {
    n = read(), m = read();
    for (ui i = 1; i < 2 * n; ++i) {
        l[i] = read(), r[i] = read();
        vt[i] = read(), val[i] = read();
        if (l[i] < r[i])
            lc[i] = read(), rc[i] = read();
        if (l[i] == 1 and r[i] == n)
            rt = i;
    }
    go(rt, rt), build(rt);
    for (ui i = 1; i <= m; ++i) {
        ui sl = read(), sr = read(), v = read();
        upd(rt, sl, sr, v);
        printf("%u\n", res);
    }
    return 0;
}

《字符串》from 何钒佑

场上这么多人能过是因为调和级数分块+数点的两只 \(\log\) 直接过了,常数很小。事实上这个算法给跑出来的 Runs 去个重就是单 \(\log\) 了。

首先考虑跟 NOI2023 字符串类似的,先把条件转化成后缀字典序的比较,然后除去算错的贡献。

这里算错的贡献是平方串,考虑用 Runs 刻画这个结构。

发现我们需要考虑所有 \(rk_i<rk_{i+l}\) 的平方串 \(S[i,i+2l-1]\),相当于我们只需要考虑 \(S_{r-p+1}<S_{r+1}\) 的 Runs,也就是说 Runs 的构造方法中你只需要跑一半,恰好构造出来全部满足这个条件的 Runs。(Runs 跟字典序结构还真适配!)

接下来对于每个 Runs 的所有平方串长 \(2kp\) (满足 \(2kp\le r-l+1\))数点,由于上面那篇博客中提到的 \(\sum \lfloor\frac{r-l+1}{p}\rfloor\)\(O(n)\) 级别的,所以复杂度做到了 \(O(n\log n)\)

代码唯一稍微难写的地方在于 SA,如果换成二分+哈希那就巨好写了。

#include <algorithm>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;
typedef pair<int, int> pii;
int read() {
    char c = getchar();
    int x = 0;
    while (c < 48 or c > 57)
        c = getchar();
    do
        x = (x * 10) + (c ^ 48), c = getchar();
    while (c >= 48 and c <= 57);
    return x;
}
const int N = 500003;
const int Lg = 19;
int n, q;
struct LCP {
    char s[N];
    int buc[N], rk[N], sa[N], od[N], id[N], ht[Lg][N], w, p;
    bool eq(int x, int y) {
        return od[x] == od[y] && od[x + w] == od[y + w];
    }
    void getSA(int m) {
        for (int i = 1; i <= n; ++i)
            ++buc[rk[i] = s[i]];
        for (int i = 1; i <= m; ++i)
            buc[i] += buc[i - 1];
        for (int i = n; i; --i)
            sa[buc[rk[i]]--] = i;
        for (int i = 1; i <= m; ++i)
            buc[i] = 0;
        w = 1;
        p = 0;
        while (true) {
            for (int i = n; i > n - w; --i)
                id[++p] = i;
            for (int i = 1; i <= n; ++i)
                if (sa[i] > w)
                    id[++p] = sa[i] - w;
            for (int i = 1; i <= n; ++i)
                ++buc[od[i] = rk[i]];
            for (int i = 1; i <= m; ++i)
                buc[i] += buc[i - 1];
            for (int i = n; i; --i)
                sa[buc[rk[id[i]]]--] = id[i];
            for (int i = 1; i <= m; ++i)
                buc[i] = 0;
            rk[sa[1]] = p = 1;
            for (int i = 2; i <= n; ++i) {
                if (!eq(sa[i], sa[i - 1]))
                    ++p;
                rk[sa[i]] = p;
            }
            if (p == n)
                break;
            w <<= 1, m = p, p = 0;
        }
    }
    void getLCP() {
        s[n + 1] = '!';
        for (int i = 1, k = 0; i <= n; ++i) {
            if (k)
                --k;
            if (rk[i] == 1)
                continue;
            while (s[i + k] == s[sa[rk[i] - 1] + k])
                ++k;
            ht[0][rk[i]] = k;
        }
        for (int t = 1; t < Lg; ++t)
            for (int i = 2; i + (1 << t) - 1 <= n; ++i)
                ht[t][i] = min(ht[t - 1][i], ht[t - 1][i + (1 << (t - 1))]);
    }
    int qry(int x, int y) {
        if (x == y)
            return n - x + 1;
        x = rk[x], y = rk[y];
        if (x > y)
            swap(x, y);
        int k = __lg(y - x);
        return min(ht[k][x + 1], ht[k][y - (1 << k) + 1]);
    }
} A, B;
char s[N];
int wl[N << 1], wr[N << 1], wp[N << 1], num;
void expand(int x, int y) {
    if (s[x] != s[y])
        return;
    int len = y - x;
    int lA = A.qry(x, y);
    int lB = B.qry(n - x + 1, n - y + 1);
    if (lB <= len and lA + lB > len) {
        ++num;
        wl[num] = x - lB + 1;
        wr[num] = y + lA - 1;
        wp[num] = len;
    }
}
int stk[N], tp;
int tr[N];
vector<int> vx[N], vl[N];
vector<pii> vec[N];
void upd(int x, int v) {
    while (x <= n)
        tr[x] += v, x += (x & -x);
}
int qry(int x) {
    int res = 0;
    while (x)
        res += tr[x], x ^= (x & -x);
    return res;
}
int res[N], qx[N], ql[N];
int main() {
    read(), n = read(), q = read();
    char c = getchar();
    while (c < 'a' or c > 'z')
        c = getchar();
    for (int i = 1; i <= n; ++i)
        A.s[i] = B.s[n - i + 1] = s[i] = c, c = getchar();
    A.getSA(123), A.getLCP();
    B.getSA(123), B.getLCP();
    tp = 0;
    for (int i = n; i; --i) {
        while (tp and A.rk[stk[tp]] < A.rk[i])
            --tp;
        if (tp)
            expand(i, stk[tp]);
        stk[++tp] = i;
    }
    for (int i = 1; i <= num; ++i)
        for (int x = wp[i]; 2 * x <= wr[i] - wl[i] + 1; x += wp[i])
            vec[x].emplace_back(wl[i], wr[i] - 2 * x + 1);
    for (int i = 1; i <= q; ++i) {
        vx[A.rk[qx[i] = read()]].emplace_back(i);
        vl[ql[i] = read()].emplace_back(i);
    }
    for (int i = 1; i <= n; ++i) {
        for (auto [l, r] : vec[i])
            upd(l, 1), upd(r + 1, -1);
        for (int x : vl[i])
            res[x] -= qry(qx[x]);
    }
    for (int i = 1; i <= n; ++i)
        tr[i] = 0;
    for (int i = n; i; --i) {
        upd(A.sa[i], 1);
        for (int x : vx[i])
            res[x] += qry(A.sa[i] + ql[x]) - qry(A.sa[i]);
    }
    for (int i = 1; i <= q; ++i)
        printf("%d\n", res[i]);
    return 0;
}

《格雷码》from 范斯喆

抽象构造题,没改。

但是可以看维基

Round 14

《Two Permutations》from 杨博

出了小清新构造,yb 老师我爱你。

但是我做了大半场最后没有做出来,然后发现 yb 老师在赛前发的犇犇里说自己出了互测最简单的普及组题目,遂疯狂破大防,yb 老师我讨厌你。

adhoc 题的难度就应该被称之为 adhoc,而不是普及组/提高组/NOI 这种称呼。 yxh 做不出题不要死鸭子嘴硬。

注意到 \(v_i>i\) 显然不合法,然后猜测 \(v_i\le i\) 均有解。

\((a_i,a_{i+n})\) 视为坐标系上的一个点 \(i\),注意到 \(v_i\) 的限制相当于点 \(i\)\(v_i\) 构成逆序对,与 \(<v_i\) 的所有点构成顺序对。

考虑 \(v_i\le i\) 构成了一棵树,所以!我场上的想法是递归对一个子树构造,然后通过手玩注意到合法构造的树根往往是 \(x\) 轴或 \(y\) 轴上的极值。定义 solX(int u) 表示钦定 \(u\)\(x\) 这一维的最大值,构造 \(u\) 的子树,返回子树中所有点构成的横纵坐标相对顺序。solY(int u) 的定义反过来。

然后你发现这个递归很有道理的一点是当你对于 solX(u),你会先递归调用 \(u\) 的所有儿子 \(v\)solY(v),然后你发现对于每一颗子树的相对顺序你一定会把 \(y_u\) 插在 \(y_v\) 的前面,然后保持 \(x_u\) 是极值。

然后就做完了?然后我就寄了,问题在于与 \(<v_i\) 的所有点构成顺序对的限制,意味着你需要把所有儿子的构造以某种顺序合并成一个大构造,发现直接按点编号排不对,按 \(v_i\) 排归并也不对,但总存在一种合法的归并方式。

事实上我的做法跟正解真实差之毫厘,却谬以千里了。我们不应该考虑对于每一颗子树构造,而是应该直接增量构造,按照编号从小到大扫描所有点。不过我们在上面的两个想法是极其正确的,一个是你需要保证当前加入构造的点是某一维的极值;另一个是,你可以通过对树进行奇偶分层,钦定奇数层的点刚加入时为 \(x\) 维极值,偶数层的点刚加入时为 \(y\) 维极值。

接下来我们可以极其容易地发现本题的构造:用链表维护 \(x\)\(y\) 坐标的相对顺序,按照编号顺序加点入 \(i\),如果是奇数层节点,那么 \(x\) 坐标的设置为新的极值(加到链表末尾),\(y\) 坐标设置为其父亲的前驱;偶数层节点则相反;根则同时直接插到两个链表的最后就行了。这样的话,只在平面上考虑所有 \(\le v_i\) 的点,在这些点中 \(v_i\) 恰好是极值,你将 \(i\) 的该位坐标设置为前驱,那么前面的所有点自然都与 \(i\) 成顺序对。

#include <algorithm>
#include <cstdio>
#include <vector>
using namespace std;
int read() {
    char c = getchar();
    int x = 0;
    while (c < 48 or c > 57)
        c = getchar();
    do
        x = (x * 10) + (c ^ 48), c = getchar();
    while (c >= 48 and c <= 57);
    return x;
}
const int N = 400003;
int n;
int v[N], s[N];
int pa[N], pb[N];
bool par[N];
void solve() {
    n = read();
    for (int i = 1; i <= n; ++i)
        v[i] = read();
    for (int i = 1; i <= n; ++i)
        if (v[i] > i) {
            puts("No");
            return;
        }
    int ea = 0, eb = 0;
    for (int i = 1; i <= n; ++i)
        if (v[i] == i) {
            par[i] = 0;
            pa[i] = ea;
            pb[i] = eb;
            ea = eb = i;
        } else {
            par[i] = par[v[i]] ^ 1;
            if (par[i]) {
                pa[i] = pa[v[i]], pa[v[i]] = i;
                pb[i] = eb, eb = i;
            } else {
                pb[i] = pb[v[i]], pb[v[i]] = i;
                pa[i] = ea, ea = i;
            }
        }
    for (int i = n; i; --i) {
        s[i] = ea, ea = pa[ea];
        s[i + n] = eb, eb = pb[eb];
    }
    puts("Yes");
    for (int i = 1; i <= 2 * n; ++i)
        printf("%d ", s[i]);
    putchar('\n');
}
int main() {
    int tc = read();
    while (tc--)
        solve();
    return 0;
}

《冲刺》from 黄建恒

暂缓 GF 科技。

《串联》from 唐一文

点分治模板?考虑直接堆套路做完!中间部分是一个二维数点,我直接写了线段树。最后擦着时限极限过了?

\(O(n\log^2 n)\),可以通过一些比较繁琐的手段优化到单 \(\log\)

#include <algorithm>
#include <cstdio>
#include <vector>
#define lc (p << 1)
#define rc (p << 1 | 1)
using namespace std;
typedef long long ll;
template <typename T> T read() {
    char c = getchar();
    T x = 0;
    while (c < 48 or c > 57)
        c = getchar();
    do
        x = (x * 10) + (c ^ 48), c = getchar();
    while (c >= 48 and c <= 57);
    return x;
}
const int N = 200003, INF = 0x3f3f3f3f;
int hd[N], ver[N << 1], nxt[N << 1], tot = 1;
void add(int u, int v) {
    nxt[++tot] = hd[u], hd[u] = tot, ver[tot] = v;
}
int a[N], b[N], n, rk, now;
ll V, res = 1e18;
int mnw, all, rt;
int sz[N];
bool del[N];
struct node {
    int a;
    ll b;
    int c, id;
    node(int A = INF, ll B = 0, int C = 0) : a(A), b(B), c(C), id(0) {
    }
} s[N];
void dfs(int u, int fa, int A, ll B) {
    if (A > a[u])
        A = a[u];
    B += b[u];
    s[++rk] = node(A, B, now);
    sz[u] = 1;
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (v == fa or del[v])
            continue;
        dfs(v, u, A, B);
        sz[u] += sz[v];
    }
}
void findrt(int u, int fa) {
    sz[u] = 1;
    int mxw = 0;
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (v == fa or del[v])
            continue;
        findrt(v, u);
        sz[u] += sz[v];
        if (sz[v] > mxw)
            mxw = sz[v];
    }
    if (all - sz[u] > mxw)
        mxw = all - sz[u];
    if (mnw > mxw)
        mnw = mxw, rt = u;
}
int col[N << 2];
ll buc[N];
void build(int p = 1, int l = 1, int r = rk) {
    col[p] = 0;
    if (l == r)
        return;
    int mid = (l + r) >> 1;
    build(lc, l, mid);
    build(rc, mid + 1, r);
}
void update(int x, int c, int p = 1, int l = 1, int r = rk) {
    if (!col[p])
        col[p] = c;
    else
        col[p] = -1;
    if (l == r)
        return;
    int mid = (l + r) >> 1;
    if (x <= mid)
        update(x, c, lc, l, mid);
    else
        update(x, c, rc, mid + 1, r);
}
int jump(ll x, int c, int p = 1, int l = 1, int r = rk) {
    if (buc[r] < x or !col[p] or col[p] == c)
        return 0;
    if (l == r)
        return r;
    int mid = (l + r) >> 1;
    int t = jump(x, c, lc, l, mid);
    if (t)
        return t;
    return jump(x, c, rc, mid + 1, r);
}
void solve(int u) {
    all = sz[u], mnw = INF;
    findrt(u, 0);
    del[u = rt] = 1;
    s[rk = 1] = node(a[u], b[u], u);
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (del[v])
            continue;
        now = v;
        dfs(v, u, a[u], b[u]);
    }
    sort(s + 1, s + rk + 1, [](node x, node y) {
        return x.b < y.b;
    });
    for (int i = 1; i <= rk; ++i)
        s[i].id = i, buc[i] = s[i].b;
    sort(s + 1, s + rk + 1, [](node x, node y) {
        return x.a > y.a;
    });
    build();
    for (int i = 1; i <= rk; ++i) {
        int t = jump((V + s[i].a - 1) / s[i].a - s[i].b + b[u], s[i].c);
        if (t)
            res = min(res, buc[t] + s[i].b - b[u]);
        update(s[i].id, s[i].c);
    }
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (del[v])
            continue;
        solve(v);
    }
}
int main() {
    n = read<int>(), V = read<ll>();
    for (int i = 1; i <= n; ++i) {
        a[i] = read<int>(), b[i] = read<int>();
        if ((ll)a[i] * b[i] >= V)
            res = min(res, (ll)b[i]);
    }
    for (int i = 1; i < n; ++i) {
        int u = read<int>(), v = read<int>();
        add(u, v), add(v, u);
    }
    sz[1] = n, solve(1);
    printf("%lld\n", res);
    return 0;
}

Round 15

《药水》from 武林

之后再说。

《字符游戏》from 姚熙

其实完全不用点分治啊,随便分析一下可以发现记忆化复杂度完全正确!

考虑给一个串这个游戏怎么玩,发现这是 SG 函数经典应用,枚举字符 \(c\),切断所有字符 \(c\) 的位置,形成的每一个子串都是一个子游戏。

我们考虑如何分析,对于每一个点 \(u\),我们求出 \(u\) 不断往上跳直到遇到第一个颜色为 \(c\) 的点 up[u][c],求出 \(u\) 开始(包含)到 up[u][c] 结束(不包含)的树链的 SG 值 val[u][c]。这个可以用倍增求出,倍增数组形如往上跳 \(2^k\) 个颜色为 \(c\) 的点,中间部分所有 SG 值的异或和。

然后考虑回答询问,先枚举分割的颜色 \(c\),然后利用刚刚的预处理的信息往上跳,直到 LCA 之前的最后一个颜色为 \(c\) 的点。(需要特殊处理 LCA 的颜色就是 \(c\) 的情况)

然后你发现这就只需要递归地求包含 LCA 的这一段路径就行了。

发现这个过程,只会访问形如这样的路径:钦定一个颜色 \(c\),从 LCA 开始往 \(u\)\(v\) 扫直到扫到一个颜色第一次出现(当然,也有可能直接扫到 \(x,y\)),中间部分形成的路径。自然地,这样的路径只 \(O(|\Sigma|^2)\) 种。

所以说记忆化一下,回答单次询问的复杂度就做到了 \(O(|\Sigma|^3\log n)\)。为啥跑进 1s 了?

#include <algorithm>
#include <cstdio>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/hash_policy.hpp>
using namespace std;
int read() {
    char c = getchar();
    int x = 0;
    while (c < 48 or c > 57)
        c = getchar();
    do
        x = (x * 10) + (c ^ 48), c = getchar();
    while (c >= 48 and c <= 57);
    return x;
}
const int N = 50003, Lg = 16, C = 10;
typedef pair<int, int> pii;
__gnu_pbds::gp_hash_table<int, pii> mp[N];
int n, q, num;
int a[N], ft[N], dfn[N], de[N];
int mn[Lg][N];
int up[N][C], val[N][C];
int f[N][Lg], g[N][Lg];
int hd[N], ver[N << 1], nxt[N << 1], tot;
void add(int u, int v) {
    nxt[++tot] = hd[u], hd[u] = tot, ver[tot] = v;
}
inline int calc(int msk) {
    return __builtin_ctz(~msk);
}
inline int cmp(int x, int y) {
    if (dfn[x] < dfn[y])
        return x;
    else
        return y;
}
inline int lca(int u, int v) {
    if (u == v)
        return u;
    if ((u = dfn[u]) > (v = dfn[v]))
        swap(u, v);
    int k = __lg(v - u++);
    return cmp(mn[k][u], mn[k][v - (1 << k) + 1]);
}
int p[C];
void dfs(int u, int fa) {
    mn[0][dfn[u] = ++num] = fa;
    ft[u] = fa;
    de[u] = de[fa] + 1;
    f[u][0] = up[fa][a[u]];
    g[u][0] = val[fa][a[u]];
    for (int t = 1; t < Lg; ++t) {
        int nx = f[u][t - 1];
        f[u][t] = f[nx][t - 1];
        g[u][t] = g[nx][t - 1] ^ g[u][t - 1];
    }
    for (int c = 0; c < C; ++c)
        up[u][c] = up[fa][c];
    up[u][a[u]] = u;
    for (int c = 0; c < C; ++c)
        p[c] = c;
    sort(p, p + C, [&](int x, int y) {
        return de[up[u][x]] > de[up[u][y]];
    });
    for (int i = 0; i < C; ++i) {
        int x = up[u][p[i]];
        int msk = 0;
        for (int j = 0; j < i; ++j) {
            int y = up[u][p[j]];
            if (!y)
                continue;
            int cur = val[u][p[j]];
            for (int t = Lg - 1; ~t; --t)
                if (de[f[y][t]] > de[x])
                    cur ^= g[y][t], y = f[y][t];
            cur ^= val[ft[y]][p[i]];
            msk |= (1 << cur);
        }
        val[u][p[i]] = calc(msk);
    }
    for (int i = hd[u]; i; i = nxt[i]) {
        int v = ver[i];
        if (v == fa)
            continue;
        dfs(v, u);
    }
}
int ncnt;
int qry(int nx, int ny) {
    if (nx == ny)
        return ncnt = 1;
    if (nx > ny)
        swap(nx, ny);
    auto it = mp[nx].find(ny);
    if (it != mp[nx].end())
        return ncnt = it->second.first, it->second.second;
    int z = lca(nx, ny);
    int cnt = 0, msk = 0;
    for (int c = 0; c < C; ++c) {
        int x = nx, y = ny;
        int cur = 0;
        if (de[up[x][c]] > de[z]) {
            cur ^= val[x][c], x = up[x][c];
            for (int t = Lg - 1; ~t; --t)
                if (de[f[x][t]] > de[z])
                    cur ^= g[x][t], x = f[x][t];
            x = ft[x];
        }
        if (de[up[y][c]] > de[z]) {
            cur ^= val[y][c], y = up[y][c];
            for (int t = Lg - 1; ~t; --t)
                if (de[f[y][t]] > de[z])
                    cur ^= g[y][t], y = f[y][t];
            y = ft[y];
        }
        if (a[z] == c) {
            cur ^= val[x][c] ^ val[y][c];
            msk |= (1 << cur);
            if (!cur)
                ++cnt;
        } else if (x != nx or y != ny) {
            cur ^= qry(x, y);
            msk |= (1 << cur);
            if (!cur)
                ++cnt;
        }
    }
    mp[nx][ny] = make_pair(cnt, calc(msk));
    return ncnt = cnt, calc(msk);
}
int main() {
    n = read(), q = read();
    char c = getchar();
    while (c < 48 or c > 57)
        c = getchar();
    for (int i = 1; i <= n; ++i)
        a[i] = c ^ 48, c = getchar();
    for (int i = 1; i < n; ++i) {
        int u = read(), v = read();
        add(u, v), add(v, u);
    }
    dfs(1, 0);
    for (int t = 1; t < 19; ++t)
        for (int i = 1; i + (1 << t) - 1 <= n; ++i)
            mn[t][i] = cmp(mn[t - 1][i], mn[t - 1][i + (1 << (t - 1))]);
    while (q--) {
        int x = read(), y = read();
        if (qry(x, y))
            printf("Alice %d\n", ncnt);
        else
            puts("Bob");
    }
    return 0;
}

《子集和》from 赵英智

场上只写了 65,标算的 \(\log\) 去得出其不意啊!牛的。

循环卷积多项式乘法信息难有可减性,至少很麻烦可减,所以这里就当不可减信息做就行了。

注意到这里的信息虽然合并是 \(O(m^2)\) 的,但如果是最后只合并一次,那么就只用 \(O(m)\) 求出一个单点就行了。同时,合并一个单点信息也是只用 \(O(m)\) 的。

询问两个单点怎么做?考虑将询问挂到线段树上第一次劈开它的节点,为了保证答案只合并一次,我们需要对于线段树的一个区间 \([l,r]\) 的每一个点 \(x\),求出 \([1,r]/\{x\}\) 的信息和 \([l,n]/\{x\}\) 的信息。这个可以用缺一分治做,预处理复杂度两只 \(\log\) 带上 \(m\)

然后考虑一般的询问,其实就是拆成 \(x\in [l_1,r_1],y\in [l_2,r_2]\) 的所有询问的和。一个简单的想法是直接在线段树上计算,由于你求出了挖去每一个位置的信息,你也可以对这些信息做前缀和来快速区间查询。所以一个 \([l_1,r_1],[l_2,r_2]\) 的询问可以快速拆成 \(\log\) 个所有点在同一个线段树区间上的询问。用前缀和数组就可以快速计算这种询问了,询问复杂度 \(O(qm\log n)\) 无法通过。这也是我场上的想法。

注意到古板地在线段树上拆分很愚蠢。注意到我们可以把询问直接拆成一个前缀和一个后缀查询的形式。考虑如何解决这种形式的询问。你可以直接 DP 求出,每一个前缀的信息,以及每一个前缀挖去恰好一个点的信息和。后缀同理。

那么你处理一段前缀一段后缀的询问时,直接把它挂到第一次劈开它的线段树节点上,那么除了算挖去的点包含在线段树里的贡献,剩下的贡献都形如对于 \([1,l-1]\)\([r+1,n]\) 这样的区间挖去恰好一个点的信息和,加上 \([l,mid]\)\([mid,r]\) 的信息。直接利用刚才的 DP 信息就行了。询问复杂度降低到了 \(O(qm)\)

#include <cstdio>
#include <vector>
#define lc (p << 1)
#define rc (p << 1 | 1)
using namespace std;
typedef long long ll;
typedef __int128 lll;
int read() {
    char c = getchar();
    int x = 0;
    while (c < 48 or c > 57)
        c = getchar();
    do
        x = (x * 10) + (c ^ 48), c = getchar();
    while (c >= 48 and c <= 57);
    return x;
}
const int P = 1000000007, M = 200;
const int Q = 1000002, N = 10002;
int n, m, q;
lll res[Q], arr[M];
int a[N], b[N], tmp[M];
int al[Q], ar[Q], bl[Q], br[Q];
int f[N << 2][M];
int qa[Q << 2], qb[Q << 2];
inline void inc(int &x, int v) {
    if ((x += v) >= P)
        x -= P;
}
void go(int *F, int l, int r, int p) {
    if (r - l + 1 <= m) {
        for (int x = l; x <= r; ++x) {
            for (int i = 0; i < m; ++i)
                tmp[i] = F[i];
            for (int i = 0, j = a[x]; i < m; ++i, ++j) {
                if (j == m)
                    j = 0;
                inc(tmp[j], F[i]);
            }
            for (int i = 0, j = b[x]; i < m; ++i, ++j) {
                if (j == m)
                    j = 0;
                inc(tmp[j], F[i]);
            }
            for (int i = 0; i < m; ++i)
                F[i] = tmp[i];
        }
    } else {
        for (int i = 0; i < m; ++i)
            if (f[p][i])
                for (int j = 0, k = i; j < m; ++j, ++k) {
                    if (k == m)
                        k = 0;
                    arr[k] += (ll)f[p][i] * F[j];
                }
        for (int i = 0; i < m; ++i)
            F[i] = arr[i] % P, arr[i] = 0;
    }
}
void mul(int l, int r, int p) {
    if (l == r) {
        inc(f[p][0], 1), inc(f[p][a[l]], 1), inc(f[p][b[r]], 1);
        return;
    }
    int mid = (l + r) >> 1;
    mul(l, mid, lc), mul(mid + 1, r, rc);
    for (int i = 0; i < m; ++i)
        f[p][i] = f[lc][i];
    go(f[p], mid + 1, r, rc);
}
int p0[N][M], p1[N][M];
int s0[N][M], s1[N][M];
int cur[N << 2][M];
int sm[N][M], now[M];
void calc(int l, int r, int p) {
    if (l == r) {
        for (int i = 0; i < m; ++i)
            sm[l][i] = cur[p][i];
        return;
    }
    int mid = (l + r) >> 1;
    for (int i = 0; i < m; ++i)
        cur[lc][i] = cur[rc][i] = cur[p][i];
    go(cur[lc], mid + 1, r, rc);
    calc(l, mid, lc);
    go(cur[rc], l, mid, lc);
    calc(mid + 1, r, rc);
}
void solve(int l, int r, vector<int> vec, int p) {
    if (l == r)
        return;
    int mid = (l + r) >> 1;
    vector<int> lef, rig;
    for (int i = 0; i < m; ++i)
        cur[lc][i] = p0[l - 1][i];
    calc(l, mid, lc);
    for (int i = 0; i < m; ++i)
        cur[rc][i] = s0[r + 1][i];
    calc(mid + 1, r, rc);
    for (int i = 0; i < m; ++i)
        now[i] = p1[l - 1][i];
    go(now, l, mid, lc);
    for (int i = 0; i < m; ++i)
        inc(sm[l][i], now[i]);
    for (int i = 0; i < m; ++i)
        now[i] = s1[r + 1][i];
    go(now, mid + 1, r, rc);
    for (int i = 0; i < m; ++i)
        inc(sm[r][i], now[i]);
    for (int x = l + 1; x <= mid; ++x)
        for (int i = 0; i < m; ++i)
            inc(sm[x][i], sm[x - 1][i]);
    for (int x = r - 1; x > mid; --x)
        for (int i = 0; i < m; ++i)
            inc(sm[x][i], sm[x + 1][i]);
    for (int x : vec) {
        if (qb[x] <= mid) {
            lef.emplace_back(x);
            continue;
        }
        if (qa[x] > mid) {
            rig.emplace_back(x);
            continue;
        }
        if (x & 1) {
            res[x >> 2] -= (ll)sm[qa[x]][0] * sm[qb[x]][0];
            for (int i = 1, j = m - 1; i < m; ++i, --j)
                res[x >> 2] -= (ll)sm[qa[x]][i] * sm[qb[x]][j];
        } else {
            res[x >> 2] += (ll)sm[qa[x]][0] * sm[qb[x]][0];
            for (int i = 1, j = m - 1; i < m; ++i, --j)
                res[x >> 2] += (ll)sm[qa[x]][i] * sm[qb[x]][j];
        }
    }
    solve(l, mid, lef, lc);
    solve(mid + 1, r, rig, rc);
}
int main() {
    n = read(), m = read(), q = read();
    for (int i = 1; i <= n; ++i)
        a[i] = read(), b[i] = read();
    p0[0][0] = 1;
    for (int x = 1; x <= n; ++x) {
        for (int i = 0; i < m; ++i)
            p0[x][i] = p0[x - 1][i], p1[x][i] = p1[x - 1][i];
        for (int i = 0, j = a[x], k = b[x]; i < m; ++i, ++j, ++k) {
            if (j == m)
                j = 0;
            if (k == m)
                k = 0;
            inc(p1[x][i], p0[x - 1][i]);
            inc(p0[x][j], p0[x - 1][i]);
            inc(p0[x][k], p0[x - 1][i]);
            inc(p1[x][j], p1[x - 1][i]);
            inc(p1[x][k], p1[x - 1][i]);
        }
    }
    s0[n + 1][0] = 1;
    for (int x = n; x; --x) {
        for (int i = 0; i < m; ++i)
            s0[x][i] = s0[x + 1][i], s1[x][i] = s1[x + 1][i];
        for (int i = 0, j = a[x], k = b[x]; i < m; ++i, ++j, ++k) {
            if (j == m)
                j = 0;
            if (k == m)
                k = 0;
            inc(s1[x][i], s0[x + 1][i]);
            inc(s0[x][j], s0[x + 1][i]);
            inc(s0[x][k], s0[x + 1][i]);
            inc(s1[x][j], s1[x + 1][i]);
            inc(s1[x][k], s1[x + 1][i]);
        }
    }
    for (int i = 0; i < q; ++i) {
        int al = read() - 1, ar = read(), bl = read(), br = read() + 1;
        qa[i << 2 | 0] = ar, qb[i << 2 | 0] = bl;
        qa[i << 2 | 1] = ar, qb[i << 2 | 1] = br;
        qa[i << 2 | 2] = al, qb[i << 2 | 2] = br;
        qa[i << 2 | 3] = al, qb[i << 2 | 3] = bl;
    }
    vector<int> init;
    for (int i = 0; i < (q << 2); ++i)
        if (qa[i] and qb[i] <= n)
            init.emplace_back(i);
    mul(1, n, 1);
    solve(1, n, init, 1);
    for (int i = 0; i < q; ++i) {
        res[i] %= P;
        if (res[i] < 0)
            res[i] += P;
        printf("%d\n", (int)res[i]);
    }
    return 0;
}

Round 16

《数位 DP》from 孙恒喆

牛牛题。

场上想到了一个固定 \(m\) 刻画 \(c_i\) 合不合法的判定,据此设计了一个 DP,但是这个 DP 有个地方想假了,于是就寄了。但说实话这个想法只是绕了很多,其实还是与原做法殊途同归的,毕竟还是搞出了一个可以 DP 的判定。

脱离这个思维。这个题需要一个关键想法,那就是应该考虑 \(c_i\) 能生成哪些 \(m\),而不是 \(m\) 能生成哪些 \(c_i\)

首先你可以把所有 xor 操作看成 or 操作,因为你发现这个题你永远可以把 \(a_i\) 二进制表示中的一个 1 修改成 0。那么你修改 1 的定义,令 1 代表有可能是 1 有可能是 0,此时 xor 操作的表现与 or 相同。

接下来,考虑这个只有与和或操作的序列能生成哪些 \(m\)。归纳一下就可以发现其能生成的 \(m\) 一定是一段前缀。

我们得到了我们关于 \(c_i\) 的一个好的判定算法:

维护一个能生成 \(m\) 的上限的二进制表示 \(s\),如果遇到 and 操作直接取与就行了,如果遇到 or 操作先与 \(c_i\) 取或,然后注意到最高的 \(s\)\(c_i\) 都为 1 的二进制位,这个位后面可以生成一个全 1 的后缀,所以再与这一段全 1 后缀取个或。

接下来考虑如何把这个过程倒过来。维护一个 \(s\) 表示前缀的 \(c_i\) 生成的数的最低限度:

对于 and 操作,只需要要求当前的 \(c_i\) 高于这个最低限度。

对于 or 操作,这个时候相当于从 \(s\) 的二进制表示中差去 \(c_i\) 这个集合,然后还是找到最高的 \(s\) 为 0 但是 \(c_i\) 为 1 的位,将 \(s\) 后面的位全部清零。只有 or 操作会对 \(s\) 产生变化。

考虑对于一个固定的 \(s\) 变化序列计算这个过程中生成的 \(c_i\) 的方案数。对于 and 操作直接乘上 \(2^k-s\),对于 or 操作需要一些更精细的考虑,按下不表。这样子我们就得到了倒着做的一个想法。

我们考虑拆开这个 \(2^k-s\),对于每次 and 操作钦定一个 \(p\),满足 \(s\) 的对应位是 1(当然,\(m\) 的对应位也自然需要是 1),然后给答案乘上 \(-2^{p}\);或者不钦定直接乘上 \(2^k\)

由于我们需要对于所有 \(m\) 进行考虑,所以不能是从一个固定的 \(m\) 出发去 DP。注意到如果钦定第一个位置的操作是 and,这样的话 DP 到最终 \(s\) 没有任何限制(可以认为计算的初值是 \(2^k-1\))。那么在这个从后往前倒着 DP 的过程中我们不需要知道最终的 \(s\) 的形态,而只需要知道最初的 \(s\) 的形态。于是我们考虑再次倒过来,但是 DP 状态里初始化 \(s\) 为所有位置都不知道。

接下来往后扫,如果遇到一个 and,钦定了某一位 \(p\) 是 1 就记录到状态里。然后发现,这些已经确定是 1 的位置对于 or 操作的影响是:

找到所有已经确定是 1 的位置中最低的那一个 \(x\),在比它还高的位置上不能有 \(s\) 为 0 但 \(c_i\) 为 1 的位,否则的话 \(p\) 这一位早就被清零了,也就是说这里如果 \(c_i\) 该位是 0 表示 \(s\) 这一位没发生任何变化,如果 \(c_i\) 的该位是 1 那么一定是变化前的 \(s\) 这一位是 0,变化后的 \(s\) 这一位是 1。注意到一个位为 1 的是一段后缀,而这个后缀开始的端点一定早于这一位被钦定为 1 的位置。对于这一位你只需要决策后缀开始的端点在哪一次or 操作就行了,也就是说方案数乘上 \(x\) 第一次不高于这个位的位置和这个位第一次被钦定的位置之间,or 操作的个数加上一(因为可以一直是保持一样到最后)。

而对于比 \(x\) 还低的位呢?发现由于前面你没有对于这些位置进行任何钦定,也就是说这部分 \(s\) 随便变成啥样都行,那么这一部分的 \(c_i\) 当然也是随便取,系数是 \(2^x\)

接下来考虑如何完成这个 DP。首先由于模数是 \(2^{32}\),考虑利用这个性质。我们首先是需要在状态中维护 \(x\) 轮廓线的形态,遇到 and 加入一个新的被钦定的位的时候,需要我们定位到这个位第一次出现在轮廓线上方时的位置。

那么这东西的状态数是否可以接受呢?维护轮廓线形态,实际上就是维护每一个位作为 \(x\) 的出现次数,而每作为 \(x\) 出现一次,系数都要多乘上一个 \(2^x\),由于系数积不能乘爆 \(2^32\),那么这个东西实际上就是拆分数。

注意到一个问题,就是 \(x=0\) 的时候,其乘上多少个都不会爆,所以我们在存储状态的时候应该这样存储:对于当前的 \(x\) 不将其加入目前的轮廓线中,对于目前的轮廓线,记录每个位的出现次数,对于每个从未出现在轮廓线的位置的 \(p\) 只需要记录其是否出现过。这样状态数就是严格拆分数级别了。

#include <cstdio>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/hash_policy.hpp>
#include <random>
using namespace std;
mt19937_64 rng(20070329);
int read() {
    char c = getchar();
    int x = 0;
    bool f = 0;
    while (c < 48 or c > 57)
        f |= (c == '-'), c = getchar();
    do
        x = (x * 10) + (c ^ 48), c = getchar();
    while (c >= 48 and c <= 57);
    return f ? -x : x;
}
const int S = 200003, N = 1003, K = 33;
typedef unsigned ui;
typedef unsigned long long ull;
__gnu_pbds::gp_hash_table<ull, int> mp;
int n, k, q, id, now;
bool s[N];
ui w[S][K], v[S][K];
ui a[K], msk[S];
ull val[K];
ui f[S], g[S];
int ask() {
    ull res = 0;
    for (int i = 0; i <= k; ++i)
        res += a[i] * val[i];
    auto it = mp.find(res);
    if (it == mp.end()) {
        mp[res] = ++id;
        for (int i = 0; i <= k; ++i)
            w[id][i] = a[i];
        return id;
    } else
        return it->second;
}
ui goshift(ui x, int y) {
    if (y >= 32)
        return 0;
    return x << y;
}
void trans(int x) {
    if (!f[x])
        return;
    g[x] += f[x] << k;
    int sum = a[k], mn = 0;
    while (mn < k and !a[mn])
        ++mn;
    for (int i = k - 1; ~i; --i)
        if (a[i]) {
            g[x] -= f[x] << i;
            sum += a[i] - 1;
        } else {
            if (i < mn) {
                ui t = goshift(f[x], i + mn * (now - sum));
                if (t) {
                    a[i] = 1;
                    a[mn] += now - sum;
                    int y = ask();
                    g[y] -= t;
                    a[i] = 0;
                    a[mn] -= now - sum;
                }
            } else {
                if (f[x] << i) {
                    a[i] = 1;
                    int y = ask();
                    g[y] -= (ui)(now - sum + 1) * (f[x] << i);
                    a[i] = 0;
                }
            }
        }
}
int main() {
    n = read(), k = read(), q = read();
    char c = getchar();
    while (c != 'X' and c != 'A' and c != 'O')
        c = getchar();
    for (int i = 0; i <= k; ++i)
        val[i] = rng();
    s[1] = 1;
    for (int i = 2; i <= n; ++i) {
        if (c == 'A')
            s[i] = 1;
        c = getchar();
    }
    ask();
    f[1] = 1;
    for (int i = 1; i <= n; ++i) {
        if (s[i]) {
            int nid = id;
            for (int x = 1; x <= nid; ++x) {
                for (int t = 0; t <= k; ++t)
                    a[t] = w[x][t];
                trans(x);
            }
            for (int x = 1; x <= id; ++x)
                f[x] = g[x], g[x] = 0;
        } else
            ++now;
    }
    for (int x = 1; x <= id; ++x) {
        int mn = 0, sum = w[x][k];
        while (mn < k and !w[x][mn])
            ++mn;
        for (int i = k - 1; ~i; --i)
            if (w[x][i]) {
                sum += w[x][i] - 1;
                msk[x] |= (1u << i);
            } else {
                if (i < mn)
                    v[x][i] = 1;
                else
                    v[x][i] = (now - sum + 1);
            }
        f[x] = goshift(f[x], mn * (now - sum));
    }
    while (q--) {
        ui m = read();
        ui res = 0;
        for (int x = 1; x <= id; ++x)
            if ((msk[x] & m) == msk[x]) {
                ui tmp = f[x];
                for (int i = 0; i < k; ++i)
                    if ((m ^ msk[x]) >> i & 1)
                        tmp *= v[x][i];
                res += tmp;
            }
        printf("%u\n", res);
    }
    return 0;
}

《数据结构》from 朱屹帆

去年那道互测题帮 zyf 验了但是没写代码,疯狂悔恨!!!但是场上不想写这个题了,于是疯狂在开 T1。

重标号树剖记得是三年前我们这一届组织打 NOI2021 同步赛时,小 Z 场上独立搞出来的想法切了 D1T1。(当时我们这一届菜到没几个人会 D1T1,谁成想三年后的今天能有结局如此。)

当然,这并不是重标号树剖第一次被发明,但是小 Z 与重标号树剖就此结下了不解之缘。

那今年这道互测题究竟出现了什么问题呢?为啥题面还没写完,大样例还和题目对不上呢?

其实因为小 Z 以为他的互测题在最后一场!然后我场上疯狂用 QQ 摇正在上预科的小 Z。

最后了解到这个题的状况是:

题面就是还没有施工完,数据是造好了,但是出题人造数据的同时在出一场联考,不小心把联考题的大样例和数据传到了 OJ 上。然后由于不知道今天互测所以没有进行 final check。

所以 yxh 这次打算写不写代码呢?会写的,待会再说。

《problem》from 马梓航

这不是二维数点 + 二项式定理板子吗?看到题都怀疑自己是不是看错题了?

#include <cstdio>
#include <vector>
using namespace std;
int read() {
    char c = getchar();
    int x = 0;
    while (c < 48 or c > 57)
        c = getchar();
    do
        x = (x * 10) + (c ^ 48), c = getchar();
    while (c >= 48 and c <= 57);
    return x;
}
const int N = 100003;
const int P = 1000000007;
typedef long long ll;
int n, k, m;
vector<int> Val[N], Var[N];
vector<int> Vbl[N], Vbr[N];
int al[N], ar[N];
int bl[N], br[N];
int qp(int a, int b) {
    int res = 1;
    while (b) {
        if (b & 1)
            res = (ll)res * a % P;
        a = (ll)a * a % P;
        b >>= 1;
    }
    return res;
}
inline void inc(int &x, int v) {
    if ((x += v) >= P)
        x -= P;
}
int tr[N];
void upd(int x, int v) {
    for (int i = x; i <= n; i += (i & -i))
        inc(tr[i], v);
}
int qry(int x) {
    int res = 0;
    for (int i = x; i; i ^= (i & -i))
        inc(res, tr[i]);
    return res;
}
int res[N];
int c[20][20];
int neg(int x) {
    if (x)
        return P - x;
    else
        return 0;
}
int main() {
    n = read(), k = read(), m = read();
    for (int i = 0; i <= k; ++i) {
        c[i][0] = 1;
        for (int j = 1; j <= i; ++j) {
            c[i][j] = c[i - 1][j - 1] + c[i - 1][j];
            if (c[i][j] >= P)
                c[i][j] -= P;
        }
    }
    for (int i = 1; i <= n; ++i) {
        al[i] = read(), ar[i] = read();
        Val[al[i]].emplace_back(i);
        Var[ar[i]].emplace_back(i);
    }
    for (int i = 1; i <= m; ++i) {
        bl[i] = read(), br[i] = read();
        Vbl[bl[i]].emplace_back(i);
        Vbr[br[i]].emplace_back(i);
    }
    for (int i = 1; i <= n; ++i)
        tr[i] = 0;
    for (int i = n; i; --i) {
        for (int x : Val[i])
            upd(ar[x], qp(ar[x] - al[x] + 1, k));
        for (int x : Vbl[i])
            inc(res[x], qry(br[x]));
    }
    for (int i = 1; i <= n; ++i)
        tr[i] = 0;
    for (int i = n; i; --i) {
        for (int x : Vbr[i])
            inc(res[x], (ll)qry(bl[x] - 1) * qp(br[x] - bl[x] + 1, k) % P);
        for (int x : Var[i])
            upd(al[x], 1);
    }
    for (int t = 0; t <= k; ++t) {
        for (int i = 1; i <= n; ++i)
            tr[i] = 0;
        for (int i = 1; i <= n; ++i) {
            for (int x : Var[i]) {
                upd(al[x], qp(ar[x], t));
                upd(ar[x], neg(qp(ar[x], t)));
            }
            for (int x : Vbr[i])
                res[x] = (res[x] + (ll)qry(bl[x] - 1) *
                                       qp(P - bl[x] + 1, k - t) % P * c[k][t]) %
                         P;
        }
    }
    for (int t = 0; t <= k; ++t) {
        for (int i = 1; i <= n; ++i)
            tr[i] = 0;
        for (int i = n; i; --i) {
            for (int x : Vbr[i]) {
                res[x] = (res[x] + (ll)qry(n - bl[x] + 1) * qp(br[x], k - t) %
                                       P * c[k][t]) %
                         P;
                res[x] = (res[x] + (ll)neg(qry(n - br[x])) * qp(br[x], k - t) %
                                       P * c[k][t]) %
                         P;
            }
            for (int x : Var[i])
                upd(n - al[x] + 1, qp(P - al[x] + 1, t));
        }
    }
    for (int i = 1; i <= m; ++i)
        printf("%d\n", res[i]);
    return 0;
}
posted @ 2024-10-15 19:43  yyyyxh  阅读(722)  评论(0编辑  收藏  举报