题目链接
题意简述
题目中说的比较清楚 , 这里再粘贴一遍 :
给定一棵 \(n\) 个点的树 . 你需要从 \(1\) 号点开始 , 不断地给树上的结点染色 . 在某一轮染色过程中 , 假设你是从编号为 \(u\) 的结点开始染色 , 现在染色到编号为 \(v\) 的结点 , 那么下面你只能尝试给编号为 \(v+1\) 的结点染色 . 如果存在一条树链 , 使得 \(u,u+1,\dots,v,v+1\) 都在这条树链上 , 那么编号为 \(v+1\) 的结点被成功染色 ; 否则 , \(v+1\) 在这一轮中不被染色 , 你只能开启新一轮的染色过程 , 并以 \(v+1\) 作为新一轮染色过程的起始结点 .
设直到结点 \(n\) 被染色 , 一共经历了 \(m\) 轮染色 . 你需要输出染色轮数 \(m\) .
题目分析
本题考察知识 :
对于前 \(30\%\) 的数据 , 考察树的基本知识 , 以及对树的 \(\rm dfs\) 算法 , 属于入门组难度 .
对于前 \(60\%\) 的数据 , 考察对树链性质的观察 , 属于普及组难度 .
对于 \(100\%\) 的数据 , 考察对树链性质的剖析 , 最近公共祖先和树上 \(\rm dfs\) 序的掌握 , 属于提高组难度 .
各档部分分解法
\((1):n=200\)
读完题目 , 你会发现按题意模拟就可以了 . 从 \(1\sim n\) 枚举每一个结点 , 假设当前枚举到 \(v\) (尝试对 \(v\) 染色) , 而这一轮染色的起始结点为 \(u\) . 那么我们尝试找到一条树链能够覆盖 \(u,u+1,\dots, v-1,v\) . 那么 , 你可以尝试 \(\mathcal O(n^2)\) 地枚举所有树链 , 然后判断它们是否都在树链上 , 只要存在一条即可 .
对于枚举树链 , 有一个较简单的做法 , 就是枚举 \(u=1\sim n\) 分别为根节点 , 然后做 \(n\) 遍 \(\rm dfs\) 即可 .
这样的复杂度是 \(\mathcal O(n^3)\) 的 . 当然还有针对该算法的优化 , 在某一轮染色中 , 起始结点是 \(u\) , 你可以二分 \(v=u+1\sim n\) , 来判断 \(u\sim v\) 是否被一条树链覆盖 . 这样的复杂度是 \(\mathcal O(mn^2\log n)\) 的 , 在 \(m\) 比较小的时候要比上面 \(\mathcal O(n^3)\) 的算法优秀 , 但是很可惜 , 出题人并没有特意给这种做法相应的部分分 . (因为 ... 太懒了)
\((2):n=2000\)
注意到 , 对于 \(u,u+1,\dots , v-1,v\) , 可能会有多条树链将其覆盖 . 但是一定会存在这样的树链 , 它的两端 \((x,y)\) 满足 \(u\leq x,y\leq v\) , 也就是 \(x,y\) 是 \(u\sim v\) 内的结点 . 并且这样的树链是唯一的 , 最短的 . 可以自己画画图 , 进行理解 . 为了方便叙述 , 我们称这样的链是覆盖链 .
假设初始染色结点为 \(u\) , 当前枚举到 \(v\) (尝试对 \(v\) 染色) , \(u\sim v-1\) 的覆盖链为 \((x,y)\) . 那么 , 如果存在新的对 \(u\sim v\) 的覆盖链 , 其中一个端点一定为 \(x\) 或者 \(y\) 或者 \(v\) , 分别枚举 , 判断并更新新的覆盖链即可 . 这样 , 一次判断就被优化到了 \(\mathcal O(n)\) . 总复杂度为 \(\mathcal O(n^2)\) , 可以通过该部分分 .
\((3):n=200000\)
前排提示 , 出题人造了菊花树(树的平均度数很大) , 链图(树的深度很大) , 蒲公英(树的平均度数和深度都较大) 的数据 , 应该能把暴力卡掉 (希望吧 QAQ) , 如果你使用关于深度和度数的暴力程序拿到了满分 , 那说明 , 您太强了 !
前置知识为最近公共祖先和树上 \(\rm dfs\) 序 , 不会的同学可以直接问 HDM 什么时候教 , 或者找找蓝书(算法竞赛进阶指南) , 或者查看 oi-wiki 上的相关知识 .
\(\rm dfs\) 序在本题的应用为 , 判断两个结点 \((u,v)\) , \(v\) 是否在 \(u\) 的子树内 .
我们以任意一个结点为根节点 , 做好求 lca 的预处理和 dfs 序的预处理 ; 对于覆盖链 , 我们维护其两个端点和两个端点的 lca : \((u,v,lca)\) .
覆盖链有三种情况 : \((1):u=v\) , 覆盖链是一个点 ; \((2):u\) 是 \(v\) 的祖先 , 即从 \(v\) 一直往父亲走可以走到另一个端点 \(u\) ; \((3):u\) 和 \(v\) 的最近公共祖先 \(lca\neq u,\neq v\) , 也就是从 \(v\) 沿着父亲走到 \(lca\) , 再从 \(lca\) 向儿子走才能走到 \(u\) . 我们分别维护这三种情况的覆盖链 , 并且在 \((2)\) 情况时 , 为了方便 , 我们令 \(u\) 是 \(u,v\) 中深度更小的那个结点 .
当前覆盖链是 \((1)\) 情况时 :
考虑新枚举的结点 \(x\) . 显然新的覆盖链的两个端点为 \(u\) 和 \(x\) . 只需要考虑 \(u\) 和 \(x\) 的位置关系即可 .
当前覆盖链是 \((2)\) 情况时 :
如果 \(x\) 在 \(u\) 的子树之外 , 那么新的覆盖链显然为 \((v,x,\mathrm{lca}(v,x))\) . 如果 \(x\) 在 \(u\) 的子树之内 , 有如下情况 :
\(1.\ x\) 在 \(v\) 的子树之内 , 可以形成新的覆盖链 ;
\(2.\ x\) 不在 \(v\) 的子树之内 , 但是 \(v\) 在 \(x\) 的子树之内 , 说明 \(x\) 在 \(u\rightarrow v\) 这一条树链上 ;
\(3.\ x\) 不在 \(v\) 的子树之内 , 但是 \(\mathrm{lca}(v,x)=u\) , 说明可以形成 \(v\rightarrow u\rightarrow x\) 的新覆盖链 ;
对于其他的情况 , 不能形成新的覆盖链了 .
当前覆盖链是 \((3)\) 情况时 :
这部分就比较简单了 , 尝试自己想想 ? 不会的话 , 可以看标程是如何实现的 .
参考程序
题解使用的是树链剖分求 \(\rm lca\) , 这可能会促使你去看一看树链剖分的内容 (提示 , 提高组可能不考) . 最一般的方法是倍增求 \(\rm lca\) .
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=202020,M=404040;
int n,m;
int val[N];
int head[N],ver[M],nxt[M],num,tot=1;
int dfn[N],In[N],Out[N],cnt,fa[N],son[N],dep[N],top[N],sz[N];
void add(int x,int y) {
ver[++tot]=y,nxt[tot]=head[x],head[x]=tot;
}
void dfs1(int x,int father) {
fa[x]=father,sz[x]=1;
for(int i=head[x];i;i=nxt[i]) {
int y=ver[i];
if(y == father) continue;
dep[y]=dep[x]+1,dfs1(y,x),sz[x]+=sz[y];
if(sz[y] >= sz[son[x]])
son[x]=y;
}
}
void dfs2(int x,int apex) {
top[x]=apex,dfn[x]=++cnt,In[x]=cnt;
if(son[x]) dfs2(son[x],apex);
for(int i=head[x];i;i=nxt[i]) {
int y=ver[i];
if(y == fa[x] or y == son[x]) continue;
dfs2(y,y);
}
Out[x]=cnt;
}
int Lca(int x,int y) {
while(top[x] != top[y]) dep[top[x]] < dep[top[y]] ? (swap(x,y) , x=fa[top[x]]) : x=fa[top[x]] ;
return dep[x] < dep[y] ? x : y;
}
bool Insubtree(int root,int x) {return In[root] <= dfn[x] and dfn[x] <= Out[root];}//Is x in subtree(root) ?
void solve() {
int u=1,v=1,lca=1;
bool ischain=1;
m=1;
for(int i=2;i<=n;++i) {
int x=i;
if(u == v) {
if(Insubtree(u,x))
v=x,lca=u;
else if(Insubtree(x,u))
u=x,lca=x;
else
lca=Lca(u,x),v=x,ischain=0;
}
else if(ischain == 1) {
if(Insubtree(u,x)) { // x in subtree(u) , u -> x
if(Insubtree(x,v)) // v in subtree(x) , u -> x -> v
;
else if(Insubtree(v,x)) // x in subtree(v) , u -> v -> x
v=x;
else if(Lca(v,x) == u) // x in the other subtree of u , x -> u -> v
u=x,ischain=0;
else //over !
++m,u=v=lca=x,ischain=1;
}
else if(Insubtree(x,u))// u in subtree(x), x -> u -> v
u=x,lca=x;
else {
// x if not only not the ancestor of u but also not the subpoint of u
lca=Lca(u,x),u=x,ischain=0;
}
}
else {
if(Insubtree(lca,x)) { // x in subtree(lca) , lca -> x
if(Insubtree(x,v)) // v in subtree(x) , lca -> x -> v
;
else if(Insubtree(x,u)) // u in subtree(x) , lca -> x -> u
;
else if(Insubtree(v,x)) // x in subtree(v) , u -> lca -> v -> x
v=x;
else if(Insubtree(u,x)) // x in subtree(u) , v -> lca -> u -> x
u=x;
else //over !
++m,u=v=lca=x,ischain=1;
}
else // over !
++m,u=v=lca=x,ischain=1;
}
}
}
int main() {
scanf("%d",&n);
for(int i=1,u,v;i<n;++i) {
scanf("%d %d",&u,&v);
add(u,v),add(v,u);
}
dfs1(1,0);
dfs2(1,1);
solve();
printf("%d\n",m);
return 0;
}