UOJ Logo rushcheyo的博客

博客

UOJ Long Round #2 题解

2021-06-27 20:00:43 By rushcheyo

后门

Idea, data, std, solution from command_block

算法一

直接按照定义递归计算,复杂度递归式为 T(n)=O(n)+d2d×T(n/d)

解得 T(n)=O(n2.73) ,过 800 不是问题。

期望得分 30

算法二

感谢 ezoilearner 的帮助。

先特判 n=1 的情况。

考虑贡献,设 S(n,m) 为长为 n 的序列的第 m 个位置的贡献系数。

我们的目标是求出 S(n,0)S(n,n1)。然后答案可以这样计算 :

ans=m=0n1S(n,m)Am

考虑如何计算单个 S(n,m)

将一种筛选记为 (c,k) ,表示保留了所有模 kc 的位置。

筛选是可以叠加的。 两次筛选 (c1,k1),(c2,k2) 叠加,则等效于筛选 (c1+k1c2,k1k2)

S(n,m) 相当于计数有多少种筛选序列叠加后最终得到单一的 {Am}

注意到我们总是可以根据 m,k 唯一确定出 (c,k) 中的 c=mmodk ,所以我们可以只关心筛选中的模数 k ,而不关心余数 c

考虑一个筛选序列符合要求的充要条件。记筛选序列为 {k1,k2,k3,...,kp}

  • 在筛选 k1p1 生效后(只有最后一个 kp 未生效),剩余的数至少有两个(否则就停止了)。

    s=i=1p1ki (前 p1 个筛选叠加),则充要条件为 0ms (在前面有一个数)或 m+s<n (在后面有一个数)。

    等价的条件为 smax(m,nm1)

  • 在最后一步,筛选中的数变为单一的 m。(并停止)

    最后一步的筛选 kp 要满足 skp>max(m,nm+1) (将前面的条件取反)

    除此之外,还要满足 kp 不超过 s 生效后的序列长度。

    这个长度为 m/s+(n1m)/s+1 (分别计算 m 前后的位置个数)。

    总的条件为 kp(max(m,nm1)/s,m/s+(n1m)/s+1]

综上,我们只需要得知 i=1p1kikp 即可判定一个筛选序列是否符合要求。

定义 H(n) 为各元素 >1 的有序整数序列乘积为 n 的方案数。

H(n) 可由以下简单 DP 求解 : H(n)=d|n,dnH(d)

复杂度为 O(i=1nd(i))=O(nlogn)

考虑枚举 s ,则 kp 的方案数可以确定。k1p1 得到 s 的方案数即为 H(s)

S(n,m)=s=1max(m,nm1)H(s)(m/s+(nm1)/s+1max(m,nm1)/s)=s=1max(m,nm1)H(s)(min(m,nm1)/s+1)

上式容易整除分块计算。逐个计算 S ,复杂度 O(nn) 。期望得分 65

算法三

对每个 m 均进行整除分块是不优的,考虑利用之前的信息来优化复杂度。

首先做一步转化 :

D(n)=sH(s)n/s

S(n,m)=D(min(m,nm1))+s=1max(m,nm1)H(s)

然后考虑如何求 D(1n)

定义 ΔD(n)=D(n)D(n1)

ΔD(n)=sH(s)(n/s(n1)/s)

注意到 n/s(n1)/s 当且仅当 s|n ,且此时 (n1)/s=n/s1

于是有 :

ΔD(n)=s|nH(s)

H(s) 原本的递推式结合,即可得到 ΔD(n)=2H(n)

但有一个例外,当 n=1 时是 ΔD(1)=H(1)=1

于是瓶颈就仅仅在于求出 H(1n) 。复杂度 O(nlogn)。期望得分 100

本题 std 代码量不足 500 B ,是一道合格的小清新数论题!(大雾)

算法四

这是题目原本的标准做法,由于觉得不太适合 A 题就削弱了。

考虑如何在线性时间内求出 H(1n)

根据递推式观察 H(n) 的组合意义。

根据唯一分解,以各个指数为基,将数 n=p1c1p2c2...pmcm 看做高维空间的一个点 (c1,c2,...cm) (坐标中的大量 0 未写出)

这个点每一步可以走向维度坐标只减不增的区域,求走到原点的方案数。

不难发现,若两个坐标将维度任意排序后相同,则 H 函数的值也是相同的(不同维度的地位没有区别)。为了方便,我们约定坐标按照基底素数从小到大排序,并去掉 0

在这种定义下,在 2×107 中,仅仅只有 4223 种不同的坐标。对每种坐标进行 DP 求出 H 函数值的复杂度可以忽略。

然后我们将所有坐标(以及对应函数值)插入字典树,便于检索。

接下来的任务就是对每个 m[1,n] 求出其质因数分解坐标,并取出对应函数值。

筛出所有质数后,可以利用「树形筛」来得到每个数的坐标。(这个命名是临时的)

该筛法的思想是,从小到大考虑各个质因子,以及其指数(即逐位确定坐标)。配合字典树,可以快速找到对应的函数值。

具体流程可见 评测记录dfs4 部分。

可以发现,该筛法恰会访问 1n 中的每个数各一次,且每个搜索树节点都不会在不合法的分支尝试中浪费超过 O(1) 的时间。因此,该筛法的复杂度即为 O(n)

跳蚤猜密码

Idea from rushcheyo, data, std, solution from mayaohua

算法一

n=1 的做法样例解释告诉你了。

对于 n=2 的数据,设矩阵 A=(abcd),那么询问 B=02×2 可以得到 det(A)=adbc,再询问 B=e1,1 可以得到 (a+1)dbc,做差即可得到 d,同理询问其它 ei,j 可以问出所有的 a,b,c,d

这样至多花费 5 次询问,可以通过子任务 1,期望得分 14 分。

算法二

在算法一的基础上,我们考虑减少一次询问,注意到我们如果知道了 a,b,cdet(A)=adbc,那么若 a0 我们可以唯一确定 d

这样在大部分情况下我们可以用 4 次询问通过 n=2 的数据,但是可能会遇到除 0 的情况,一个简单做法是我们初始时随机一个矩阵 C,每次询问的时候都把 B 加上 C,这样解出来的是 A+C,减回去即可。这样既可以大概率避免出现除 0 的窘境了,但是数据造水了,并没有造 0 的数据。

这样至多花费 4 次询问,可以通过子任务 12,期望得分 20 分。

算法三

对于 n20,T=8000 的数据,我们希望得到一个询问次数 n3 的算法。

考虑逐位确定。不妨设我们要确定 A1,1,我们考虑取矩阵函数 Bx=x(Ine1,1),这样 det(A+Bx) 就是一个关于 x 的不超过 n1 次多项式,且 n1 次项系数正好为 A1,1,那我们取 nx 插值即可。同理可以取置换,用类似方法来求出所有的 Ai,j

这样询问次数为 n3,可以通过子任务 3,期望得分 24 分,结合算法二期望得分 44 分。

算法四

考虑得到更优秀的算法。

我们同样先问 B=0n×n 来得到 det(A),随后问所有的 B=ei,j 来得到 det(A+ei,j)=det(A)+Mi,j,其中 Mi,j(i,j) 位置代数余子式,即 (1)i+j 乘上 A 去掉 i 行和 j 列剩余的子矩阵 Ai,j的行列式,于是我们可以得到所有的 Mi,j

线性代数中有一个著名的定理,对于可逆矩阵 AA=1det(A)A1。这里 AA 的伴随矩阵,有 (A)i,j=Mj,i。那么假定 A 可逆,我们知道了 det(A) 和所有的 Mi,j,就能推出 A1 了,再做个矩阵求逆就可以得到 A 了。

但这里 A 不一定可逆,我们可以用类似算法二的策略,预先随机一个矩阵 C 加上去,最后再减回来。这样的效果相当于随机了一个 Fp 下的 n 阶矩阵,它行列式不为 0 的概率(正确率)是 (1p(n1))(1p(n2))(1p1)1p1p2=11p1 这样询问次数为 n2+1,可以通过子任务 1,3,4,期望得分 65 分,结合算法二期望得分 71 分。

算法五

对于最后一个子任务,限制询问次数为 n2,还需要进一步优化。

考虑用算法二的思想来优化算法四。我们尝试不问 e1,1,假定 A 可逆,这样可以得到所有的 Mi,j(i,j)(1,1))与 det(A)。由于 det(A)=det(A)n1,若 AM1,1 不是 0 我们可以同样解出 M1,1A 的),便能节约一次。

由于 A=1det(A)A1,上面条件相当于 A1M1,1 不是 0,由 union bound(也就是 P(AB)P(A)+P(B) 知道错误率不超过 2p1

这样询问次数为 n2,可以通过子任务 15,期望得分 100 分。

霸占排行榜

Idea, data, std, solution from zx2003 and Isonan

简要题意

给出一个 trie​ 树以及 T=Sa1+Sa2++Sak,对于 trie 树上的每个节点,求出该点表示的串 uiT 中的出现次数。

记号约定

match(s) 表示 s 在 trie 树上能够匹配的最长后缀对应的节点。

exi(s) 表示 trie 树上存在一个节点 x 使得 ux=s

depx 表示点 x 在 trie 树上的深度。

anca,b 满足 anca,b 是 a 在 fail 树上的祖先,且 depanca,b=b

算法一

对原 trie 树建立 AC 自动机,把串T在上面走一遍,最后做一遍 fail 树上前缀和即可。

注意由于字符集过大,而且 trie 树的所有叶子深度和没有保证,故需要使用可持久化数组优化建立 AC 自动机的过程。

复杂度 O(llogw+mSlogw),可以通过 subtask 1,期望得分 7。

算法二

考虑优化串 T 在 AC 自动机上行走的过程。

假设我们已经求出了 endi,j 表示 match(uj+Si[1leni,j]),其中 leni,j=max{x|exi(uj+Si[1x])}

之后我们尝试求出 diri,j=match(uj+Si)

由于 endi,j 已知,所以我们可以知道从点 j 出发沿 Si 在 AC 自动机上行走时,第一次跳 fail 边后到达的节点 k=fail(endi,j)

如果 depkdependi,jdepj,则意味着 udiri,j 完全是 Si 的后缀,diri,j=dir1,j,注意本题中1号点为根节点。

否则我们可以从 k=anck,depk(dependi,jdepj) 处继承答案。

注意由 AC 自动机的性质,k 一定是 j 在 fail 树上的祖先。于是整个算法流程可以在 fail 树上递推计算,DFS 的时候可以开个桶存储在当前 DFS 栈中深度为定值的节点,以便快速查询 anca,b

注意特判不需要跳 fail 边的情况,此时有 exi(uj+Si),故 diri,j=endi,j

那么对于每个 i,我们可以得到 match(Sa1++Sai)

这样问题进一步被转化为:

对于一个串 Si,已知每次开始加入该串时在哪个节点,求每个节点在所有加入该串的过程中被经过多少次。

那么观察从 x 开始的一次加入,我们首先会走到 endi,x,然后跳到 failendi,x

这里我们仍然沿用求 diri,j 时的分类讨论模式。

如果 depy|Si| ,则 k 使得 uy=match(Si[1k]),我们只需对所有 kk ,将 match(Si[1k]) 加上相应的值即可,这可以通过一个后缀和来解决。

否则,接下来我们会走到 endy,这里 y=ancfailendi,x,depfailendi,x(depxdepi,x)

容易发现 yx 的后缀,亦即 x 在 fail 树上的祖先。于是我们可以按长度从大到小枚举 trie​ 树上的节点,并在 trie 树上差分得到答案。

于是唯一问题变为 endi,j 的求法,如果在 trie 树上暴力匹配,并且计算 match(Si[1k]) 时暴力跳 fail 边且使用哈希表存边来规避可持久化数组,则总复杂度为 O(llogw+nl2+S+m),可以通过 subtask 1, 3,期望得分 24。

算法三

对于 subtask 2,我们可以使用 exKMP​ 来优化求 endi,j 的过程。

复杂度为 O(nl+S+m),可以通过 subtask 1, 2, 3,期望得分 38。

算法四

继续优化求解 endi, 的过程。

每次计算 endi, 时,首先通过每条重链上 exKMP 求出每个点在该点所在重链上最多走到哪里。

这样问题形式就转为有若干形如 (a,b) 的询问 ,表示在点 a 的轻子树中查询后缀 b 的匹配情况。

对于所有的 i ,总共有 nl 个这样的询问。我们将所有询问离线处理,对于每个a,按照后缀 b 从小到大处理询问。然后类似于求虚树的过程,对于当前询问,求出 trie 上的匹配点 x;转到下一个询问的时候,x 不断跳父亲直到 ux 成为后缀 b 的前缀,这里可以使用哈希来进行判定;然后再沿树边往下走。由于每个点上被访问的出边字符是单调递增的,所以可以用一个指针代替可持久化数组。

关于对所有串的所有后缀进行排序的过程,由于字符集的关系,如果使用其它后缀数据结构会极大地降低运行效率,所以 std 使用了 SA-IS。

最后总复杂度 O(llogl+llogw+nl+S+m),可以通过 subtask 1, 2, 3, 4, 5,期望得分 100。

算法五

from mayaohua, Itst

仍然考虑优化求解 endi,,但是这次我们转换思路,转为计算 trie 上每个节点对于祖先的 endi, 的贡献。

具体的,对于每个 i,我们在 trie 上 DFS 的同时,对于当前点 x 维护出 ux 的最长后缀 p,满足其也是 si 的前缀。

则从 x 往下走的时候,对于 p 的一个长为 j 的 border,如果 x 往下走的对应边没有字符 Si[j] ,则 x 在 trie 上的 j 级祖先 y,其 endi,y 就等于 x;并且所有的 endi, 都一定能被这样正确计算。

于是唯一的问题就是找到这些 border。

我们先对 si 进行预处理,对于每个前缀 i,求出 lsti 表示 i 的最长 border 使得其后继字符和 i 相等;再对于每个字符 c,使用可持久化数组存储后继字符为 c 的最长 border。由于前缀 i 的 border 构成 O(logi) 段等差数列,于是数组中非 0 位置的数量也只有 O(logi) 个,暴力存储所有非 0 位置,即可以 O(SlogS) 的复杂度完成预处理。如果你使用可持久化平衡树来存储非 0 位置,则可以做到 O(SloglogS) 的复杂度。

之后在 trie 上 DFS 的时候,我们将点 x 匹配到的串 p ,遍历一遍存储了其所有 border 后继字符的数组。对于每个字符 c,如果有 x 的出边等于 c ,则这部分复杂度不超过 trie 的边数亦即 O(l);否则一定至少有一个点 yendi,y 被算了出来,这部分复杂度仍为 O(l)

最后总复杂度 O(llogw+nl+SloglogS+m),期望得分 100。

如果可持久化数组实现不当,可能无法通过 subtask 5,只能得到 67 分。

全是 AI 的比赛

Idea, data, std, solution from mayaohua

首先向大家表示抱歉,由于出题人没有出提交答案题的经验,每个测试点没有设计多档部分分,区分度不太足。捂脸熊

原本的想法是出一个对着 AI 攻击的游戏题,但由于出题人能力所限,最后的成品和预期差距比较大,测试点的设计可能不够有趣,但还是希望大家玩得开心。

测试点 1

所有的 AI 都会定向攻击你,且你很难通过给它们加能量来改变它们的目标(即使改变了也不优秀,因为一旦攻击就会吸引仇恨)。因此你的选择只能是逐个击杀并尽量最小化击杀过程中承受的伤害。

注意到所有 AI 都是 100 级,只有体力的区别。显然最优的策略是按体力从小到大击杀,直接排序后模拟一下过程即可。

测试点 2

初始时所有 AI 都不会攻击你,且 299 号 AI 都永远不会攻击你,但只要你攻击 100 号 AI,立刻会遭到反击从而失败。于是你的策略只能是一击秒杀掉 100 号 AI。

但你的等级是不够高的,于是你可以依靠 299 号 AI 给你反复加能量来提升一次攻击的伤害。但初始时你的威胁度比较高,它们都不会给你加能量,于是你需要通过先给它们加能量来降低威胁度。可以用相邻交换证明最优策略是按 aici,1 从大到小排序后依次把 ci,1 降到 0,模拟完这个过程后只需要挂机直到可以一击击杀 100 号 AI,最后把其它 AI 依次击杀即可(此时你的等级已经足够秒杀所有 AI 了)。

这个测试点把上述的算法卡满了,合法解恰好要 10000 轮。

测试点 3

299 号 AI 同样永远不会攻击你,但 100 号 AI 会一直攻击你且不能改变目标。因此你的目标就是在尽量少的轮数内击杀 100 号 AI,然后再依次击杀掉剩下的 AI。

你的最优策略显然是在 299 号 AI 中选择若干个,并按某种顺序依次击杀。如果我们已经找到了一个排列,就可以通过 DP 或者直接枚举前缀等方法来找到优秀的解。标程做法是先按 aibi 从大到小排序得到初始排列,再通过爬山/退火随机交换调整来找到较好的排列。

测试点 4

这是一个精心构造的测试点。

初始时所有 AI 都会攻击你,但对每个 AI 你都可以通过一次给它加能量的操作来转移它的攻击目标。更加仔细的观察会发现所有 AI 的第二目标正好是两两配对的,且对每个 AI 的攻击会拉极大的仇恨,因此对于一对 AI,你只需要给编号小的那个加能量,就可以让它们停止攻击你并互相攻击了。显然你应该按每对 AI 的 a 之和从大到小依次执行这个过程,在 49 轮后就没有 AI 攻击你了。

接下来每对 AI 会反复互相攻击,并在 10009000 轮中的某个轮数后其中一个 AI 死亡,模拟这个过程可以发现在一对中的一个 AI 死亡时,另一个 AI 的体力也不会非常大(大概在 106 级别),如果你在之前已经对这个存活下来的 AI 进行了适当的攻击(可以通过给另一个 AI 加能量来避免吸引仇恨),使得它剩余的体力在斩杀线内,就可以在下一轮直接击杀掉它使得你不会被它攻击。于是你可以大胆猜测一定有一个解能使你在这个过程中从不会被攻击,这一定是最优解!事实上你只需要按这个死亡轮数排序,然后直接依次执行进行攻击来调整,由于你后面的等级会快速提升,你每次调整所需的轮数会越来越少,实践证明确实是可行的。

测试点 5

所有 AI 会互相攻击直到仅存活下一个,且你的威胁度十分低,以至于不论如何攻击,都仅会在剩余一个 AI 后被攻击。那么要想不受伤害,你必须在剩余一个 AI 的下一轮就把它击杀。

一个想法是类似测试点 4 那样,在限定轮数内对最后存活下来的 AI 进行适当攻击,保证你最后能直接斩杀掉。但是由于你的初始等级太低,直接做是做不到的。于是你可以考虑选择一个中间的 AI,先通过同样的调整击杀掉它来增加自己的等级,再对最后存活下来的 AI 进行攻击。这个测试点设计的十分松,通过简单的枚举即可找到这个中间的 AI。

哈希杀手

Idea, data, std, solution from EntropyIncreaser

题意:给定一个 n1 次多项式在 1,q,,qn1 处的点值,要求还原出 m 次项系数。点值只有 k 个位置不为 0

算法一

使用 Θ(n2) 的拉格朗日插值算法。期望得分 5

算法二

子任务 7 满足 qn=1。我们熟悉这一情况就是 DFT。此时我们知道

[xm]f(x)=1nj=0n1qmjf(qj)

可以在 Θ(klogn) 时间内计算。期望得分 10

算法三

我们考虑优化拉格朗日插值算法在这一特定问题上的做法,先按照定义写出插得的多项式的表达式:

f(x)=i=0n1f(qi)j=0n1(xqj)(xqi)0jn1,ji(qiqj)

我们先翻转系数,写作

f(x)=j=0n1(1qjx)i=0n1f(qi)0jn1,ji(qiqj)1(1qix)

其中 0jn1,ji(qiqj) 展开后就可以 Θ(n) 计算所有,我们可以认为核心问题就是计算

i=0n1fi1qix=i=0n1jfiqijxj=i=0n1j(fiq(i2))(xjq(j2))q(i+j2)

其实也就是 Chirp Z-变换的转置。可以在 Θ(nlogn) 时间内解决,预计得分 25

算法四

为了解决更大的 n,我们肯定要考虑只算某一项系数有什么特点了。

对于每个 0in1,我们需要求出 [xk]ji(1qjx)。设 P=i=0n1(1qix),S=i=0yi1qix,我们设 F(x,y)=PS,那么带入 q 可得到方程。

F(qx,y)=(i=1n(1qix))(i=0yi1qi+1x)=(1qnx1xP)y1(S(1x)1)(1)=y11qnx1x(F(x,y)(1x)1P)F(x,qy)=P(i=0yiqi1qix)F(x,y)xF(x,qy)=P11yxF(x,qy)=F(x,y)P11y(2)xF(x,y)=F(x,q1y)P11q1y

我们的目的是对所有 i 提取 [xmyi],因此我们得到一个仅沿着 y 的递推式。接下来,我们将式 1 带入式 2。

y(1x)F(qx,y)=(1qnx)(F(x,y)(1x)1P)yF(qx,y)q1y(qx)F(qx,y)=(F(x,y)1qnx1xPqnxF(x,y))yF(qx,y)q1y(F(qx,q1y)P(qx)11q1y)=F(x,y)1qnx1xPqn(F(x,q1y)P11q1y)

fi=[xmyi]F(x,y),那么两边提取系数 [xmyi],设 i1,便有

qmfi1qmi(fi1[xm]P)=fiqni(fi[xm]P)(1qni)fi=(qmqmi)fi1+(qmiqni)[xm]P

便可逐此递推,而且 1qni0 已经蕴含于 qi 互不相同这一题设中。

我们还需要求出 [xm]P,这可以如法炮制:

P(qx)=1qnx1xP(x)(1x)P(qx)=(1qnx)P(x)qipiqi1pi1=piqnpi1pi=qi1qnqi1pi1

时间复杂度 Θ(n),预计得分 50

算法五

为了最后的子任务,我们需要在 o(n) 时间内计算某一项 i[xmyi]F(x,y)0jn1,ji(qiqj) 以及计算 [xm]P

这种递推式并非常系数,它和我们曾经遇到的整式递推更为相似,递推系数是一个关于 (q,qi) 的有理分式。

我们首先来看 0jn1,ji(qiqj) 的多次询问如何解决。其可以拆解为计算

q(n1)i0jn1,ji(1qji)=(1)iq(n1)i(j=1ni1(1qj))(j=1i(1qj))=(1)iq(n1)i(j=1ni1(1qj))qi(i+1)2(j=1i(1qj))

因此问题归结于计算 1qj 的连乘积。我们设块大小 bn,考虑计算

B(x)=j=1b(1qjx)1,qb,q2b, 处的点值。我们可以通过前面所说的递推方法得到 B(x) 的系数,然后通过 Chirp Z-变换计算出这些点值。复杂度为 O(nblogn)。进行一次询问的时候,我们只需要再加上剩下的零散部分,复杂度 Θ(b)。当询问次数 k 较小的时候,只能取 b=n,复杂度为 Θ(nlogn+kn)。较大时解得 b=nlognk,复杂度 Θ(knlogn)

其他几个递推式可以使用类似手段处理,只需将式子通分。不过在数据范围内一个易于实现的方法是令 b=min(n,500),这样的话 Θ(b2) 算出各部分多项式系数,然后 Θ((n/b)logn+kb) 也可以接受。

预计得分 100

当然,这并非最优理论复杂度,我们可以先得到 n 个点值之后,每个询问距离最近的点只有 n 距离,可以将剩下的跳跃过程二进制分解,每轮进行多点求值。这样就能将复杂度降到 O~(n+k),但是由于所含的 log 因子可能较多,在目前的数据范围下可能不会有很好的表现。

晚了一步……

我们算法五的步骤,在 2020 年被学术界捷足先登了:

  • Fast Computation of the N-th Term of a q-Holonomic Sequence and Applications, Alin Bostan, Sergey Yuekevich. (arxiv)

Picks loves segment tree IX

Idea, data, std, solution from Itst

jiry_2Picks 哥哥不会骂我吧 QAQ

V=232 表示值域。

算法一

我会暴力!模拟题目过程,复杂度 O(nq),可以通过子任务 1 获得 2 分。

算法二

我会找子任务!发现子任务 5 没有 + 运算,那么每一个二进制位是独立的。对于每一个二进制位每个操作就只有变成 1、变成 0、取反、不变这四种可能。

可以使用一些数据结构完成,为了给后面的算法进行铺垫,这里给出 O(nlogV) 预处理 O(1) 查询的方法:

维护每个操作前所有异或操作的 x 的异或和,并计算 prex,y 表示第 x 个操作之前(不包含 x)的所有操作中对第 y 位有赋值影响(即变 1 或变 0)的操作中编号最大的一个操作,不存在记为 0。这显然可以 O(nlogV) 预处理。

对区间 [l,r] 查询第 k 位取值时,考察 prer+1,kl 的大小关系。若 lprer+1,k 则其受到了赋值操作影响,最后一个赋值操作是 prer+1,k,故询问 prer+1,kr 中所有异或操作的异或和结合 prer+1,k 个操作的操作类型即可得到当前位答案;否则操作只有异或,直接计算 lr 所有异或操作的异或和即可。

复杂度 O(nlogV+q),可以通过子任务 5,结合算法一可获得 8 分。

算法三

我参加了联合省选 2020!(?)

对于子任务二,只有异或操作和加操作且允许离线。考虑将所有询问离线,沿着操作序列扫描线并维护一个 Trie,Trie 中包含了所有会进行这一次操作的询问。也就是说对于一个询问 (v,l,r,L),在操作序列扫到第 l 个元素时将 v 插入,即将扫到第 r+1 个元素时在 Trie 上找到之前插入的 v 现在变成了什么值,取二进制第 L 位即可。找到插入的位置变成了什么值,可以考虑在插入时记录 v 所对应的叶子节点编号,需要查询时从该节点开始不断跳父亲求得答案。

那么这个 Trie 的每个节点不仅需要维护儿子,还需要维护父亲。同时需要支持全局异或和全局 +1。联合省选 2020 中就需要运用这样的 Trie 结构:按照位数从低到高插入每个数,异或操作体现为交换表示某些二进制位的节点的儿子,可以打标记实现;而 +1 操作只需要首先交换根节点的儿子表示最低位的变化,然后递归进入 0 表示的儿子继续做以处理进位即可。这只会递归 logV 次。

最终时间复杂度 O((n+q)logV) 空间复杂度 O(qlogV),可以通过子任务 2,结合算法一、二可获得 16 分。

算法四

我会树合并!

对于子任务 6、7 考虑沿用算法三的做法。这需要我们支持在 Trie 上进行与和或操作。这个的处理实际上是较为容易的。

仍然在每个节点上打标记,标记为有哪一些二进制位被赋为 0、有哪一些二进制位被赋为 1、有哪一些二进制位被翻转。下传标记时,若当前位被置 0 或者置 1,则其两个儿子会合并到一起。通过 Trie 的树合并即可实现这一点。需要注意的是树合并时需要下传标记,而下传标记也需要合并,故需要一些特殊的实现。

这样的树合并的复杂度分析与树合并一致,每一次合并需要 O(1) 的复杂度减少 Trie 上的一个节点,故树合并复杂度为 O(qlogV)

同时注意到节点可能会合并,故与算法三不同地,在 r+1 位置查询答案时 l 位置插入的节点可能由于合并到其他节点上被删除。此时可以在外部对这 q 个节点维护一个并查集记录每个节点的合并状态以找到真正的节点。

总时间复杂度 O((n+q)logV) 空间复杂度 O(qlogV),常数较大。可以通过子任务 2、6、7,结合算法一、二可获得 36 分。

算法五

from mayaohua

我会线段树!(总算是有一个和线段树有关系的 Sub 了)

考虑对原序列建立线段树,那么在每个节点需要知道一个任意值经过这个区间的操作后变为什么值。考虑沿用算法三提到的 Trie 结构。由于无法提前知道询问,需要对所有值都给出答案,但建立一个满 Trie 是不现实的。考虑进行一些优化。

注意到操作涉及的节点数量非常有限,考虑只把有意义的节点建立出来。这里我们认为无意义的节点指的是在 Trie 上没有做过任何操作(与兄弟交换或者被传标记)的节点。每次异或时在根节点上打上标记。+1 操作与原来一致,但可能在路径中新建额外的节点。但新建节点数量不会超过 2logV,故总时空复杂度是 O(nlognlogV) 的。

维护每个节点所表示的初始值域区间,每一次查询找到其所在的初始值域区间对应的节点,则所有 Trie 上的操作没有影响到无效节点对应的若干位。最后跳父亲还原答案,记得把这些节点上剩余的标记进行处理。

时间复杂度 O((n+q)lognlogV) 空间复杂度 O(nlognlogV),可通过 Subtask 2、3,结合算法一、二、四可获得 50 分。

算法六

from mayaohua

我会分析操作性质!

注意到如果所有询问的 l 都是 1,则可以利用算法 5 中的节点内 Trie 树结构,对整个序列建立一个可持久化的这样的结构,即可在 O(nlogV) 的空间内进行前缀询问查询。

对于子任务 4 考虑分析没有与和或操作的性质。一个不难注意到的性质是:给定一个只有 +1 和异或的操作序列,设集合 S={0,1,,V1},考虑映射 f:SS,其中 f(x) 表示的是 x 经过操作序列后变成的数,则 f 是一个一一映射。

利用这个性质,设 fl,r(x) 表示做序列 lr 的操作对应的映射,则求询问 v,l,r,L 的结果可以通过 f1,r(f1,l11(v)) 来得到,也就拆成了两个前缀询问。

f1,r(x) 的询问使用之前的可持久化结构进行计算,而 f1,l11(x) 的询问实际上是更加简单的:在 f1,l1(x) 对应的数据结构中沿着 x 走,通过其最后到达的有效节点的信息和经过的标记即可得到 f1,l11(x) 的值。

时间复杂度 O((n+q)logn) 空间复杂度 O(nlogn),可以通过 Subtask 2、3、4,结合算法一、二、四获得 66 分。

算法七

考虑将算法四和算法五结合起来,也就是将算法五中每个节点的数据结构更换为算法四中的树合并 Trie。想法是很简单的,但是操作起来可能有不小的困难。

第一点是节点的合并可能会导致两个区间变为同一个区间,不过它们的区间长度会是相同的所以只需要添加一些额外的结构就可以描述这样的关系:对于同一个长度的所有区间维护一个并查集表示区间之间的合并关系,再对每个被分裂区间维护其分裂之后产生的两个区间。这样可以在 O(logVα(n)) 的复杂度内查询每个值对应的区间。并查集的节点可以动态开保证空间使用不超过 O(nlognlogV)

第二点也是最重要的问题,空间常数过大。线段树内每个节点需要记录至少 8 字节标记(因为每一位有四个状态可以用两个 bit 压掉),还有分裂产生的并查集上的记录,这将导致空间常数的大量增加,很有可能无法在 512MB 的空间限制下通过本题。

当然如果你用这个算法通过了,那你真的很厉害!同时存在其他做法也可以在博客评论区进行交流。可以通过 Subtask 2、3、6、8,结合算法一、二、六获得 80 分。

算法八

我不会 Trie!也不会线段树!能不能做这题呢?能!

一个很朴素的想法是把 +1 变成 bitxor 1 去维护,问题出在进位。那么我们可以考虑什么时候某一个 +1 操作会影响到正在询问的第 L 位呢?显然地,这要求底下的所有位全部进位了,也就是当且仅当这个操作过后第 0L1 个二进制位全部都是 0

这可以给我们启发。每个位置末尾 0 的个数是一个很重要的状态,这决定了 +1 操作是否会给当前的二进制位进行翻转。因此此时可以考虑找到运行操作的过程中所有末尾有 L10 的时刻,把这些位置的 +1 操作换成对当前二进制位的翻转操作,其分割的区间则可以通过算法二的数据结构进行查询。

第一个问题是如何找到这样的状态。考虑不断迭代,把末尾 0 的个数变多。具体设计两个递推数组:

  • fi,x:给出一个末尾至少i0 的数(显然高位不会产生影响),从第 x 个操作开始进行操作,则做完第 fi,x1 个操作后其末尾第一次又有至少 i0。若不存在取 fi,x=
  • gi,x:给出一个末尾恰有 i0(也就是第 i 位为 1)的数,从第 x 个操作开始进行操作,则做完第 gi,x1 个操作后其末尾第一次又有至少 i+10。若不存在取 gi,x=

f0,g0 都是容易推出的,而 fi,x 可以通过 fi1,x 推出:若 fi1,x,考察第 i1 位从第 x 个操作做到第 fi1,x1 个操作的结果,特别需要注意的是如果第 fi1,x1 个操作是 +1 要把它变成翻转操作(因为低位都是 0),此时如果答案是 0fi,x=fi1,x,否则现在末尾就是恰好 i10,有 fi,x=gi1,fi1,xgi,x 则可以从 fi,x 类似推出。

给出询问 v,l,r,L,若 v 末尾恰好有 p0p<L,则尝试将 l 移动到 gp,l 处增加 0 的个数。如果 gp,l>r+1 意味着整个过程中末尾 0 的个数不足以用 +1 操作影响第 L 位,此时只需要用算法二的数据结构查询整段的位运算操作产生的影响。否则移动并用算法二的数据结构判断新的 p 是多少。

如果某个时刻 pL 了,不妨假设此时左端点移动到了 l,则先查询 v 的第 L 位在 [l,l1] 区间的结果,再从 l 开始查询。注意到不断让 l 变成 fL,,l 就可以遍历低 L 位为 0 的状态,此时有一个暴力为直接暴跳并计算直到即将跳出 r+1,最后再把末尾一段计算进去即可。但这样显然复杂度有问题。

还可以注意到的一点是 f 数组是静态结构,考虑建立 logV 棵树,第 i 棵树上 jfi,j 连边。此时查询操作就相当与在某棵树上跳祖先并维护信息。对每棵树都树链剖分并维护一些重链上信息就可以做到 O(logn) 查询了,这部分比较简单不作赘述。

总时间复杂度 O(q(logV+logn)+nlogV),空间复杂度 O(nlogV),可以通过所有子任务,获得 100 分!

有一个较为显著的常数优化是把 fg 的状态设计为要求 fi,x1gi,x1 位置都是 +1 操作,因为其他情况没有贡献,这样树剖部分常数会好一些。但不加这个常数优化也可以较快通过该题。

拓展算法——算法九

from zx2003

我们显然可以将算法八进行拓展得到 O(nlogV) 时空预处理 O(lognlogV) 单次查询的算法解决计算最终答案值是多少而不只是单个二进制位的问题。但我们还可以做得更优。

还是沿用算法八的 f,g 数组以及不断增加末尾 0 的个数的过程。不妨假设增加到末尾 p0 之后无法增加,则 >p+1 的位使用算法二数据结构,第 p+1 位使用 f 数组的树链剖分,而注意到此时 p 位的答案可以通过 v=0 的一次询问得到。

故只需要额外增加 v=0 的询问的在线查询即可。这只需要对算法四进行简单的修改:不提供删除操作并进行可持久化即可。对于叶子合并的问题,可以对合并过程构建出一棵有根森林,每条边由被合并的点指向合并到的点,边权为合并时间。之后每次查询在该森林上树链剖分或倍增跳即可找到该版本下其合并到的节点,然后在对应版本的 Trie 上跳父亲。

这样的预处理时空复杂度 O(nlogV) 单次查询复杂度 O(logV+logn)。但是其也有一个致命的问题:可持久化、记父亲、好几个标记的 Trie 的空间占用相当的大,同时其他部分也有一定空间占用,很难以比较优美的空间常数通过。有相关想法或实现的选手可以与出题人或 zx2003 哥哥交流。

评论

不会
评论回复
Itst:谢谢 picks 哥哥!
T2中线性代数的式子是不是 A*=det(A)×A^{-1}? @rushcheyo

发表评论

可以用@mike来提到mike这个用户,mike会被高亮显示。如果你真的想打“@”这个字符,请用“@@”。