幻影彭的彩虹

记录青春的扇区

从ZKW线段树看线段树的性质

最近遇到了很多线段树性质相关的题目,故在此做一个总结。

常规建树

代码

1
2
3
4
5
6
7
8
9
void build(int l,int r,int rt){
do_something();
if(l==r){
return ;
}
int mid=l+r>>1;
build(l,mid,rt<<1);
build(mid+1,r,rt<<1|1);
}

性质

  • 左儿子长度不小于右儿子。
  • 节点编号 rt 描述了从根到自身的父子关系,也间接描述了该点长度和根节点长度的关系,具体的,根节点的最大或最小长度是一个关于该节点长度的一次函数,这个一次函数只由 rt 决定。
  • 编号最大的节点不一定是 r,事实上可以在 \(\log n\) 的时间内求出,如果左儿子多一层,那么一定在左儿子,否则在右儿子,决定某个儿子是否会多一层,当且仅当目前长度为奇数,且 popcount 值为 2

例题

一道简单的题

一道比较简单的题

非常规建树

非常规建树一般会指定建树的 mid 位置。

区间覆盖数

即覆盖一个区间 \([l,r]\) 的最小线段数。

先转成 \((l,r)\),然后依次跳叶节点 \(l,r\) 的父亲,先判断 \(l,r\) 的父亲是否相同,相同就可以走人了。

如果 \(l\) 作为左儿子跳上去,那么该节点的右儿子会对该区间贡献一次,因为该节点的右儿子的父亲超出了区间,但自身在区间内。如果 \(r\) 作为右儿子跳上去,那么该节点的左儿子会对区间贡献一次,原因同理,如此就可以找到区间覆盖数,这也是 ZKW线段树 的原理。

例题

给定一个指定 mid 节点的线段树,需要支持 rotate 一个节点,rotate 定义为伸展树(splay)的 rotate,强制在线询问区间覆盖数。

先考虑没有修改,那么就是找到 \(l,r\)\(lca\),然后一路统计 \(l\)\(r\) 作为左右儿子的次数即可,可以用倍增快速解决。

修改本质上就是断边和加边,用 LCT 维护。

ZKW线段树简介

建树

为了方便,需要建一棵有 \(2^k\) 个节点的树,主要是为了让高度相同,求出第一个不小于 \(n\)\(2\) 的次幂的方式:k=32-__builtin_clz(n-1)

建树时注意没有数据的地方应该弄成 "0",且注意初始数组大小,并且要开 \(2^k+5\) 以免溢出。

建树时记录单点的编号。

单点修改

直接从下往上改就可以了。

1
2
3
void update(int rt,int c){
a[rt].mx1=c;while(rt!=1)push_up(rt>>=1);
}

区间查询

个人习惯闭区间,且有效区间落在 \([1,n]\)

查询时看是否为兄弟节点,如果是就停下,否则对于左端点,是左子树则加上兄弟右子树。对于右端点,是右子树则加上兄弟左子树。另外需要加上两个端点。

1
2
3
4
5
6
7
8
9
10
node query(int lrt,int rrt){
if(lrt==rrt)return a[lrt];
node ret=a[lrt]+a[rrt];
while(lrt>>1!=rrt>>1){
if(~lrt&1)ret=ret+a[lrt+1];
if(rrt&1)ret=ret+a[rrt-1];
lrt>>=1,rrt>>=1;
}
return ret;
}

注意特判区间长度为 \(1\) 的情况。

区间修改

由于是从下往上查询,所以只能采用标记永久化的方式,像查询那样做修改,在对应的点上打 Tag。

效率

对于 \(10^5,3\times 10^5,10^6\) 的随机数据进行了测试,效率改进因子约为 \(0.4\)

简单数论函数和应用

参考资料

部分定义和约定

符号

  • \(\operatorname{id}\)\(\operatorname{id}\) 数论函数,\(\operatorname{id}(x)=x\)
  • \(\epsilon\) : 单位函数 \(\epsilon(x)=[x=1]\)
  • \([]\) :中扩号表达式,中扩号内条件成立则为 \(1\),否则为 \(0\)
  • \(\boldsymbol{1}\)\(\boldsymbol{1}\) 函数,若 \(f(n)=\boldsymbol{1}\),则 \(\forall x \in N^+,f(x)=1\)

数论函数

  • 一个数论函数定义域为正整数,值域为复数。

  • 一个数论函数为积性函数,当且仅当 \(\forall (a,b),\gcd(a,b)=1\rightarrow f(a\times b)=f(a)\times f(b)\)

  • 一个数论函数为完全积性函数,当且仅当 \(\forall a,b,f(a\times b)=f(a)\times f(b)\)

迪利克雷卷积

迪利克雷卷积为数论函数的乘法操作,定义两个数论函数的迪利克雷卷积定义为 \[ g=f*h,g(n)=\sum\limits_{d|n} f(d)\times h(\frac{n}{d}) \]

迪利克雷卷积的除法操作就是逆操作,一般只能构造得到。

迪利克雷卷积的性质(证明见下一部分):

  • 两个积性函数的迪利克雷卷积仍是积性函数。
  • 两个积性函数的迪利克雷卷积除法结果仍是积性函数。

为了方便,以下所有数论函数的乘法未经说明均为迪利克雷卷积。

常见积性函数

  • 欧拉函数 \(\phi(n)\)
  • 莫比乌斯函数 \(\mu(n)\)
  • 约数个数函数 \(d(n)\)

一些结论的简单证明

迪利克雷卷积的性质

简单性质

  • 积性函数 \(f\),满足 \(f(1)=1\)

逆元存在且唯一

所有数论函数存在逆元,即对于所有 \(f\),存在 \(g\) 使得 \(f*g=\epsilon\),直接构造对应的 \(g(n)\) 即可。 \[ g(n)=-\dfrac{\sum\limits_{d|n,d\neq n}{f(d)\times g(\frac{n}{d})}}{f(1)}\\ g(1)=\frac{1}{f(1)} \] 因此数论函数的逆元存在且唯一。

交换律

\(f * g=g * f\),显然成立

结合律

\(f * g * h=f * (g * h)\),成立,但不显然。

\(f * g * h=f_1,f * (g * h)=f_2,f * g=a,g * h=h * g=b\) \[ f_1(n)=\sum\limits_{d_1|n} a(d_1)\times h(\frac{n}{d_1})=\sum\limits_{d_1|n}\sum\limits_{d_2|d_1} f(d_2)\times g(\frac{d_1}{d_2})\times h(\frac{n}{d_1}) \]

\[ f_2(n)=\sum\limits_{d_1|n} b(d_1)\times f(\frac{n}{d_1})=\sum\limits_{d_1|n}\sum\limits_{d_2|d_1} h(d_2)\times g(\frac{d_1}{d_2})\times f(\frac{n}{d_1}) \] 对于任意一组满足 \(d_1|n,d_2|d_1\)\(d_1,d_2\),构造 \(d_2'=\frac{n}{d_1},d_1'=\frac{n}{d_2}\) ,容易发现这样的构造是一一对应的,满足 \(f(d_2)\times g(\frac{d_1}{d_2})\times (\frac{n}{d_1})=f(\frac{n}{d_1'}) \times g(\frac{d_1'}{d_2'}) \times h(d_2'))\)

因此对于上式中的每一项,下式都有一项与之一一对应,因此上下式相等。

分配律

\(f*(g+h)=f*g+f*h\)

显然成立。

两个积性函数的迪利克雷卷积为积性函数

\(f,h\) 为积性函数,则 \(f*g=h\)\(h\) 为积性函数。

\(\forall a,b \ , \gcd(a,b)=1\ ,h(a\times b)=h(a)\times h(b)\),满足 \[ \begin{align} h(a\times b)=&\sum\limits_{d_1|a}\sum\limits_{d_2|b}f(d_1\times \frac{b}{d_2})\times g(\frac{a}{d_1}\times d_2)\\ =&\sum\limits_{d_1|a}\sum\limits_{d_2|b}f(d_1)\times f(\frac{b}{d_2})\times g(\frac{a}{d_1})\times g( d_2)\\ =&\sum\limits_{d_1|a} f(d_1)\times g(\frac{a}{d_1})\sum\limits_{d_2|b} f(\frac{b}{d_2})\times g(d_2)\\ =&h(a)\times h(b) \end{align} \]

两个积性函数的迪利克雷卷积除法为积性函数

证明 \(f,h\) 为积性函数,且 \(h*g=f\),则 \(g\) 为积性函数。

\(h'*h=\epsilon\),则 \(g=f*h'\)

即证明积性函数的逆元也为积性函数。

证明

使用了数学归纳法。

完全积性函数相关

\(w\) 是完全积性函数,则 \[ (g\cdot w) * (f \cdot w) = (g * f)\cdot w \]

常见的积性函数关系

  • \(\mu* \boldsymbol{1}=\epsilon\)
  • \(\phi* \boldsymbol{1}=\operatorname{id}\)
  • \(\mu * \operatorname{id} = \phi\)

莫比乌斯反演

莫比乌斯反演的常见做法是用其它易于交换求和符号的项替换掉不容易求和的相关项。

不容易交换求和的项一般有 \(d,\gcd\) 等。

数论分块

莫比乌斯反演或者其它数数题中常用的优化方式。

核心原理是对于 \([1,n]\) 中所有数 \(i\)\(\frac{n}{i}\) 的结果只有 \(\sqrt{n}\) 个。

证明是容易的,对于 \([1,\sqrt{n}]\),一共有 \(\sqrt{n}\) 个值,对于 \([\sqrt{n},n]\),一共也只有 \(\sqrt{n}\) 个值。

枚举的方式是先枚举一个 \(l\),然后计算出一个最大的 \(r\),满足 \(\frac{n}{r}=\frac{n}{l}\),容易证明 \(r=\big\lfloor\dfrac{n}{\lfloor\frac{n}{l}\rfloor}\big\rfloor\)

1
2
3
for(int l=1,r=1;l<=n;l=r+1,r=n/(n/l)){
do_something();
}

莫比乌斯函数

定义

\[ \mu(n)= \begin{cases} 1&n=1\\ 0&n\text{ 含有平方因子}\\ (-1)^k&k\text{ 为 }n\text{ 的本质不同质因子个数}\\ \end{cases} \]

\(\mu*\boldsymbol{1}=\epsilon\)

这是核心结论了。

考虑证明 \(\forall n>1,\mu(n)=0\)

考虑每个约数的贡献,显然每个质因子只需要考虑一次,如果质因子出现多次,那么根据定义贡献为 \(0\)

设有 \(k\) 个质因子,那么选出奇数个和选出偶数个的方案显然是相等的,所以和为 \(0\)

莫比乌斯反演

一般形式

\[ \begin{align} f*\bold{1}=g\iff& g*\mu=f \\ g(n)=\sum\limits_{d|n}f(d)\iff& f(n)=\sum\limits_{d|n}\mu(d)\times g(\frac{n}{d}) \end{align} \]

证明是显然的,因为 \(\boldsymbol{1}*\mu=\epsilon\)

常用

\([\gcd(i,j)=1]=\sum\limits_{d|\gcd(i,j)}\mu(d)\)

枚举 \(d\),即可快速计算。

欧拉反演

名字是杰哥取的。

常用形式

\(\gcd(i,j)=\sum\limits_{d|\gcd(i,j)}\phi(d)\)

证明

即证明 \(\phi*\boldsymbol{1}=\operatorname{id}\)

我们只需要证明 \(\phi*\boldsymbol{1}\)\(p^c\) 处取值为 \(\operatorname{id}(p^c)\),由于 \(\phi,\boldsymbol{1},\operatorname{id}\) 均为积性函数,自然在所有位置成立。 \[ \begin{align} (\phi*\boldsymbol{1})(p^c)=&1+\sum _{i=1}^{c} (p-1)\times p^{i-1}\\ =& 1+\frac{p^c-1}{p-1}\times(p-1)\\ =&p^c\\ =&\operatorname{id}(p^c) \end{align} \]

筛法

介绍四大筛法。

四大筛法通常用于求一些积性函数的前缀和。

假设要求 \(f\) 的前缀和。

\(F(n) = \sum\limits_{i=1}^n f(i),H(n)=\sum\limits_{i=1}^n h(i),G(n)=\sum\limits_{i=1}^n g(i)\)

杜教筛

杜教筛的核心是构造两个容易求前缀和的函数 \(g,h\),满足 \(h = f * g\)

\[ \begin{align} H(n)&=\sum\limits_{i=1}^n h(i)\\ &=\sum_{i=1}^n\sum_{d|i}g(d)f(\frac{i}{d})\\ &=\sum_{d=1}^ng(d)\sum_{i=1}^{\lfloor\frac{n}{d}\rfloor} f(i) \end{align} \] 将右边 \(d\ge 2\) 的项移到左边 \[ H(n)-\sum\limits_{d=2}^ng(d)F(\lfloor\frac{n}{d}\rfloor)=F(n)g(1) \] \(H(n)\) 是好求的,然后 \(g(1)=1\),后面的项对 \(n\) 数论分块。

然后有一个结论 \(\big\lfloor\dfrac{\lfloor\frac{n}{a}\rfloor}{b}\big\rfloor=\lfloor\dfrac{n}{ab}\rfloor\)。因此要求的项只有 \(\sqrt n\) 项。

线性筛前 \(n^\frac{2}{3}\) 项的前缀和,可以取到最优复杂度 \(n^\frac{2}{3}\)。复杂度证明见 OI-WIKI,证明

PN(Powerful Number) 筛

定义 Powerful Number 是每个质因数质数不小于 \(2\) 的数。

如果能构造一个容易求前缀和的积性函数 \(g(x)\),满足 \(g(p)=f(p)\),那么我们就可以在 \(O(\sqrt n)\) 的时间复杂度内计算 \(F(n)\)

具体的,考虑构造 \(h = f / g\),所以 \(f(p) = h(1)g(p) + h(p)g(1)\),由于 \(g(p) =f (p)\),所以有 \(h(p)\) 处取值为 \(0\),由于 \(g,f,h\) 都是积性函数,所以 \(h\) 仅在 Powerful Number 处有取值,其余处取值为 \(0\)

考虑 \[ \begin{align} F(n)&=\sum_{i=1}^nf(i)\\ &=\sum_{i=1}^n\sum_{d|i}h(d)g(\frac{i}{d})\\ &=\sum_{d=1}^nh(d)\sum_{i=1}^{\lfloor\frac{n}{d}\rfloor}g(i) \end{align} \] 枚举所有 Powerful Number,计算 \(h(d)G(\lfloor\frac{n}{d}\rfloor)\) 即可,Powerful Number 的个数是 \(O(\sqrt n)\) 的。

构造 \(h\) 的话,可以直接用 \(g * h = f\),并使用卷积的定义构造,当然,\(f(p^c)\) 必须要容易求。

州阁筛

Exchange-Argument

  • 参考资料:2022 集训队论文《浅谈一类基于交换的贪心在信息学竞赛中的应用》

  • 本文主要介绍树上形式。

一般形式

你需要为 \(n\) 个元素安排一个排名,得到一个序列 \(a\),最小化某函数 \(F(a)\)

如果存在一个满足传递性和强完全性的关系 \(\le\),满足 \(a,b\) 均为元素, $ ab,F(s_1+a+b+s_2)F(s_1+b+a+s_2)$,则按照 \(\le\) 排序后的排名可以最小化 \(F(a)\)

典例为 NOIP2012 国王游戏。

我们在这里略去证明,详见参考资料。

  • 强完全性指 \(\forall a,b, a\le b\bigvee b\le a\),显然满足强完全性一定满足自反性。
  • 条件中 \(\{s_1+a+b+s_2\}\) 为全集。

树上问题

例题

给定一棵树,需要为树的每个节点安排排名 \(p_u\),父节点的排名需要低于子节点。每个点有一个价值 \(c_i\),需要最小化 \(\sum c_u\times p_u\)

分析与结论

唯一的变化是,对于原问题元素的比较 \(\le\),拓展到了对于子序列的比较,即 \(a,b\) 为原来元素的一个序列。

以下是一个重要结论

考虑一个元素 \(v\),我们希望说明如果它的父亲 \(u\)\(u>v\),那么 \(v\) 一定在 \(u\) 之后立刻被选择,此时将 \(v,u\) 合并,可以得到一个规模为 \(n-1\) 的子问题。

考虑证明 \(u\) 被选择后一定会选择 \(v\),对于一个满足树的限制的排列 \(p\),如果 \(u,v\) 不相邻,那么由于 \(v>u\),所以对于序列 \(p_1,p_2\dots u,p_l,p_{l1} \dots p_r,v,\dots,p_n\),一定有 \(u>p_{l,r} \bigvee p_{l,r}>v\),否则 \(v<u\),如果为前者,那么交换 \(u\)\(p_{l,r}\),后者同理,所以 \(v\) 一定紧接着 \(u\) 被选择。

我们可以用以下两种方式解决问题。

方式1

考虑最大的元素 \(v\),显然可以合并它和它的父亲 \(u\),变成一个新的规模更小的问题。

合并的过程可以用并查集维护连通块,注意区分并查集父亲和实际父亲。

使用 priority_queue 或者 set 可以实现加入删除和查询最小值,priority_queue 的方式是同时维护一个代表删除的 priority_queue,实际上 priority_queue 会比 set\(1\) 倍。

注意实现的时候,如果找到了根,那么根其实有可能不在 priority_queue 里面,可删除的 priority_queue 如果删除了不存在的元素是会出问题的,所以一定要特判掉。

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

方式2

我们考虑对每个子树求其答案。

利用分析中的结论,不妨将一棵子树划分成一些必须连续选择的序列,作为树对排列顺序的要求,将这些连续的序列视为一个元素后,我们可以对序列按照正常方式排序得到最优解。

划分序列的过程是对于一棵子树 \(u\),将它的所有儿子的序列拿来排序,尝试将 \(u\) 与最小的序列合并得到新的序列,如果 \(u\) 当前所在的序列较大,由于结论我们知道合并后一定能拿到最优解,否则停止合并,此时 \(u\) 所在序列的权值最小,一定会被最先选择,其中 \(u\) 又一定会被第一个选择。

容易发现合并的过程一定不会改变子树内的选择顺序。

合并时采用启发式合并,利用 setpriority_queue 维护当前子树序列,复杂度为 \(O(n\log^2n)\),但优势在于可以求出所有子树的答案。

当然可以使用可并堆达到 \(O(n\log n)\)

注意到其实一遍 dfs 就可以解决问题,因为启发式合并复杂度保证的来源是合并一次的复杂度为 \(O(\min(|u|,|v|))\),所以并不需要提前计算子树大小,直接合并即可,setpriority_queueswap 操作是 \(O(1)\) 的。

碎碎念

Exc-Arg 除了直接解决问题外,也可以用来简化问题,将选择元素并重排的问题变为排序后选择子序列的问题。

对于树上的情况,我还没有见到过相关的问题。

多测的奇妙问题

最短路

你清空了吗?

你真的清空了吗?

你真的真的真的清空了吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void dij(int s,int t){
memset(vis,0,sizeof(vis));
memset(dis,1,sizeof(dis));
//while(!q.empty())q.pop();
dis[s]=0;q.push(s);
while(!q.empty()){
int u=q.top()%M;q.pop();
if(vis[u])continue;vis[u]=1;
if(vis[t])return ;
//!!! priority_queue 还没空就跑路了,你不玩完谁玩完
for(int i=head[u];~i;i=a[i].next){
int v=a[i].v,w=a[i].w;
if(dis[u]+w>=dis[v])continue;
dis[v]=dis[u]+w;
q.push(dis[v]*M+v);
}
}
}

一些迷惑问题

Prim 最短路

1
2
3
4
5
6
7
for(i=1;i<=n;i++){
int tar=0;
for(j=1;j<=n;j++)if(dis[tar]>dis[j]&&!pd[j])tar=j;
pd[tar]=1;ans+=dis[tar];dis[tar]=0;
for(j=1;j<=n;j++)cmin(dis[j],dis[tar]+val[tar][j]+val[j][tar]);
// for(j=1;j<=n;j++)if(!pd[j])cmin(dis[j],dis[tar]+val[tar][j]+val[j][tar]);
}

你的边权算对了吗?

有人之前写的 Prime 算法

匿名函数排序

1
2
sort(b+1,b+m+1,[](int a,int b){return a<b;});
sort(b+1,b+m+1,[](int a,int b){return b>a;});

你的排序,是这个升序的,还是这个降序的,还是这个无序的。

线段去包含

思考清楚到底怎么排序,如果右端点相同,按什么排序。

端点会不会是负数,maxn 初始值会不会太大(开 0,结果有线段端点是 0 你把它干了)

需要记录一些原来编号的排序

想清楚排序得到的 rk 数组到底是什么,不要乱查乱用,该求逆的求逆。

一些技巧的坑

非显式建边

举个例子,点有个性质 \(c_i\),可以花费 \(k_j\) 的代价从任意 \(c_{a_j}\) 的跳到任意 \(c_{b_j}\) 的点。

然后你对每个 \(c\) 建立一个点,从每个点向它连了一条边。

然后你发现你的每一个 \(c\) 可以花费 \(0\) 的代价互相到达。

正确的方式是每个 \(c\) 建立入点和出点。

可删除的优先队列

一般用两个优先队列实现,需要保证被删除的元素一定存在

语言本身的坑

cerr

调试的时候 cerr 很好用的,就算忘了删也不会爆零,但是 cerr 真的很慢,因为它直接向标准输出流输出了,没有过缓冲区,相当于每次输出一次就 fflush(out) 一下,TLE 没商量。

QQ 群一笔画问题

Author:Huan_yp

一笔画问题,即给定一张无向图 \(G(V,E)\),每个点的度数均为偶数或者仅有两个点度数为奇数,需要求出一条不重复经过边的路径,遍历所有的边。

由于在算法竞赛中,这个问题是简单的,这里只讨论在 QQ 群一笔画中,快速解决问题的方式。

归纳构造法证明

已知给定图为一笔画图,故一定满足每个点的度数均为偶数或者仅有两个点度数为奇数,并且图联通,若存在两个点度数为奇数,则从其中一个点开始,否则可以从任意点开始。

执行以下过程。

  • 在当前节点任意找一条边,如果可以找到,则重复此过程。
  • 如果无法找到:
    • 图已经被遍历,结束,得到保存的路径。
    • 该点度数为 \(0\),所以一定回到了起点或者到了终点。记录并删除该路径中所有边,并回溯到上一个存在出度的点,可以证明一定存在这样的点,否则图不连通。该点的度数一定为偶数,对该点进行上述过程,得到一条首尾相接的路径,插入到原路径,得到完整的欧拉(回)路。
  • 容易发现,这样的过程将图遍历了一遍,所以最后得到的是完整的欧拉路径。

代码实现比较简单,只需要使用栈在退出时记录当前节点,得到的,从栈顶到栈底,就是一条完整的欧拉路径。

下面是一个例子:

\(1->2->5\) 走,\(5\) 处无路可走,回到 \(2\),记录一条路径 \(2->5\),从 \(2\) 继续走 \(2->3->4->2\),回到了,插入原路径变为 \(1->2->3->4->2->5\)

正确的操作方式

由于找边的耗时比较长,我们需要尽可能少的找边。

  • 如果有 API 可以读取边的情况,那么直接做一遍上述过程即可得到答案。
  • 如果不存在对应的 API,我们直接进行上诉操作,可以拿一个程序记录已经走过的点和边并提示路径。
  • 如果图不为欧拉回路,则存在死路,判断方式比较简单,当前点是否为起始点,如果死路位于起始点,直接进行路径回溯操作,即步骤二。否则撤销所有操作,从当前节点开始逆向进行已经进行过的操作。

这样单纯的找边的次数恰好为 m 次,即边数。

回溯和走边的操作总和不会超过 2m。

平均执行次数

考虑图为随机生成,期望的行走次数,感觉是 polylog 级别,但是我不会证明。

蒙特卡洛模拟

咕咕咕,没时间写代码。

采用蒙特卡洛模拟算法对该结论进行验证

严谨数学证明

咕咕咕。

也许可以考虑每条边回溯次数期望 \(e_{u,v}\)

Tarjan算法和坑

某场多校,某道题,有负环,需要排除(负环)。

某人写了,又交了,吃罚时,需要排除(某人)。

队友写了,又交了,他过了,某人完了。

数据有了,调试了,调完了,Tarjan锅了。

本文提一下 Tarjan 算法里的坑。

算法流程和原理

通过对图进行 DFS 遍历,得到 DFS 树。

容易发现每个 SCC 是一个树上的联通块,这是由 DFS 的过程保证的,如果分开了,那么就不满足 DFS 的性质。

DFS 一个点,会访问到所有它能到的点。由于它们是 SCC,所以 DFS 该 SCC 的任意节点的时候,一定会让 SCC 中所有未访问的节点都在它的子树里,所以不会出现断开的情况。

考虑树的每个节点是否为 SCC 的根。定义一个节点为 SCC 的根,当且仅当这个节点是该 SCC 中最浅的节点。

定义 low[u]\(u\) 能通过自己或者自己的子树到达的点,时间戳的最小值。维护一个栈,表示当前还没有确定连通块的节点。

如果一个点 \(u\)low[u]=dfn[u],那么该点无法到达时间戳更小的点,所以一定是 SCC 的根,弹出所有栈中的值直到 \(u\),作为该 SCC 的所有点。

如果访问了一个已经访问过的点 \(v\),分两种 CASE,其一是它已经被弹栈了,这样的话该边一定是横插边,直接不管,因为 \(v\) 到不了 \(u\)。如果还没有弹栈,说明该点的 SCC 还没有确定,所以这个点是可以到 \(u\) 的父亲的,也能到 \(v\)。所以这种点是有效的,\(u\) 因此能到达更上面的点,不是 SCC 的根。

不同的写法

省略全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//AC Code
void dfs(int u){
dfn[u]=low[u]=++s;
st[++top]=u;in[u]=1;
for(int i=0;i<e[2][u].size();i++){
int v=e[2][u][i].v,w=e[2][u][i].e,f=e[2][u][i].f;
if(dfn[v]){
if(in[v])cmin(low[u],dfn[v]);
continue;
}
dfs(v);cmin(low[u],low[v]);
}
if(dfn[u]==low[u]){
cnt++;int v;
do rk[v=st[top--]]=cnt,in[v]=0; while(v!=u);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//AC code
void dfs(int u){
dfn[u]=low[u]=++s;st[++top]=u;
for(int i=0;i<e[2][u].size();i++){
int v=e[2][u][i].v,w=e[2][u][i].e,f=e[2][u][i].f;
if(dfn[v]){
if(!rk[v])cmin(low[u],dfn[v]);
continue;
}
dfs(v);cmin(low[u],low[v]);
}
if(dfn[u]==low[u]){
cnt++;int v;
do rk[v=st[top--]]=cnt; while(v!=u);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//WA code
void dfs(int u){
dfn[u]=low[u]=++s;st[++top]=u;in[u]=1;
for(int i=0;i<e[2][u].size();i++){
int v=e[2][u][i].v,w=e[2][u][i].e,f=e[2][u][i].f;
if(dfn[v]){
if(in[v])cmin(low[u],dfn[v]);
continue;
}
dfs(v);cmin(low[u],low[v]);
}
if(dfn[u]==low[u]){
cnt++;int v;
do rk[v=st[top--]]=cnt; while(v!=u);
}
in[u]=0;
}

注意到前两种 Code 维护的都是栈中元素,因此没有问题。

但第三种写法,维护的是所有的祖先,当当前节点 \(u\) 连接到的点 \(v\),可以通过其返祖边回到更上面的父亲时,这种做法会漏掉 \(v\)。因此是错误的。

举个例子。

访问 \(3\) 时,\(4\) 已经从 \(in\) 中移除,但是我们仍然可以通过 \(4\) 到达 \(3\),所以会出现错误。

洛谷的模板题,数据相当之水,第三种做法可以通过,但它是错误的。

一些碎碎念

缩点的时候,也就是求强连通分量,\(low[u]\)\(low[v],dfn[v]\)min,都是可以的,因为不影响判断某个点是否是 SCC 的根。

如果图是无向图,那么不会存在横插边的问题,所有边都是返祖边。

求边双连通分量,和 low[v]min 同样没有问题,因为不影响一个点是不是边双的根的判断。

判断一个点是边双根的方式是,它无法通过自己或者儿子,到达更上面的点。这也意味着,该点与其父亲的连边是桥。

边双连通具有传递性

求点双连通分量,判断割点,如果和 low[u] 取 min,是会出问题的。

判断一个点是割点的方式是,它的任意一个儿子无法在没有它的情况下到达上面的点。特别注意,该点如果没有儿子,就不是割点。

点双,就是以割点为界的所有连通块。

下图是一个取 low[v] 出问题的例子。

先访问 \(3\),再访问 \(5\),会错误的认为 \(5\) 能够到 \(1\),我们 low 的定义是通过自身或自身的子树能到的最小时间戳,而如果和 low[v]min,就势必会经过其它的点。在这里,\(5\) 通过了 \(4\) 到达 \(1\),因此如果删去 \(4\)\(5\) 就不能到 \(1\),这里需要保证定义的严谨性。

前面的强连通分量,只需要能够到达,但不关心怎么到达,所以没问题。

边双也只关心能否到达更前面点,所以没有问题。

T1

签到题。

简单观察,发现最后的数为 \(\sum_{i=1}^{n}a_i\times\tbinom{n-1}{i-1} \pmod m\)

题目需要求出无关量,本质上是求系数为 \(0\) 的项,即求 \(\tbinom{n-i}{i-1} \equiv 0\pmod m\)\(i\)

因为 \(m\) 不为质数,所以没法直接处理阶乘,但很容易给出一个递推的 \(O(n^2)\) 做法。

注意到我们只关心 \(\tbinom{n-i}{i-1}\) 是否为 \(m\) 的倍数,\(m\) 的质因子也比较少 \(\leq 10\),所以我们可以直接记录每一个质因子的质数即可,这样就可以直接阶乘。

T2

简单题。

有个很重要的限制,一条边只能经过两次,说白了就是进入一棵子树之后,必须先确定叶节点有没有他的女朋友之后才能出去。

所以就能设计出树形 DP,\(dp[u][0/1]\),表示 \(u\) 的子树是否有渡边女朋友,走完子树 \(u\) 的期望。

最终答案为 \(dp[u][1]\)

考虑转移,首先,有小弟把守的节点,\(dp[u][0]=0\),因为渡边家兴只需要询问小弟就可以走完这棵子树。

计算 \(dp[u][0]\),就是 \(\sum\limits_{v\in E(u)}dp[v][0]+2\),就是搜索完所有的儿子节点加上进入每个儿子节点的消耗。

计算 \(dp[u][1]\),本质上我们是要确定一个搜索子树的顺序,让期望搜索时间最小,考虑对于一个顺序 \(p\),期望时间是多少,记 \(v\) 子树中叶子个数为 \(sz_v\),显然是 \[ \Large\sum\limits_{i=1}^{k} dp[p_i][1]\times \frac{sz_{p_i}}{sz_u} + (dp[p_i][0]+2)\times\frac{\sum\limits_{j=i+1}^{k}sz_{p_j}}{sz_v} \] 即枚举所在子树,加上额外的搜索空子树的时间。

我们可以证明,按照 \(\frac{dp[v][0]+2}{sz_v}\) 升序排序后的顺序是最优的,具体证明参考国王游戏。

T3

有些毒瘤的题。

简单观察发现每个字符都和其上下的位置高度相关,将 NOI 划分为 9 个部分,三个字母均分为中间和两边三个部分,再加上两个空白部分共 11 部分,考虑 DP,设 \(dp[i][s][l][r]\) 表示第 \(i\) 列,状态为 \(s\) ,上下为 \(l,r\) 的最大值。

其它的转移是简单的,我们着重强调 N 中间部分的转移。它转枚举了上一个上下端点 \(l',r'\),向 \(l,r\) 转移,条件为 \(l\le r'+1\),枚举当前的上端点 \(l\),我们可以动态维护一个数组 \(dp\_max[i]\) 表示考虑所有 \(r=i,l'\le l\)。转移使用当前的 \(l\) 对应的 \(dp\_max\) 数组\([1,r]\)\(\max\) 即可。

这道题的思维难度甚至没有 T2 大。

T4

思维题。

找规律和暴力各有 10pts。

Solution1

先把 BFS 序转化为 \(1,2,\dots n\),然后再来考虑。

因为要求平均高度,所以我们需要求出总高度和总个数,转化后一棵树的高度等于 \(n\) 号节点的深度。

现在考虑划分 \(1-n\) 这个 BFS 序列。可以发现一个划分最多只能对应一棵树。

一个显然的必要条件是相同高度的元素在 DFS 序列中递增,另一个显然的必要条件是 DFS 序列中 \(dep_{a_i}+1\ge dep_{a_{i+1}}\)

事实上,这两个条件也是充分的。

\(dp[i][0/1]\) 表示考虑到 \(i\),总数和总高度。转移检查哪些左端点可以转移即可,事实上,可以转移的左端点一定是一个区间,可以处理 \(dp\) 数组的前缀和完成快速转移。

判断转移区间和结论证明,留作思考,后文有 Details。

如果可能,尽量不要看 Details,自行思考

Solution2

考虑一个划分合法的必要条件,Solution1 中已经提到。

考虑哪些位置可以划分,把一个间隔看成一个 01 变量,选择划分为 \(1\),发现可以转化为一个这样的问题:

你需要确定长度为 \(n-1\) 的二进制串,有若干个限制。每个限制形如

  • \([l,r]\) 至多有一个 \(1\)。即:\(dep_{a_i}+1\ge dep_{a_{i+1}}\) 转化而来的 \([a_i,a_{i+1})\)
  • \(x\) 位置必须为 \(1\) 。即:如果 \(pos_i>pos_{i+1}\),那么由于同高度在 \(a\) 中的位置递增,则 \(i\) 位置必须为 \(1\)

很容易设计出一个 \(O(n^2)\) 的动态规划做法。

观察第一个条件,如果 \(a_{i+1}>a_i\) 才会有第一个限制,如果 \(a_i+1=a_{i+1}\),那么相当于这个限制不存在。

否则,因为保证一定存在一棵合法的树,所以 \([a_i,a_{i+1}]\) 区间的 \(pos\) 数组必须能够被划分为两个连续上升子序列。\(a_i\) 又和 \(a_{i+1}\) 紧紧挨在一起,那么得知必定会被划分,因为中间的某个数一定会冲突。我们很容易模拟得到被划分的位置,之后这个区间的所有数都不能再被划分。

对于没有限制的位置,我们划分与不划分的方案数是相同的,因此对期望的贡献是 \(0.5\)

将所有位置的期望加起来就是答案。

Solution1 Details

Transform

看看样例,发现从 \(1-n\) 排列比较好想,所以考虑先把点做个变换,弄成 \(1-n\),所以 \(b\) 变成了 \(1,2,3,\dots,n\)

然后记 \(u\) 的深度为 \(dep_u\),发现 \(\forall i\in[1,n),dep_i\le dep_{i+1}\le dep_i+1\)

然后又发现,对于 BFS 序列为 \(1,2,3,\dots n\) 的树,一个点遍历子节点的顺序一定是按编号从小到大遍历

所以,确定了每个点的深度和 DFS 序列。可以唯一确定一颗树。

感性证明的话,确定深度之后把点画出来,画在一个二维平面上,深度相同的点在一层按编号从小到大排列,然后在 DFS 序列上走,如果下一个点深度更大,那肯定是往下连,如果深度不变或者更小,那一定是回去了一部分再往下走了一个,这样的逻辑可以唯一确定一棵树。

自上到下,自左到右遍历二维平面所有点的过程,就是 BFS 的过程

请认真理解二维平面的含义。

内含比较严格的证明

本质上,确定深度的过程就是划分 \(b\) 序列的过程

Native DP Algorithm

所以考虑对 BFS 序列的划分过程 DP,设 \(dp[i][k]\) 表示考虑到第 \(i\) 个点,深度为 \(k\) 的合法划分方案总数。

然后我们需要枚举一个左端点 \(j\),考虑如何判断这个划分是否合法。

首先有个必要条件:同一深度的点,在 DFS 序列上的位置必须递增,因为我们遍历一个点的儿子的顺序是从小到大。称该条件为条件一

其次,我们模拟一下在 DFS 序列上走的过程,发现一个点 \(u\) 的下一个点 \(x\) 一定有 \(dep_u+1\ge dep_x\),原因是显然的。称该条件为条件二

满足了以上条件,我们可以说明一定可以构造出一棵唯一对应的树。具体的,对于一个点 \(u\),下一个点是 \(v\),找到它或者它的祖先 \(u'\), 满足 \(dep_v=dep_{u'}+1\),那么 \(v\) 的父亲就是 \(u'\)。由于第一个条件,限制了处理的 \(v\) 一定是该层第一个还没有安排父亲的点。所以每个点只会恰好被安排一次父亲,得到一个合法的树。

这样的话,我们再记录一维划分起点,变成 \(dp[i][j][k]\)

转移枚举当前起点和上一个划分的起点,判断两个条件可以简单的 \(O(n)\) 做。

复杂度为 \(O(n^4)\),因为对于 \(n\) 个深度 \(k\) ,一共只需要判断一次。

Observaion1&&Optimization1

  • 发现其实不用记录具体每个深度有几个元素,只需要记录深度之和与树的个数就可以计算答案。
  • 假设划分的区间为 \([l,r]\) ,深度为 \(d\)。条件二等价于,\(a\)\([1,l)\) 的后一个元素 \(x\),一定满足 \(x\le r\)。因为每次加入一段区间的点之后,上一次的深度为 \(d-1\) 的点的右端点,如果还没有确定深度,就会被确定为 \(d\)。所以加入后,存在右端点还没确定深度的点,其本身深度只能为 \(d\) 了。所以可以直接判断交界处的深度关系,判断方式是 \(\forall i\in[1,n),a_i>j \or a_{i+1}\ge i\)

40Pts 的代码运用了第二个观察,请阅读。

运用第一个观察,DP 状态简化为 \(dp[i][j][0/1]\)

再次运用第二个观察,其实已经无需记录 \(j\),DP 状态进一步简化为 \(dp[i][0/1]\)

上述做法的复杂度是 \(O(n^3)\) 的,考虑优化。

参看 Codes 部分的 40Pts 做法。\(sum\) 数组的含义是,\(sum[i][j]\) 表示以 \(i\) 结尾,深度为 \(j\) 的方案总数。

40Pts 的部分没有对层数做简化

Observaion2&&Optimization2

  • 其实 \(dp\) 数组没有用,只需要记录一个 \(sum\) 数组就可以了。

  • 条件一,显然可行的 \(j\) 是一个右端点为 \(i\) 的区间,对于每个 \(i\),可以处理出满足 \(p\) 数组(参考题解开头的定义)区间递增的最小左端点,作为 \(j\) 左端点的限制。

  • 条件二,显然可行的 \(j\) 是一个前缀,并且右端点随 \(i\) 增大,这限制了 \(j\) 的右端点。

由于条件一,二的限为区间转移限制,所以记录一下 \(sum\) 的前缀和即可做到转移 \(O(n)\)

利用尺取法的思想可以 \(O(n^2)\) 的计算条件二的右端点。

但进一步观察,发现对条件二进行了一些无用的 check,每次移动端点时,有用的 check 只有一个,就是值为右端点本身的位置。

所以 check 变成了 \(O(1)\),总复杂度 \(O(n)\)

参考 100Pts 代码,注意,其中的 \(dp\) 代表 40pts 写法中的 \(sum\)\(sum\) 代表其前缀和。

Notes

  • DP 过程中最大值达到了 \(2^{n}\),需要手写科学计数法,可以忽略指数差距过大的加减运算。

Codes

40pts \(O(n^3)\)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include<bits/stdc++.h>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}const int N=205;
int i,j,k,n,s,t,m,tp1,tp2;
int a[N],p[N],b[N];
double dp[N][N][N],sum[N][N];
signed main()
{
read(n);
for(i=1;i<=n;i++)read(a[i]);
for(i=1;i<=n;i++)read(b[i]),p[b[i]]=i;
for(i=1;i<=n;i++)a[i]=p[a[i]];
for(i=1;i<=n;i++)p[a[i]]=i;
dp[1][1][1]=1;sum[1][1]=1;
for(i=2;i<=n;i++){
for(j=2;j<=i;j++){
//条件1
for(k=j+1;k<=i;k++)
if(p[k-1]>p[k])break;

if(k!=i+1)continue;

//条件2
for(k=1;k<n;k++)
if(a[k]<j&&a[k+1]>i)break;
//如果交界处,一个深度小于 d,另一个大于 d,那么寄。
if(k!=n)continue;

for(k=1;k<=n;k++)
dp[i][j][k]+=sum[j-1][k-1];
}
for(j=1;j<=i;j++)
for(k=1;k<=n;k++)
sum[i][k]+=dp[i][j][k];

}
double ans=0,cnt=0;
for(i=1;i<=n;i++){
cnt+=sum[n][i];
ans+=sum[n][i]*i;
}
printf("%0.3lf",ans/cnt);
return 0;
}

100 pts \(O(n)\)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include<bits/stdc++.h>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}
const int N=202005;
int i,j,k,n,s,t,m,tp1,tp2;
int a[N],p[N],b[N],lst[N],far[N];
struct Double{
double val;
int p;
Double cap(Double x){
while(abs(x.val)>1e18){
x.val/=2;
x.p++;
}
while(abs(x.val)<1e-18){
x.val*=2;
x.p--;
}
return x;
}
void operator =(int x){p=0,val=x;}
Double operator +(const Double &x){
if(x.p-p>50)return x;
if(p-x.p>50)return *this;
return cap({val+x.val*pow(2,x.p-p),p});
}
void operator +=(const Double &x){*this=*this+x;cap(x);}
Double operator -(){return Double{-val,p};}
Double operator -(Double &x){return cap((*this)+(-x));}
Double operator /(const Double &x){return cap({val/x.val,p-x.p});}
double get(){return val*pow(2.0,p);}
};
Double dp[N][2],sum[N][2];
signed main()
{
read(n);lst[1]=1;
for(i=1;i<=n;i++)read(a[i]);
for(i=1;i<=n;i++)read(b[i]),p[b[i]]=i;
for(i=1;i<=n;i++)a[i]=p[a[i]];
for(i=1;i<=n;i++)p[a[i]]=i;
for(i=2;i<=n;i++){
if(p[i]>p[i-1])lst[i]=lst[i-1];
else lst[i]=i;
}
dp[1][0]=1,dp[1][1]=1;
sum[1][0]=1,sum[1][1]=1;
far[1]=1;
for(i=2;i<=n;i++){
for(j=far[i-1]+1;j<=i;j++){
if(a[p[j-1]+1]<=i||p[j-1]==n)continue;
else break;
}
far[i]=--j;
if(far[i]>=lst[i]){
dp[i][0]=sum[j-1][0]-sum[lst[i]-2][0];
dp[i][1]=(sum[j-1][1]-sum[lst[i]-2][1])+(sum[j-1][0]-sum[lst[i]-2][0]);
}
sum[i][0]=dp[i][0]+sum[i-1][0];
sum[i][1]=dp[i][1]+sum[i-1][1];
}
printf("%0.3lf",(dp[n][1]/dp[n][0]).get());
return 0;
}

树形背包的比较严谨的复杂度证明

转移方式

1
2
3
4
5
6
7
8
9
10
11
12
void dfs(int u,int fa=0){
sz[u]=1;
for(auto v:e[u]){
if(v==fa)continue;
dfs(v,u);sz[u]+=sz[v];
for(i=min(k,sz[u]);i>=0;i--){
for(j=max(0,i-(sz[u]-sz[v]));j<=i&&j<=sz[v];j++){
//O(trans) do something
}
}
}
}

证明,总复杂度为 \(O(n^2)\)

证明 \(n^2\) 直接忽略 \(i\) 的循环和 \(k\)\(\min\) 的操作。

考虑在某个点合并子树的过程,本质上,如果把 \(sz_u\) 看成当前子树内所有的点,我们的第一个循环是限制了合并后的总大小,第二个循环限制了已经合并的子树的大小,以及待合并的子树的大小。

所以在一个点 \(u\) 处,只有每一对以它为 LCA 的点对会贡献一次,其它点对均不贡献。

所以总复杂度为点对数量 \(O(n^2)\)

证明,总复杂度为 \(O(trans\times min(n^2,nk))\)

\(O(n^2)\) 的证明是老生常谈的事情了,考虑 \(k\leq n\) 的情况,试证明总复杂度为 \(O(nk)\)

考虑一对点在 LCA 处的贡献,

考虑合并子树时的三种 case,小于 \(k\) 向大于 \(k\) 合并,大于 \(k\) 向大于 \(k\) 合并,小于 \(k\) 向小于 \(k\) 合并。

我们分别说明三种情况的时间复杂度都是 \(O(nk)\) 的。

Case1

小于合并到大于。

准确的说,是小于 \(k\) 的子树合并之后,大小大于 \(k\)

考虑每一个点对这种情况的贡献,每个点显然只会参与一次过程,因为参与之后它所在的块的大小就大于 \(k\) 了。

总复杂度 \(O(nk)\)

Case2

大于合并到大于。

考虑每一次合并,复杂度 \(O(k^2)\),会永远失去一个大于 \(k\) 的块。而我们最多会生成 \(\frac{n}{k}\) 个大小大于 \(k\) 的块,因为一个点至多参与一次生成新块的过程。

总复杂度 \(O(nk)\)

Case3

小于合并到小于,那么这种情况就只会发生于大小小于 \(k\) 的子树内以及其父亲的子树间。如果一个点的父亲的大小也不超过 \(k\),我们忽略它本身,直接在它的父亲处计算所有复杂度贡献。这样所有的复杂度分成两个部分,小于 \(k\) 的子树内,以及其兄弟间(也就是在一棵很大的子树中,有几个比较小的儿子先合并了)。

第一部分的复杂度显然是 \(\leq nk\) 的(本质上是选了几个自身不超过 \(k\),和不超过 \(n\) 的变量求平方和,参考 \(n^2\) 的证明)

第二部分,每个点只会参与一次这个过程,因为参与一次之后它所在的块大小就大于 \(k\) 了,所以这一部分的总复杂度也是 \(O(nk)\)

综上所述,树形背包的复杂度为 \(O(nk)\)

思考

在上面的代码的边界做一些改动,哪些改动会让复杂度假掉。

CF1709 总结和题解

A

B

C

题意

有个合法括号序列,部分字符被 ? 替换了,问是否存在唯一的一种? 的方案,使得括号序列合法,即判断填 ? 使得括号序列合法的方案数是否等于1。存在唯一方案输出 YES,方案不唯一输出 NO

序列长度 \(\sum n\le 2\times 10^5\),测试点数 \(T\leq 5\times 10^4\)

第一行输出测试点总数 \(T\)

之后每一行一个字符串 \(s\) 表示替换掉部分字符后的合法括号序列

题解

考虑一个括号序列,令 ( 为 1,) 为 -1,记形成的新序列为 \(c\),然后对该序列做前缀和得到 \(s\),容易发现括号序列合法的充要条件是 \(s_n=0\)\(\forall i\in[1,n],s_i\ge0\)

已知给定的带 ? 的序列是一个合法序列变来的,所以考虑统计三种字符的个数,?,(,) 分别个数记为 cnt cnt0 cnt1,发现如果出现 cnt + min(cnt0,cnt1)=n/2 一定是 YES,因为每个 ? 的填法已经确定了。

因为一定要合法,所以 \(s_n\) 一定为 0,现在就需要让每一个 \(s_i\) 尽量大,方法就是把 ( 尽量往前面填,这样会让每个 \(s_i\) 都尽可能大。

然后我们考虑一种稍稍不那么优秀的方案,就是交换一组由两个 ? 构成的 () 序列,发现交换 () 交界处的那两个一定最优,所以求一下交界处的 min 看看是否大于 2 就行。

可以证明,如果这样交换不行,那么其它任何交换方案都不行。

我写麻烦了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include<bits/stdc++.h>
#define y1 y3647
#define INF 1000000000
#define LL long long
#define pii pair<int,int>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}
template<typename _type1,typename _type2>void cmin(_type1 &a,const _type2 b){if(a>b)a=b;}
template<typename _type1,typename _type2>void cmax(_type1 &a,const _type2 b){if(a<b)a=b;}
const int N=1e6+10;
int i,j,k,n,s,t,m,tp1,tp2;
char ch[N];
int c[N];
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// freopen(".in","w",stdout);
read(t);
while(t--){
scanf("%s",ch+1);
n=strlen(ch+1);int cnt=0,cnt0=0,cnt1=0;
for(i=1;i<=n;i++){
if(ch[i]=='?')cnt++;
if(ch[i]=='(')cnt0++;
if(ch[i]==')')cnt1++;
}
if(cnt<=1||cnt+min(cnt0,cnt1)==n/2){
puts("YES");
continue;
}
int flag=0,top=0,t1=0;
for(i=1;i<=n;i++){
if(ch[i]=='(')top++;
if(ch[i]==')')top--;
if(ch[i]=='?'&&cnt0!=n/2)top++,t1++,cnt0++;
else if(ch[i]=='?'&&cnt0==n/2)top--;
c[i]=top;
}
int lst=0,min_val=INF,all=0;
for(i=1;i<=n;i++){
if(ch[i]=='?'){
all++;
if(min_val>=2&&all==t1+1)flag=1;
lst=i;
min_val=INF;
}
cmin(min_val,c[i]);
}
if(flag)puts("NO");
else puts("YES");
}
return 0;
}


D

题意

有一个 \(n\times m\) 的网格,第 \(i(i\in[1,m])\) 列的 \([1,a_i]\) 行被锁定了。

你有个机器人,你可以给它发命令,让它向上下左右移动一格,但是机器人有 Bug,你发的每个命令都会被执行 \(k\)不是瞬移 \(k\) 格,而是走 \(k\) 次,每次一格。在任何一个时刻,机器人都不能处于被锁定的格子或者网格外。

给定 \(q\) 组询问,每组询问给定五个参数 \(x_s,y_s,x_f,y_f,k\),代表起点终点坐标和参数 \(k\),问能否从起点到终点,能输出 YES,不能输出 NO

第一行输入 \(n,m\)

第二行输入一个长度为 \(m\) 的数组表示 \(a\)

第三行一个整数 \(q\)

接下来 \(q\) 行每行 \(5\) 个整数 \(x_s,y_s,x_f,y_f,k\) 描述一个询问。

$ 1 n ^9 ; 1 m ^5 ; 1q ^5$

\(a[y_s] < x_s \le n ; 1 \le y_s \le m ; a[y_f] < x_f \le n; 1 \le y_f \le m; 1 \le k \le 10^9\)

题解

假设全部点都没有 \(lock\),考虑从 \((x_1,y_1)\) 能到 \((x_2,y_2)\) 的充要条件,显然是 \(k|abs(x_1-x_2)\) 并且 \(k|abs(y_1-y_2)\)

现在加入限制。允许 \([n-a_i,n]\) 走太麻烦了,翻转一下,变成允许 \([1,a_i']\) 走。

然后最优方案就是先走到当前的最底下,然后横着走过去。

所以找到最低能走到哪个位置,然后做个区间 min,判断最低位置 \(x\) 和区间 min 的关系,如果 \(x\le \min\),显然可以,否则不行。

区间 min 用 ST 表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include<bits/stdc++.h>
#define y1 y3647
#define INF 1000000000
#define LL long long
#define pii pair<int,int>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}
template<typename _type1,typename _type2>void cmin(_type1 &a,const _type2 b){if(a>b)a=b;}
template<typename _type1,typename _type2>void cmax(_type1 &a,const _type2 b){if(a<b)a=b;}
const int N=4e5+10;
int i,j,k,n,s,t,m,q;
int st[25][N],lo[N];
int ask(int l,int r){
if(l>r)swap(l,r);
int len=lo[r-l+1];
return min(st[len][l],st[len][r-(1<<len)+1]);
}
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// freopen(".in","w",stdout);
read(m),read(n);

for(i=2;i<=n;i++)lo[i]=lo[i>>1]+1;
for(i=1;i<=n;i++)
read(st[0][i]),st[0][i]=m-st[0][i];

for(i=1;i<=20;i++)
for(j=1;j+(1<<i)-1<=n;j++)
st[i][j]=min(st[i-1][j],st[i-1][j+(1<<i-1)]);
read(q);
for(i=1;i<=q;i++){
int x1,x2,y1,y2;
read(y1),read(x1),read(y2),read(x2),read(k);
if(k==0){
if(x1==x2&&y1==y2)puts("YES");
else puts("NO");
continue;
}
y1=m-y1+1,y2=m-y2+1;
if(abs(x1-x2)%k||abs(y1-y2)%k||ask(x1,x2)<(y1%k==0?k:y1%k))puts("NO");
else puts("YES");
}
return 0;
}


E

题意

你有一棵无根树,点数为 \(n\),每个点有个点权 \(a_u\),定义一条路径 \(P(u,v)\) 的权值为经过的所有点的点权的异或和。定义一棵树是合法的,当且仅当树上所有简单路径(只经过每个点一次的路径)的的权值都不为 \(0\)

你可以对权值进行修改,可以改成任意正整数,问最少修改多少次才能让这棵树合法。

输出最小修改次数。

\(n\leq 2\times 10^5,a_i\leq 2^{30}\)

题解

发现路径权值为 \(sum[u] \bigoplus sum[v] \bigoplus a[lca(u,v)]\)\(sum[u]\) 表示从 \(1\)\(u\) 的路径上所有点点权的异或和,每一条路径,我们在路径 \(lca\) 处考虑是否不合法(权值为 0)。

我们称一个点不合法当且仅当有一条 \(lca\) 为这个点的不合法路径经过它。我们从深到浅处理每一个不合法的 \(lca\)

如果一条路径不合法,修改方案可以分为改 \(lca\) 和非 \(lca\),可以说明改 \(lca\) 是最优的。首先,可以说明,改一个点 \(u\) 的最优方案之一是改成 \(2^{u+32}\),因为这样的话所有经过这个点的路径都一定合法。然后说明改 \(lca\) 最优。如果不改 \(lca\),以这颗子树中的点为起点的路径仍然有可能不合法,而改 \(lca\),以这颗子树中的点为起点的路径一定都合法。我们假设了当前处理的 \(lca\) 是最深的,所以这颗子树内所有的不合法路径一定经过了 \(lca\),所以改了 \(lca\),所有路径就一定合法了。所以\(lca\) 最优

怎么判断一个 \(lca\) 处有不合法路径,拿个 set 维护下每颗子树内的 \(sum\),然后加入一颗子树时,枚举加入子树的所有点 \(v\),判断当前 set 中是否存在元素 \(a_[lca]\bigoplus sum[v]\),如果存在,\(lca\) 就不合法,我们给答案加一,在这颗子树的父亲计算时忽略该子树。

发现合并 set 的代价是 \(O(\sum size_u)=O(n^2)\) 的,但是可以启发式合并。

具体的,一个点 \(u\) 继承它最大的儿子的 \(set\),然后在把它自己和其它子树加入到这个 set 中,同时判断是否存在不合法路径。考虑这样的复杂度,证明和树链剖分的复杂度证明相同,继承操作可以用 std::swap,这个函数交换两个 STL 容器的复杂度,在 C++11 以上标准是 \(O(1)\) 的。

对于一个点,我们考虑它什么时候会被合并一次(就是作为非最大子树,被合并上去)。每一个点所在的 set,被合并时,它所在的新 set 的大小至少变为 \(2\) 倍(因为新 set 最开始的大小一定比该点所在的 set 大,否则该点所在的 set 会作为最后保留的 set 而不对复杂度做贡献),最后的 set 大小为 \(n\),所以每个点合并 \(\log n\) 次。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include<bits/stdc++.h>
#define y1 y3647
#define INF 1000000000
#define LL long long
#define pii pair<int,int>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}
template<typename _type1,typename _type2>void cmin(_type1 &a,const _type2 b){if(a>b)a=b;}
template<typename _type1,typename _type2>void cmax(_type1 &a,const _type2 b){if(a<b)a=b;}
const int N=2e5+10;
int i,j,k,n,s,t,m,tp1,tp2,ans;
int fa[N],sz[N],son[N],a[N],sum[N];
vector<int> e[N];
set<int> st[N];
void pre_dfs(int u){
sz[u]=1;
for(auto v:e[u]){
if(fa[u]==v)continue;
fa[v]=u;sum[v]=sum[u]^a[v];
pre_dfs(v),sz[u]+=sz[v];
if(sz[v]>sz[son[u]])son[u]=v;
}
}
void redfs(int u,int flag=0){
if(son[u]){
redfs(son[u]);
swap(st[son[u]],st[u]);
}
if(st[u].find(sum[u]^a[u])!=st[u].end())flag=1;
st[u].insert(sum[u]);
for(auto v:e[u]){
if(v==son[u]||v==fa[u])continue;
redfs(v);
for(auto val:st[v]){
if(st[u].find(val^a[u])!=st[u].end())
flag=1;
}
for(auto val:st[v])
st[u].insert(val);
}
if(flag)ans++,st[u].clear();
}
signed main()
{
read(n);
for(i=1;i<=n;i++)read(a[i]);
sum[1]=a[1];
for(i=1;i<n;i++){
int x,y;read(x),read(y);
e[x].push_back(y),e[y].push_back(x);
}
pre_dfs(1);
redfs(1);

cout<<ans<<endl;
return 0;
}


F

题意

定义二进制串为只包含 \(01\) 的字符串。

给你 \(n,k,f\)\(1\le n\leq 15,1\le f,k\leq 2\times 10^5\),你需要给每个长度不超过 \(n\) 的二进制串 \(s\) 确定一个 \(c_s\in [0,k]\) 的权值。然后你需要选出一个只包含长度恰好\(n\) 的二进制串的可重集合,使得这个可重集合最大。并且满足对于所有长度小于 \(n\) 的二进制串 \(s\),集合元素中,\(s\) 作为集合中元素前缀的次数不超过 \(c_s\)

求安排 \(c\) ,使得集合最大大小恰好为 \(f\) 的方案数,对 \(998244353\) 取模。

题解

考虑取到最大值 \(f\) 的条件,不妨令 \(n=1\),发现要求就是 \(c_0+c_1=f\),然后我们考虑一些长度为 \(n\) 的二进制串。

因为要对每个长度小于 \(n\) 的串都确定一个 \(c\),然后我们又发现,记一个前缀 \(s\) 的数量限制为 \(w_s\),实际上 \(w_s=\min(c_s,w_{s+'0'}+w_{s+'1'})\)

然后我们就可以对一个确定的 \(c\) 序列,像树形 \(dp\) 一样算出最大大小。

考虑对这个树形 \(dp\) 的过程计数,本质上,由于树是满二叉树,所以 dp 出每个值的 \(c\) 序列的方案只于层数有关。

所以设 \(dp[i][j]\) 为考虑到深度为 \(n-i\) 的满二叉树,值为 \(j\) 的安排方案总数。

转移限制有两种,第一个是 \(c\) 限制,第二种是子树限制,考虑先算一个 \(sum[i][j]\) 表示子树和为 \(j\) 的方案数。

然后可以得到 \(dp[i][j]=sum[i][j]*(k-j+1)+\sum\limits_{t> j}sum[i][t]\)。分别表示两种限制的贡献。

算出 \(sum\) 后是 \(O(k)\) 转移的。考虑 \(sum[i][j]=\sum\limits_{s+t=j} dp[i-1][s]\times dp[i-1][j]\)。就是个卷积,NTT 板子上去就行。

注意边界,处理最顶层的时候,\(dp[n][i]\) 只能由子树和来限制,因为这个点本来就是长度为 0 的串,不能填 \(c\),我在这里想当然的认为只要 \(i\le k\) 的时候有值,所以没有 AK,实际上这里 \(k< i\le 2k\) 也是有值的,因为这个点本身不会限制。

如果 TLE 了换成 64bit 的机子再交一次,64 bit 的 long long 运算要快一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include<bits/stdc++.h>
#define y1 y3647
#define INF 1000000000
#define int long long
#define pii pair<int,int>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}
template<typename _type1,typename _type2>void cmin(_type1 &a,const _type2 b){if(a>b)a=b;}
template<typename _type1,typename _type2>void cmax(_type1 &a,const _type2 b){if(a<b)a=b;}
const int N=1<<19,mod=998244353;
namespace NTT{
const int N=1<<20,mod=998244353,g=3,gi=332748118;
int i,j,k,n,s,t,m;
int a[N],b[N],rk[N];
int quick(int a,int s,int ret=1){
while(s){
if(s&1)ret=ret*a%mod;
a=a*a%mod,s>>=1;
}
return ret;
}
void NTT(int *a,int type)
{
for(i=0;i<1<<s;i++)
if(rk[i]>i)swap(a[rk[i]],a[i]);
for(int len=1;len<=s;len++)
{
int w=1,wn=quick(g,mod-1>>len);
if(type==-1)wn=quick(wn,mod-2);
for(j=0;j+(1<<len)<=1<<s;j+=1<<len,w=1)
{
for(k=j;k<j+(1<<len-1);k++,w=1ll*w*wn%mod)
{
int x=a[k],y=a[k+(1<<len-1)];
a[k]=(x+1ll*w*y%mod)%mod,a[k+(1<<len-1)]=(x-1ll*w*y%mod)%mod;
}
}
}
}
void init(int nn,int c[],int res[]){
n=m=nn;
memset(a,0,sizeof(a)),memset(b,0,sizeof(b));
for(i=0;i<=n;i++)a[i]=c[i];
for(i=0;i<=m;i++)b[i]=c[i];
while(1<<s<=n+m)s++;

for(i=1;i<1<<s;i++)rk[i]=rk[i>>1]>>1|(i%2?1<<s-1:0);
NTT(a,1),NTT(b,1);
for(i=0;i<1<<s;i++)a[i]=1ll*a[i]*b[i]%mod;
NTT(a,-1);int inv=quick(1<<s,mod-2);
for(i=0;i<=n+m;i++)
res[i]=(1ll*a[i]*inv%mod+mod)%mod;
}
}
int i,j,k,n,s,t,m,tp1,tp2,f;
void Inc(int &a,int b){
a+=b;if(a>=mod)a-=mod;
}
int quick(int a,int s,int ret=1){
while(s){
if(s&1)ret=ret*a%mod;
a=a*a%mod,s>>=1;
}
return ret;
}

int dp[16][N<<1],sum[16][N<<1],p[N];
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// freopen(".in","w",stdout);
read(n),read(k),read(f);
for(i=0;i<=k;i++)dp[0][i]=1;
for(i=1;i<=n;i++){
NTT::init(k,dp[i-1],sum[i]);
for(j=0;j<=2*k;j++){
if(i!=n)dp[i][j]=sum[i][j]*(k-j+1)%mod;
else dp[i][j]=sum[i][j];
}
if(i!=n)for(j=2*k-1;j>=0&&i!=n;j--){
if(j<=k)Inc(dp[i][j],sum[i][j+1]);
Inc(sum[i][j],sum[i][j+1]);
}
}
cout<<(dp[n][f]+mod)%mod;
return 0;
}


CF1706 小总结

其实都会做,但是各种各样的原因导致了没在场上 AK

ABC

A 题没考虑清楚就在写,WA 了一发样例,然后慢悠悠调好交上去过。

B 题愣了一会儿,弄了个伪 DP 上去,稍微有点磨蹭,但还好。

C 题做得无可挑剔。

D

D 题先想到可以枚举一个端点,二分另一个,\(O(n)\) check,但是发现枚举一个端点之后,可以快速计算另一个端点的最优值,得到一个 \(O(n^2)\) 的枚举 max 的做法过了 D1,然后感觉其实只需要查 \([\lfloor\frac{\max a_i}{k}\rfloor,\lfloor\frac{\max a_i}{k}\rfloor+\sqrt N]\)\(\{\lfloor\frac{\max a_i}{x}\rfloor,x \le k\}\),写好交上去过了 Pretests,但是 FST 了。

后面想到其实对 min 分类限制更多。暴力算 \(\min \lfloor \frac{a_i}{k_i}\rfloor\in [1,B]\),然后对于 \(\min \lfloor \frac{a_i}{k_i}\rfloor\in(B,k]\),可以发现 \(p_i\in [1,\lceil\frac{10^5}{B}\rceil]\),考虑维护 max 的变化,可以用一个 vector 记一下 \(\lfloor\frac{a_i}{p_i}\rfloor\) 的所有可能,共 \(n\times \lceil\frac{10^5}{B}\rceil\) 种,因此对于 \((B,k]\)\(\lfloor\frac{a_i}{p_i}\rfloor\) 的变化总量是 \(O(n\sqrt n)\) 的。

换一种思路考虑,还是枚举 \(x\),令 \(\lfloor\frac{a_i}{p_i}\rfloor\le x\),易知 \(p_i\ge\lfloor\frac{a_i}{x+1}\rfloor+1\),考虑每一个 \(a_i\) 的最好的 \(p_i\),不太可行。但是发现上式中能取到的 \(p_i\) 满足 \(p_i\le\lfloor\frac{N}{x}\rfloor\) 。于是考虑每一个 \(p_i\) 可以让哪些 \(a_i\) 值取到最优,这是一个区间。而我们要考虑的是 min,所以取区间中最小的计算就行。

对于一个 \(x\),区间个数是 \(\frac{N}{x}\) 的,因此总复杂度 \(\sum\limits_{i=1}^N \frac{N}{i}=n\ln n\)

E

E 题在考场上想到了可以类似整体二分的做,先给所有的二分,然后再合并一次,查询每一个询问是否可行。查询的维护可以 dsu on tree 带一个维护线段的 set,复杂度 \(O(n\log^3n)\),感觉能信仰,就写了,可惜没交上去。赛后发现其实有些边界的小错,但是交了还是 TLE 了。告诉我们 set 操作搞个 \(10^7\) 问题还是比较大。

后面发现可以将询问直接塞进 dsu 的过程中,但是处理询问只能处理较小的那一边,如果是小的合到大的上面去,此时询问挂在大的上面,会非常的恼火。于是就把询问拆到两个上面去,又发现可能两个询问会在合并时变成一个但是还没解决,所以合并的时候如果变成一个就又找了一个还没合并的把询问扔上去。

然后又 TLE 了,赛后仔细分析复杂度,发现不对劲,比如有个 \(O(n)\) 的点,然后又有个略小于它但主要是询问的点,然后合并的时候把询问全部扔到另一个点上去了,然后另一个点同这个 \(O(n)\) 点合并的时候又把询问全部扔了,询问就扔了 \(O(n^2)\) 次,寄。

然后想到可以把线段合并的过程记下来,这个就是严格 \(O(n\log n)\) 级别了,后面做一个二维偏序回答询问。成功 AC。

其实 E 题和最近做的题都暴露出几个问题,思维模式僵化,没有办法 Think out of box,同时代码中常常犯一些不容易注意到的小错误,依赖数据调试。

Sum up

关于代码小错的问题,我觉得一边写一边静态查是一个比较理想的解决方案,先认真核实思路的正确性,再检查代码和思路是否一致,同时可以在关键点插入数据输出。

思维上的问题,估计还只有多做题。

收获 1:Dsu on tree 的复杂度理解

Dsu on tree,复杂度证明的过程是这样的:考虑一个点的权值 \(w_i\),当合并时,复杂度贡献为 \(O(\min(w_u,w_v)\),将 \(w_i\) 视为 \(w_i\) 个元素。考虑每个元素的贡献次数,发现每个元素仅在其所在集合大小翻倍时贡献。所以复杂度正确。

因此,权值的构成不影响其时间复杂度

如果在合并的时候进行了额外的对 \(w_i\) 的修改,比如我的错误做法,时间复杂度就应该认真分析了。

Kruscal 重构树的应用和连通性

E 题可以用 Kruscal 重构树做,具体的,先做最小生成树,顺便合并点,现在一个连通块就是一个点,合并两个连通块时,新建一个点,点权为这条边的权值,作为被合并的两个点的父亲,任意两个点联通的时最小权值就是它们的 lca,然后区间 lca 应该都会。事实上甚至没有必要做区间 lca,我们考虑 \(i,i+1\) 什么时候联通,设其时间为 \(f(i)\),可以发现 \(ans(a,b)=\max\limits_{i\in[a,b)} f(i)\),证明比较容易,首先必须取到这个值,否则必然有两个点不连通,其次,如果取到这个值,那么一定都联通,over ,复杂度 \(O(n\log n)\)

已经知道这个性质了,那么,将询问放到 dsu 合并过程中的方法也不难写了,合并时处理其中一个询问即可。

LCA 的一些性质

从 2 中的性质,我们总结出了一些 LCA 的共有性质。

设树上一个点集为 \(S\),定义 \(\operatorname{lca}(S)\) 为点集 \(S\) 中所有点的最近公共祖先。定义符号 \(\operatorname{lca}(a_1,a_2\cdots a_n)\) 表示 \(\operatorname{lca}(\{a_1,a_2,\cdots a_n\})\)

\(\text{Lemma 1.1}\)

\(\operatorname{lca}(S\cup u)=\operatorname{lca}(\operatorname{lca}(S),u)\)

\(\text{Proof 1.1}\)

\(\operatorname{lca}(S)\)\(u\) 的祖先,则结论显然成立。

否则,\(u\) 位于 \(\operatorname{lca}(S)\) 的子树外,\(\operatorname{lca}(S\cup u)\) 必须同时为 \(\operatorname{lca}(S)\)\(u\) 的祖先,故结论同样成立。

\(\text{Theorem 1.2}\)

对于 \(S=\{a_1,a_2\cdots,a_n\}\),满足 \(\operatorname{lca}(S)\in \bigcup\limits_{i=1}^{n-1}\operatorname{lca}(a_1,a_2)\)

\(\text{Proof 1.2}\)

\(S\) 集合大小归纳,设 \(|S|=n\),且对于 \(k\le n\),结论成立。

由 Lemma 1.1,有 \(\operatorname{lca}(S\cup u) = \operatorname{lca}(\operatorname{lca}(S),u)\)

  • 若满足 \(\operatorname{lca}(S\cup u)=\operatorname{lca}(S)\),对于 \(S\cup u\),即 \(|S'|=n+1\),结论成立。
  • 若不满足 \(\operatorname{lca}(S\cup u)=\operatorname{lca}(S)\),记 \(\operatorname{lca}(S)=w\),则有 \(\forall a_i\in S,\operatorname{lca}(a_i,u)=\operatorname{lca}(w,u)=\operatorname{lca}(S\cup u)\),因此对于 \(|S'|=n+1\),结论仍然成立。

证毕。

\(\text{Lemma 2.1}\)

\(\operatorname{dfn}(u)\) 表示 \(u\) 的 dfs 序。

对三个元素 \(u,v,w\) ,满足 \(\operatorname{dfn}(u)<\operatorname{dfn}(v)<\operatorname{dfn}(w)\),有 \(\operatorname{lca}(w,u,v)=\operatorname{lca}(u,w)\)

\(\text{Proof 2.1}\)

由 Theorem 1.2,\(\operatorname{lca}(u,v,w)\in\{\operatorname{lca}(u,v),\operatorname{lca}(u,w)\}\)

假设 \(\operatorname{lca}(u,v,w)=\operatorname{lca}(u,v)\) 并且 \(\operatorname{lca}(u,v)\neq \operatorname{lca}(u,w)\)

\(x=\operatorname{lca}(u,v,w)\)

因此 \(v\)\(\operatorname{lca}(u,w)\)\(x\) 的不同子树。又有 \(\operatorname{dfn}(v)>\operatorname{dfn}(u)\),所以 \(u\) 所在的 \(x\) 的子树先被访问,即 \(\operatorname{lca}(u,w)\) 所在的 \(x\) 的子树先被访问,因此 \(\operatorname{dfn}(w)<\operatorname{dfn}(v)\)。矛盾,故假设不成立。

证毕。

\(\text{Theorem 2.2}\)

对于序列 \(a\),若集合 \(A=\{a_1,a_2\cdots a_{n}\}\),且满足 \(\forall i\in [1,n),\operatorname{dfn}(a_{i+1})>\operatorname{dfn}(a_i)\),有 \(\operatorname{lca}(a_1,a_n)=\operatorname{lca}(A)\)

\(\text{Proof 2.2}\)

归纳证明,设对 \(k=n\) 成立。

由 Theorem 1.2, \(\operatorname{lca}(A\cup \{a_{n+1}\})=\operatorname{lca}(a_{n+1},\operatorname{lca}(A)\})\)

由 Lemma 2.1,Theorem 1.2,\(\operatorname{lca}(\operatorname{lca}(A),a_{n+1})\operatorname{lca}(\operatorname{lca}(a_1,a_{n}),a_{n+1})=\operatorname{lca}(a_1,a_{n},a_{n+1})=\operatorname{lca}(a_1,a_{n+1})\)

因此定理对 \(k=n+1\) 同样成立。

证毕。

\(\text{Theorem 2.3}\)

对于序列 \(a\),若集合 \(A=\{a_1,a_2\cdots a_{n}\}\),且满足 \(\forall i\in [1,n),\operatorname{dfn}(a_{i+1})>\operatorname{dfn}(a_i)\)

\(\operatorname{lca}(a_1,a_n)\in \bigcup\limits_{i=1}^{n-1}\{\operatorname{lca}(a_i,a_{i+1})\}\)

\(\text{Proof 2.3}\)

由 Theorem 2.1 ,\(\operatorname{lca}(a_1,a_n)=\operatorname{lca}(A)\)

由 Theorem 1.1,\(\operatorname{lca}(A) \in \{\operatorname{lca}(a_i,a_{i+1}),i\in[1,n)\}\)

0%