メモリー
发现有时候确实需要写一下这种东西,不然太容易忘了。
杜教筛
求积性前缀和,即 S(n)=∑ni=1f(i)S(n)=∑ni=1f(i).
某些不是积性的函数也可以,只要能找到一个合适的 gg。
对于任意两个数论函数 f,gf,g,有∑ni=1(f∗g)(i)=∑ni=1g(i)S(⌊ni⌋)∑ni=1(f∗g)(i)=∑ni=1g(i)S(⌊ni⌋).
如果可以找到一个 g(n)g(n) 可以快速地求出 f∗gf∗g 的前缀和及 gg 的前缀和,则 g(1)S(n)=∑ni=1(f∗g)(i)−∑ni=2g(i)S(⌊ni⌋)g(1)S(n)=∑ni=1(f∗g)(i)−∑ni=2g(i)S(⌊ni⌋)。
后半部分可以递归地上数论分块,求得的若干 S(n)S(n) 需要记忆化来保证复杂度。
总复杂度 O(n3/4)O(n3/4),若预处理前 O(n2/3)O(n2/3)项,则复杂度降至 O(n2/3)O(n2/3)。
虚树
将原树上的关键点和其LCA抽出来独立建一棵树。
先求得原树dfn序,并将所有关键点按照dfn排序。求得相邻两项的LCA。将LCA和关键点一起再次排序以后,枚举相邻两项,连 LCA(pi,pi+1)→pi+1LCA(pi,pi+1)→pi+1。证明可以感性理解。O(nlogn)O(nlogn)。
跳跳棋
关键性质:跳动时只能恰好越过一个棋子。
设三个棋子位置为 a,b,c,d1=b−a,d2=c−ba,b,c,d1=b−a,d2=c−b。观察“往里跳”的操作。
如果 d1<d2d1<d2,只能挪动 aa.
如果 d1>d2d1>d2,只能挪动 cc.
如果 d1=d2d1=d2,你就寄了,动不了。
如果把一种状态作为一个节点,那么往里跳的方案数最多有一种情况,往外跳的一定有两种情况,可以依此建出二叉树森林。
如果你真建出来了森林,判断能否到达目标位置和行进步数就是简单的了。可惜你建不出来。状态太多了。
但是发现如果 d1d1 和 d2d2 的其中一个比另一个小特别多,可能会从一个方向往里面跳很多次,直到另一个比自己还小。这是辗转相除,所以这个过程只会有 logVlogV 次。
依此可以容易地求出一个点的深度(到根所需要的步数),和其 kk 级祖先。有了这两个就可以随便做了。
多项式乘法
FFT
大家伙。
计算两个次数为 nn 的多项式 F(x),G(x)F(x),G(x) 的乘积。
系数表示法计算乘积是 O(n2)O(n2) 的,而点值表示法仅需要 O(n)O(n)。尝试把两个多项式转化为点值表示,相乘再转化回去。
由拉格朗日插值法,nn 次多项式需要 n+1n+1 个点来唯一确定。在低于 O(n2)O(n2) 的复杂度下对于 n+1n+1 个点计算任意一个多项式 F(x)F(x) 看起来也很扯,只能寄希望于这些点满足一些奇妙性质。
以下认为 n+1=2kn+1=2k,如果不是就用 0 补齐高次项。
考虑将 nn 次单位根 1,ωn,ω2n,⋯,ωn−1n1,ωn,ω2n,⋯,ωn−1n 代入 F(x)F(x) 求值。
将 F(x)F(x) 的系数奇偶分类,奇数分给 AA,偶数分给 BB。则 F(x)=A(x2)+xB(x2)F(x)=A(x2)+xB(x2)。注意到:
因为 ωk+n/2n=−ωknωk+n/2n=−ωkn,所以
所以如果分别获得了 A(ωkn/2),B(ωkn/2)A(ωkn/2),B(ωkn/2) 的值,我们就可以一起算出 F(ωkn)F(ωkn) 和 F(ωk+n/2n)F(ωk+n/2n)。而计算 A(ωkn/2),B(ωkn/2)A(ωkn/2),B(ωkn/2) 是一个规模更小的子问题,递归求解是可以的。
这是 O(nlogn)O(nlogn) 的。将系数表示法转化为点值表示法仅需要进行相同的操作并将结果的每一项 ×1n×1n。鬼知道为什么。
浅证一下吧。设 {ai}{ai} 为 FF 各项系数,bi=F(ωin)=∑n−1k=0ak(ωin)kbi=F(ωin)=∑n−1k=0ak(ωin)k,ci=1n∑n−1k=0bk(ω−in)kci=1n∑n−1k=0bk(ω−in)k。那我们要证 ci=aici=ai。直接展开。
关于单位根求和,既可以直接套等比求和,也可以在单位圆上看一眼。都行,无所谓。
具体实现上,会先找到 F(i)F(i) 经过一串区间内奇偶分类的操作后会落在哪里。将初始位置二进制反转后就可以得到最终位置。鬼知道为什么。
源码大概长这个样子。
int rev[N];
void shuffle(cmplx a[],int len){
for(int i=1;i<len;i++){
rev[i]=rev[i>>1]>>1|(i&1?(len>>1):0);
}
for(int i=0;i<len;i++) if(i<rev[i]) swap(a[i],a[rev[i]]);
}
void fast_fast_TLE(cmplx a[],int len,int flag){
shuffle(a,len);
for(int k=2;k<=len;k<<=1){
int wid=k>>1;
cmplx w1={cos(2*pi/k),flag*sin(2*pi/k)};
for(int i=0;i<len;i+=k){
cmplx w={1,0};
for(int j=i;j<i+wid;j++){
cmplx p=a[j],q=a[j+wid]*w;
a[j]=p+q; a[j+wid]=p-q;
w*=w1;
}
}
}
if(flag==-1) for(int i=0;i<len;i++) a[i].real/=len;
}
NTT
考虑我们用到了上述单位根的什么性质。
- ω2k2n=ωknω2k2n=ωkn
- ωk+n2n=−ωk2nωk+n2n=−ωk2n
- (ωkn)2=ω2kn(ωkn)2=ω2kn
我们发现如果设 ωkn=g(P−1)knωkn=g(P−1)kn,在 modPmodP 下好像也满足上述性质。
(一个数 pp 的原根 gg 满足 gkgk 是纯循环(由欧拉定理,gcd(g,p)=1gcd(g,p)=1)且循环节为 φ(k)φ(k)。pp 是质数时即代表 g0,g1,⋯,gP−2g0,g1,⋯,gP−2 遍历 [1,P)[1,P)。)
所以我们将上面的 ωω 全文替换便得到了单位根。
注意并不是所有模数都可以 NTTNTT。求单位根的时候我们将 (P−1)(P−1) 除以了 nn,我们需要让其是整数。模数是 998244353=119×223+1998244353=119×223+1,允许我们递归 2323 层 NTTNTT,即多项式最高不能超过 223223 次。实际使用时不用担心,因为次数高了最先爆炸的是时限而不是正确性。
写出来大概长这个样子:
void faster_faster_TLE(int a[],int len,int flag){
shuffle(a,len);
for(int k=2;k<=len;k<<=1){
int wid=k>>1;
int w1=Pow(G,(flag*(P-1)/k+(P-1))%(P-1));
for(int i=0;i<len;i+=k){
int w=1;
for(int j=i;j<i+wid;j++){
int p=a[j],q=a[j+wid]*w%P;
a[j]=(p+q)%P; a[j+wid]=(p-q+P)%P;
w*=w1; w%=P;
}
}
}
if(flag==-1){
int Ilen=Inv(len);
for(int i=0;i<len;i++) a[i]*=Ilen,a[i]%=P;
}
}
Kingdom Partition
考虑最小割。最小割的本质是求 ∑(u,v,w)∈Ew⋅(xu∧¬xv)∑(u,v,w)∈Ew⋅(xu∧¬xv),但是因为一个点可以有三种状态,显然我们不能用单独一个bool描述一个点。考虑把一个点拆成两个bool Ap,BpAp,Bp 描述。ApAp 表示这个点是不是 AA,BpBp 表示这个点是不是 BB。
对于一条原图上的双向边 p↔qp↔q,考虑如何在它们这四个bool之间连边。需要达到的效果如下:
Ap=1,Bp=0Ap=1,Bp=0 | Ap=0,Bp=1Ap=0,Bp=1 | Ap=0,Bp=0Ap=0,Bp=0 | |
---|---|---|---|
Aq=1,Bq=0Aq=1,Bq=0 | 2w2w | 00 | ww |
Aq=0,Bq=1Aq=0,Bq=1 | 00 | 2w2w | ww |
Aq=0,Bq=0Aq=0,Bq=0 | ww | ww | 00 |
发现连 Ap→Bq,Aq→Bp,Bp→Aq,Bq→ApAp→Bq,Aq→Bp,Bp→Aq,Bq→Ap 即可。(边权均为 ww)。
但是会有问题。可能 Ap=1,Bp=1Ap=1,Bp=1。不慌,看看出现这种情况时所产生的贡献。
Ap=1,Bp=0Ap=1,Bp=0 | Ap=0,Bp=1Ap=0,Bp=1 | Ap=0,Bp=0Ap=0,Bp=0 | Ap=1,Bp=1Ap=1,Bp=1 | |
---|---|---|---|---|
Aq=1,Bq=0Aq=1,Bq=0 | 2w2w | 00 | ww | ww |
Aq=0,Bq=1Aq=0,Bq=1 | 00 | 2w2w | ww | ww |
Aq=0,Bq=0Aq=0,Bq=0 | ww | ww | 00 | 2w2w |
Aq=1,Bq=1Aq=1,Bq=1 | ww | ww | 2w2w | 00 |
注意到这个东西和 Ap=0,Bp=0Ap=0,Bp=0 没有任何区别,除了它连向 Ap=0,Bp=0Ap=0,Bp=0 时代价从 00 变成了 2w2w。也就是说如果将这些点全部替换成 Ap=0,Bp=0Ap=0,Bp=0 一定是不劣的。放心跑就行。
停时
记一个随机现象中所有可能发生的结果(称为样本点)组成的集合为 ΩΩ,记 ωω 是一个随机试验的结果:ωω 是确定取一个 ∈Ω∈Ω 中的元素,但是在正式试验之前我们不知道它具体取什么。
记 X(ω)X(ω) 为一个随机变量。XX 本质上是一个函数,将样本点映射到数值上。这样来说 ωω 就很像一个生成种子。
记一个随机过程为一系列的随机变量的集合(序列?),每个元素对应着一个时刻 tt。比如说像这样:{X0(ω),X1(ω),X2(ω),⋯}{X0(ω),X1(ω),X2(ω),⋯}。注意所有 XX 的取值均依赖于一个 ωω,即一旦 ωω 确定了整个随机过程也定下来了。把 ωω 略掉不写是可以的。
定义鞅为“未来的值的期望等于当前的值”的随机过程。形式化来讲,若随机过程 {Xt}t≥0{Xt}t≥0 对于每一个时刻 tt 满足 E(Xt+1|Ft)=XtE(Xt+1|Ft)=Xt。其中 FtFt 表示截至 tt 时刻所获得的随机过程的信息(?),即 X0,X1,⋯,XtX0,X1,⋯,Xt。(应该是,只确定了这些 XX 的值,而 ωω 仍然不确定?这句话是自己瞎猜的。)
一个鞅的停时 ττ 是一个随机变量,满足 ∀t≥0,{τ=t}∈Ft∀t≥0,{τ=t}∈Ft,即到任意时刻时我都知道当前该不该停下来。
停时定理
叙述如下:
设 {Xt}t≥0{Xt}t≥0 是一个鞅,ττ 是停时,如果满足以下条件之一:
- 【censored】
- 【censored】
- 【censored】
则 E[Xτ]=E[X0]E[Xτ]=E[X0]。
(条件是什么不重要,即使知道也不一定会判断是否满足,直接当成永远成立吧。)
势能函数
目的是求解一个随机过程(应该不需要是鞅)的停时的期望 E(τ)E(τ)。
构造一个势能函数 Φ(X)Φ(X),满足:
- E(Φ(Xt+1)−Φ(Xt)|Fn)=−1E(Φ(Xt+1)−Φ(Xt)|Fn)=−1,
- ∀Xτ∀Xτ,Φ(Xτ)Φ(Xτ) 均相等,
- Φ(Xi)=Φ(Xτ)Φ(Xi)=Φ(Xτ) 当且仅当 i=τi=τ。
人话就是势能函数每次期望 −1−1,停止时到达一个常数。
(其实没有理解为什么有第三条?防止 ii 被误认为是停时?但是不知道为什么这样被误认会有问题(
然后由于期望的线性性,裂项就(应该?)得到了 E(τ)=E(Φ(X0))−Φ(Xτ)E(τ)=E(Φ(X0))−Φ(Xτ)。停时的期望就转化成了势能函数的期望。所以我们需要做的就是确定一个美好的势能函数然后竭尽全力计算其初态的期望。
以下设 ss 为球的总数,cici 为颜色为 ii 的球的数量。
对于当前这道题,SS 表示球的颜色状态,如果让势能函数 Φ(S)=∑f(ci)Φ(S)=∑f(ci),其中 ff 是某个函数, cici 是颜色为 ii 的球的个数,则终止状态的势能函数值自然全相同。
因为期望的可加性,可直接考虑单个 f(c)f(c) 转移一次得到的东西的期望:
分别讨论了用当前颜色覆盖另一颜色的球,用另一颜色覆盖当前颜色的球,两个球都是/都不是当前颜色的情况。
设箭头右边的一坨为 f′(c)f′(c)。然后我们需要让 F(S)F(S) 转移一次期望 −1−1,所以即对于任意 ∑ci=s∑ci=s 的 {c},都有∑(f(ci)−f′(ci))=1。f′(c) 只能为 f(c)−cs。
回到上面超长的柿子,把等式套进去解,得到
设 d(c)=f(c+1)−f(c),则改写成 d(c)=d(c−1)−s−1s−c。
规定 f(0)=0,d(0)=−1,然后快乐地递推就可以得到每一个 f(ci) 了。现在还需要求 f(s)。
做完辣。
还是记 Φ(S)=∑f(si)。设一次操作选择了 x 个颜色 si 的球,y 个颜色不是 si 的球。发生的概率为 (six)(n−siy)2n。将一个被选中的球染成 si 的概率为 x+yn。于是我们可以得到下式:
(我知道你很急但你先别急(
然后因为对于所有 si 的组合都应成立所以 i 的求和可以削去。(并把 −1 变成 ∑ni=1sin 的形式。)
省略下标 i,直接记 si=s,变换然后把混在一起的变量分开一下:
然后你知道 ∑ni=0(ni)=2n,∑ni=0i(ni)=(n−1)2n−1,于是框起来的部分就无了。
具体地,第一行的尾巴是 (n+2x−s)2n−s−1,第二行的尾巴是 (n−2x+s)2n−s−1。
回代整理可得:
然后你要把和式里面的 f(s+1) 单拎出来因为这才是你想求的:(顺便,x′←s−x)
大概是这个东西吧。可能。O(n2)。
PGF
我们设 F(x)=∑cP(X=c)xc,则 F′(1)=∑cP(X=c)⋅c=E(X)。
先规定一些记号:fix(i)=(1n)i, S(i) 表示一个长为 i 的字符串,[border(i)] 指示 A[1:i] 是否是 A 的 border。
fi 表示字符串延长到长度为 i 时结束的概率,gi 表示字符串长度为 i 时仍未结束的概率。F(x)=∑∞i=0fixi,G(x)=∑∞i=0gixi。
对于一个未结束的状态,增加一个字符要么结束要么未结束,即 gi=gi+1+fi+1,写成生成函数形式如下:
然后整点小活。
于是我们只需要求 G(1) 就好了。
对于一个 S(i),如果它还没结束,我们在后面拼上一个 A 便可以强制让它结束。保证拼上一个 A 需要提供 fix(m) 的概率。我们可以写出 fix(m)⋅gi=fi+m。
上面这个东西当然是错的。我们可能刚拼上 A 的一个前缀就凑出了一个 A。这样的前缀也是 A 的后缀,即它是一个 border。
重写上面的式子:fix(m)gi=∑mj=1[border(j)]⋅fix(m−j)fi+j,然后改成生成函数。
看看 G(1) 是什么。
F(1) 即 ∑fi 显然为 1。所以我们就做完了。
二分队列
这玩意浪费了我12h。
考虑设 fT 表示坐 T 号列车刚到终点时的最小花费,将 T 按照发车时间 a 排序,则有:
其中 Query(L,R) 表示 ∑k[[li,ri]⊆(L,R)]。
注意到 min 里面的东西具有决策单调性:将 M 按照下车时间 b 排序,当 T1<T2 时,设 fT1 由 Mx 转移,fT2 由 My 转移,则有 x≤y。
(T.a 从 a1 增加到 a2 的过程中,所有决策点 M 的代价都增加了 priceT.x×Query(a1−1,a2),相对大小不变)
对于每一个决策点,维护其作为最优决策点时 T.a 的取值区间,用队列维护。产生新决策 M′ 时,在最靠后的一个取值区间 [l,r](对应决策 M)上二分,找到满足 f(M′,t)<f(M,t) 的最小的 t,使 M 的决策区间变成 [l,t−1],M′ 的决策区间为 [t,r]。加入队列。
由于询问的 T.a 单增,所以查找最优决策点时,弹出前面所有 r<T.a 的决策点即可。
而 Query 本身的值离散化后用主席树维护即可。
注意事项:
-
因为有 M.y=T.x 的限制,所以需要对每一个岛屿维护一个二分队列。
-
T 需要以 a 单增的顺序转移,而 M 却需要以 b 单增的顺序插入队列。所以不能转移完一个 Ti 就把它扔到队列里。
-
二分队列本身复杂度 O(nlogn),查询 Query 的复杂度 O(logn),总共 O(nlog2n),需要大力卡常。
性质
-
树形dp时,如果一个节点的 f 只有 min(sz,k) 个值是有用的,则 dp 一遍只需要 O(nk) 的复杂度。
-
有时题目要求维护的东西有效期是一个区间,那么可以考虑在时间轴升序的过程中模拟/整活。例えば:CSP-S 2024 T4,this
长链剖分
如果要维护的dp方程为 fv,k←fson,k+1,即信息均以绝对深度为下标,则可以通过长链剖分达到 O(n) 的复杂度。
具体地,每次将深度小的子树向深度大的子树合并。因为一条长链只会在链首完成一次合并,而所有长链的长度和为 n,所以总长就是 O(n)。
2-SAT
考虑将一个变量 p 拆成两个点 p 和 ¬p。一条边 p→q 表示如果 p 成立,q 一定也成立。连了 p→q 一定也要把 ¬q→¬p(逆否命题)连上,虽然我还不知道为什么。
如果 p 可达 ¬q,则 p 只能取 False。如果 ¬p 可达 q,则 p 只能取 True。如果互相可达,没救了。否则都行。
可达性可以缩SCC后看拓扑序。
疯题
求 $$\prod_{i=1}n\prod_{j=1}m\varphi(ij)\pmod {5\times10^7+17}$$
有:
证明:记 d=gcd(a,b)。
于是有:
着重考虑后面的部分。
再考虑指数上那一坨是什么东西。记 n′=⌊np⌋,m′=⌊mp⌋。
于是其实:
前两项预处理 φ(n) 的前缀积,后一项预处理 pp−1 的前缀积然后数论分块即可。O(√nlogn)。
还可以优化掉快速幂的 logn。因为模数足够小,我们可以 O(P) 预处理出 i 和 gi 的双射关系,对于akmodP,查表找到 x 使得 gx≡a(modP),再查表得到 gxkmodφ(P)modP 即可。
然后还可以再优化,虽然效果不明显。考虑一常数 B,我们对所有 p≤B 暴力计算其贡献,对 p>B 的部分数论分块。这是 O(T(π(B)+nB)) 的,题解说 B=√nlnn 比较好,我没理解为什么。
然后你发现你还是TLE了。那就没救了。卡常卡到明天早上吧。我不想卡就放弃了。
最小割树
定义:对原图(限制只能是无向图。。)上的点新建一棵带权树,满足任选树上两个点 s,t,路径 (s,t) 中的最小边权为 s 到 t 的最小割,且切掉这条边后产生的两个连通点集就是最小割的点集。
这个东西有点难建,一般用的是其alternative:等价流树,即忽略掉上文中 “且” 后的要求。
性质:
设 s,t 的最小割把图割成了 L,R 两部分(点和边都算在其中,割边扔掉不管),其中一侧(不妨认为是 L)的两个点 p,q 的最小割的割边集合可以完全包含在 L 之中。
somehow,通过这个和什么其它的东西就可以得到一个算法:
在当前点集内随便选两个点 p,q,在原图里跑最小割 val=Cut(p,q),在最小割树上 Link(p,q,val),对割出的两个点集分别递归跑这个即可。
网络流 补充
对于求 ∑f(di) (f(x) 为下凸函数,di 满足一定限制,由网络流模型保证)一类的问题,可以将每一个 di 向 汇点连一系列的边,第 i 条的流量为 1,代价为 f(di)−f(di−1)。
在跑上下界网络流时,设一条边的流量可以为 [a,b],如果有流量限制的边都没有费用,可以将一条边拆成一条流量为 a,费用为 −MAX 和一条流量为 b−a,费用为 0 的边来规避掉上下界。
p.s. 如果有费用是不是将两条边的费用都加上原费用就行啊。不知道。
最小费用可行流只需要把终止条件从 T 不可达改成到 T 的距离 >0 即可。
线性规划
感觉这个挺大件的,确实不是一天两天就能搞明白的东西(
一个线性规划问题的形式如下:给定 c1,c2,⋯cn,你需要确定一组 x1,x2,⋯xn,使其在满足另外给出的一些限制的前提下,∑nj=1cjxj 最大(或最小)。
每组限制都是如下形式:fi(x1,x2,⋯,xn)=∑nj=1aijxj≤bj。每组的 ≤ 也可以独立地换成 ≥ 或=。
标准形
任意一个线性规划问题都可以转化成下面的模型(标准形):
具体的转化方式是:
- 如果目标为求最小值,将所有 cj 取反求最大。
- 如果限制是 ≥,将两边同乘 −1。
- 如果限制是 = 拆成 fi≥bi、−fi≥−bi 即可。
- 如果某个 xj 没有取值限制,将其替换成两个新变量的差 xp−xq。
标准形也可以如下表示:
单纯形法
对偶问题
考虑另一种刻画目标函数的方法:
我们不求解 →x,而是新构造一组 →y,使 yi 作为约束 i —— ∑nj=1aijxj 的系数,试图用其刻画目标函数的极值。此时,每个 yi 各代表了一个约束。
则我们需要有:
即:
当 yi≥0 时,由于 ∑nj=1aijxj≤bi,目标函数的一个上界即为 ∑mi=1yibi。
惊奇地发现这也是一个线性规划:
也就是:
这两个问题互为对偶问题。(对偶问题再对偶回去是原问题)
HILO
其实挺简单的,但是没做出来,有点破防。
记 fi,j,k 表示数对 j,k 对答案的贡献,限制j 之前 <x 的最大元素为 i。则 k 之前的元素(除去 i,j,k 这三个)一定 ∈[1,i)∪(j,n]。
考虑先在排列里定下来这些元素的位置,即为 An−j+max(i−1,0)n。此后 i,j,k 必然分别占据前三个空位,这是我们不需要管的。
然后 (i,j)/k 这些元素可以随便选剩下的空位排。(j−i−2)!。于是一个 fi,j,k=An−j+max(i−1,0)n×(j−i−2)!。
显然这个 k 是来搞笑的,k∈(i,x] 的答案都一样,缩掉 k,记 fi,j=An−j+max(i−1,0)n(x−i)(j−i−2)!。
考虑特殊处理 i=0 的情况,然后就没有 max 了。展开原式。(认为 (−1)!=0)
后面这一个 ∑ 显然可以 O(1)。所以这是 O(n)。i=0 时也是 O(n)。
二分图相关
最小点覆盖(K¨onig)
结论:从左侧未匹配的节点出发进行dfs,从左往右走非匹配边,从右往左走匹配边。记录经过的点。则最小点覆盖集合为 左侧未经过的节点 和 右侧经过的节点 的并。其大小恰为最大匹配。
先证其大小等于最大匹配:对于一个左侧未经过的节点,一定对应一条匹配边(不然会作为起始点dfs)。对于一个右侧经过的节点,也会对应一条匹配边(否则意味着它是dfs树的叶子,到起始节点的路径是增广路,和最大匹配矛盾)。而显然这两种方式不会映射到同一条匹配边上,所以总节点数就是最大匹配。也可以说明最小性:每条匹配边都至少需要一个点来覆盖,更少一定不行。
再证其是点覆盖:只需证明不存在左侧经过但右侧未经过的边。上一段已证明匹配边都被匹配上了。对于非匹配边,如果左侧经过,dfs的时候一定会通过这条边。
最大独立集
显然独立集的补集是点覆盖,所以最大独立集便是上述点集的补集。
偏序集最长反链(Dilworth)
结论:对于一个点 x 拆 xin,xout,对于一条 p→q 连 pout,qin。满足 xin 和 xout 同在最大独立集中的 {x} 构成最长反链。其大小=原图的最小链覆盖
这次先证其是反链:不用证,都在独立集里了,任意两点之间不会有边。
再证其最长:记匹配边数 =m,最大独立集为 I,A 为构造出的反链。首先有 |I|=2n−m。
考虑 |I|−|A| 的意义是“xin 和 xout 中至少一个 ∈I 的 x 个数”。≤n。 所以 |A|≥n−m。
而 n−m 同时是原图最小链覆盖。而最长反链中不可能有两个点在一个链里,所以 |A|≤n−m。所以 |A|=n−m,而且 A 是最长反链。
SA
绷,半年前学的东西现在忘得一干二净了。
记 sai 表示字符串 S 中排名为 i 的后缀是哪个(记录其起始位置),rki 表示后缀 S[i,n] 的排名。核心思路是,如果我们有 S 所有长度为 w 的子串的大小顺序,通过一次双关键字排序即可得到所有长度为 2w 的子串大小顺序。操作直到 w>=n 即可。这里双关键字的值域是 n 所以可以用计数排序削掉一个 log。
下面逐行分析代码在干什么。(字符串以下标1开头)
int sa[N],rk[2*N],tmp[N],buc[N];
void Sort(int n,int w){
int ptr=0;
for(int i=n;i>n-w;i--) tmp[++ptr]=i;
for(int i=1;i<=n;i++) if(sa[i]-w>0) tmp[++ptr]=sa[i]-w;
for(int i=1;i<=n;i++) buc[i]=0;
for(int i=1;i<=n;i++) buc[rk[i]]++;
for(int i=1;i<=n;i++) buc[i]+=buc[i-1];
for(int i=n;i>=1;i--) sa[buc[rk[tmp[i]]]--]=tmp[i];
}
void SA(string s){
int n=s.length()-1;
for(int i=1;i<=n;i++) buc[s[i]]++,tmp[s[i]]=1;
for(int i=1;i<=256;i++) buc[i]+=buc[i-1],tmp[i]+=tmp[i-1];
for(int i=1;i<=n;i++) sa[buc[s[i]]--]=i,rk[i]=tmp[s[i]];
for(int w=1;w<=n;w*=2){
Sort(n,w);
int ptr=0;
for(int i=1;i<=n;i++){
if(rk[sa[i]]!=rk[sa[i-1]]||rk[sa[i]+w]!=rk[sa[i-1]+w]) ptr++;
tmp[sa[i]]=ptr;
}
for(int i=1;i<=n;i++) rk[i]=tmp[i];
if(ptr==n) break;
}
}
注:在代码中,对于在当前长度下相等的子串,其 rk[i]
相等(即 rk[i]
是在去重意义下的),但 sa[i]
直接代表某个有序排列,两两不等。对于两个相等的子串,其 sa[]
的大小关系没有保证。
SA()
的初始化部分:获得长度为 1 意义下的 sa[]
和 rk[]
。
第二行 buc[s[i]]++,tmp[s[i]]=1;
:buc[c]
记录 s[i]==c
的个数,tmp[c]
记录存不存在 s[i]==c
。
第三行 buc[i]+=buc[i-1],tmp[i]+=tmp[i-1];
:做前缀和,此时 buc[c]
记录 s[i]<=c
的数量,可以理解为所有 s[i]==c
的 i
在排列中构成一个“块”,buc[c]
是这个块的末尾。 tmp[c]
记录 c
的去重排名。
第四行 sa[buc[s[i]]--]=i,rk[i]=tmp[s[i]];
:前半句找到 s[i]
所在的块,将 s[i]
放在这个块的末尾,然后将末尾前移,使同一块内的元素从后往前填满整个块。后半句就是记录 s[i]
的排名。
Sort(n,w)
:当前的 sa[]
和 rk[]
均是在长度为 w
意义下,将 sa[]
更新到 2w
意义下,没管 rk[]
。
Sort
中,tmp[]
存储子串的临时顺序,和 sa[]
类似。第一轮中,tmp[]
存储 i
按照第二关键字 rk[i+w]
的顺序。
第二行 将 i+w>n
的 i
放在最开头。它们的 rk[i+w]=0
。
第三行 if(sa[i]-w>0) tmp[++ptr]=sa[i]-w;
:升序遍历 rk[i+w]
,挨个放入 tmp[]
中。升序的方式是依据 rk[sa[i]]
对于 i
升序。我们需要加入的第二关键字是 rk[w+1] ~ rk[n]
,我们只要这部分。
第五行 buc[rk[i]]++;
同 SA()
第三行,维护“块”的末尾,只不过关键字变成了 rk
。
第六行倒序处理 sa[buc[rk[tmp[i]]]--]=tmp[i];
:按照第二关键字(tmp[]
)降序的顺序,找到每个元素对应的“块”,从后往前填充。“块”的顺序保证第一关键字升序,遍历顺序保证块内第二关键字升序。
SA()
的倍增部分:根据 sa[]
更新出 rk[]
,临时放在 tmp[]
里。tmp[sa[i]]
应该是单调不降,相等当且仅当 sa[i-1]
和 sa[i]
的两个关键字都相等。这是简单的。
优化 if(ptr==n) break;
第一关键字互不相同,后面的没必要做了。
height 数组:heighti=lcp(S[sai−1,n],S[sai,n])。即字典序相邻的两个后缀的最长公共前缀。
引理:heightrki+1≥heightrki−1。简证一下。
记 sarki+1−1=a,sarki−1=b,那么上面的话翻译一下就是 lcp(S[a,n],S[i+1,n])≥lcp(S[b,n],S[i,n])−1。a′=b+1 时等号成立,而真正的 a 只优不劣。
求两子串 lcp:lcp(S[sai,n],S[saj,n])=minjk=i+1(heightk)。把后缀画成 Trie 看看就明白了。可以使用 ST 表 O(1) 求右边的东西。
可能还有一个比较有意义的 trick 是:在字符串上每 x 个位置放一个关键点,每一个长度为 x 的子串会覆盖恰好一个关键点。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】