这一部分的内容可以说是最杂的
只能给出一些典型题和简单的知识讲解
我们从简单的开始吧
树的重心
点分治的必要操作,难度:☆
void findroot(int now,int fa) {
f[now]=0;
size[now]=1;
for (int i=st[now];i;i=way[i].nxt)
if (way[i].y!=fa&&!vis[way[i].y]) {
findroot(way[i].y,now);
size[now]+=size[way[i].y];
f[now]=max(f[now],size[way[i].y]);
}
f[now]=max(f[now],sz-size[now]);
if (f[now]<f[root]) root=now;
}
树的直径
难度:☆~☆☆
经典例题:树的直径
喜闻乐见,树的直径有两种求法
方法一:两遍dfs
int pre[N],ans,nowx;
void dfs(int now,int fa,int dis) {
if (dis>ans) {
ans=dis; //记录最长链
nowx=now;
}
for (int i=st[now];i;i=way[i].nxt)
if (way[i].y!=fa) {
pre[way[i].y]=i; //记录路径
dfs(way[i].y,now,dis+way[i].v);
}
}
void solve() {
ans=0,nowx=0;
memset(pre,-1,sizeof(pre));
dfs(1,0,0);
memset(pre,-1,sizeof(pre));
dfs(nowx,0,0);
}
方法二:dp
f[i]
f
[
i
]
表示最长链
g[i]
g
[
i
]
表示次长链
每次用儿子的
f[son]+w(fa,i)
f
[
s
o
n
]
+
w
(
f
a
,
i
)
更新
f[i],g[i]
f
[
i
]
,
g
[
i
]
这样一个儿子最多只会对一个值产生影响(不是
f[i]
f
[
i
]
,就是
g[i]
g
[
i
]
)
因此我们可以保证
f[i],g[i]
f
[
i
]
,
g
[
i
]
代表的链在不同子树中
最后答案:
f[root]+g[root]
f
[
r
o
o
t
]
+
g
[
r
o
o
t
]
int f[N],g[N];
void dfs(int now,int fa) {
f[now]=0,g[now]=0;
for (int i=st[now];i;i=way[i].nxt)
if (way[i].y!=fa) {
dfs(way[i].y,now);
int len=f[way[i].y]+way[i].v;
if (len>f[now]) {
g[now]=f[now];
f[now]=len;
}
else g[now]=len;
}
}
dsu on tree
难度:☆☆☆
dsu on tree讲解(yveh学长压箱底的宝贝)
经典例题:dsu on tree(子树中数目最多的颜色的权值和)
很惭愧,这种算法学的不是很好
主体思路就是轻重链剖分
但是没有那么繁琐了,不用维护太多东西
首先找到每个结点的重儿子
之后再进行一遍
dfs
d
f
s
dfs d f s 流程:
dfs d f s 轻儿子
dfs d f s 重儿子
把儿子的信息记录到父亲
son=0 s o n = 0
记录答案
一键消除轻儿子影响
认真看了一下,发现很多题可以用树上莫队瞎搞。。。
树分块
难度:☆☆~☆☆☆
怎么说呢,只搞过这一道树分块,所以没什么资格聊这个问题
不过感觉就是考虑子树
树形dp
难度:☆☆☆~☆☆☆☆
下面给出的只是一部分
枚举是种好方法
经典例题:树形结构(路径的权值为权值的最大公约数)
有时候,我们在状态中不需要记录当前结点的状态,而需要记录父节点的状态
一般,根结点需要特判一下
转移的时候看准了状态的定义
经典例题:树形dp+贪心
经典例题:树形dp(服务器)
经典例题:树形dp+双元限制
经典例题:树形dp(三色二叉树)
经典例题:树形dp(时态同步)
经典例题:树形dp(树上染色,求黑白点对的距离)
有时候我们考虑每一条边的贡献,这样反而会简单一点
树形dp经常和背包结合,基本套路(注意当前背包容量从大到小枚举):
//当前结点是now
for (循环now的所有儿子)
for (i=M;i>=0;i--) //M是背包容量,我们要转移f[now][i]
for (j=0;j<=M;j++) //j是当前儿子中要占据多少容量
f[now][i]=max(f[now][i],f[now][i-j]+f[son][j]);
树上的路径我们经常拆成
(u−>lca)+(lca−>v)
(
u
−
>
l
c
a
)
+
(
l
c
a
−
>
v
)
两条
期望什么的,式子最重要
有些题两遍
dfs
d
f
s
就可以解决(第一遍
dfs
d
f
s
处理子树信息,第二遍
dfs
d
f
s
维护答案)
谨记期望的计算公式:
一般情况下,我们在转移期望的时候需要之前的期望,所以只有当前一步的花费是一个常量
经典例题:树形dp+期望概率(一)
经典例题:树形dp+期望概率(二)
经典例题:树形dp+期望概率(三)
Kruskal重构树
难度:☆☆☆☆
例题太经典了,绝对展现了Kruskal重构树的优势
Kruskal重构树
经典例题:Kruskal重构树+主席树
虚树
难度:☆☆☆☆☆
有这样一类问题:给出一棵
n
n
个结点的树,每次指定个结点,给予ta们一些性质,求出某答案,保证
∑m
∑
m
与
n
n
同阶
显然我们需要基于的算法
虚树实际上是对树的一种简化
一般题目会给出一些关键点
你会发现,那些不是关键点的结点无关紧要
而且我们可以把树上的路径压缩(比如说我们只关心权值和,最大值,最小值)
那么我们就可以考虑建立虚树,只有关键点以及任意两个关键点之间的LCA在虚树上
之后再在这棵虚树上 dp d p 啊, dfs d f s 啊,该干嘛就干嘛
虚树的构建比较重要
主要就是用一个单调栈维护结点(保证栈中的结点深度单调不降)
每产生一个新结点(不管是LCA还是关键点),我们都要塞入栈中
连边的话,深度小的向深度大的结点连边即可
const int N=100010;
int n,m;
struct node{
int y,v,nxt;
}way[N];
int st[N],tot=0,dfn[N],clo;
int pre[N][20],deep[N],dis[N][20];
int a[N],top,S[N];
void dfs(int now,int fa,int dep) {
deep[now]=dep;
pre[now][0]=fa;
dfn[now]=++clo;
for (int i=1;i<20;i++) {
if ((1<<i)>dep) break;
pre[now][i]=pre[pre[now][i-1]][i-1];
dis[now][i]=dis[now][i-1]+dis[pre[now][i-1]][i-1];
}
for (int i=st[now];i;i=way[i].nxt)
if (way[i].y!=fa) {
dis[way[i].y][0]=way[i].v;
dfs(way[i].y,now,dep+1);
}
}
int LCA(int x,int y) {
if (deep[x]<deep[y]) swap(x,y);
int d=deep[x]-deep[y];
if (d)
for (int i=0;i<20&&d;i++,d>>=1)
if (d&1)
x=pre[x][i];
if (x==y) return x;
for (int i=19;i>=0;i--)
if (pre[x][i]!=pre[y][i]) {
x=pre[x][i];
y=pre[y][i];
}
return pre[x][0];
}
void prepare() {
clo=0;
dfs(1,0,1);
}
int getlen(int x,int y) {
int sum=0;
if (deep[x]<deep[y]) swap(x,y);
int d=deep[x]-deep[y];
if (d)
for (int i=0;i<20&&d;i++,d>>=1)
if (d&1) {
x=pre[x][i];
sum+=dis[x][i];
}
if (x==y) return sum;
for (int i=19;i>=0;i--)
if (pre[x][i]!=pre[y][i]) {
sum+=dis[x][i];
x=pre[x][i];
sum+=dis[y][i];
y=pre[y][i];
}
sum+=dis[x][0];
sum+=dis[y][0];
return sum;
}
void build(int u,int w) {
if (u==w) return;
tot++;
way[tot].y=w;way[tot].nxt=st[u];st[u]=tot;
way[tot].v=getlen(u,w);
}
int cmp(const int &x,const int &y) {
return dfn[x]<dfn[y]; //按照dfs序排序
}
void solve() {
int m;
for (int i=1;i<=m;i++)
scanf("%d",&a[i]); //读入关键点
sort(a+1,a+1+m,cmp);
// int cnt=0; 我觉得这一部分很不靠谱
// a[++cnt]=a[1];
// for (int i=2;i<=m;i++)
// if (lca(a[i],a[cnt])!=a[cnt]) a[++cnt]=a[i];
// m=cnt;
tot=0; //边
memset(st,0,sizeof(st));
top=0;
S[++top]=1; //默认根结点
for (int i=1;i<=m;i++) {
int now=a[i];
int p=LCA(S[top],now);
while (1) {
if (deep[p]>=deep[S[top-1]]) {
build(p,S[top--]);
if (S[top]!=p) S[++top]=p;
break;
}
build(S[top-1],S[top]);
top--;
}
if (now!=S[top]) S[++top]=now;
}
while (top-1) build(S[top-1],S[top]),top--;
}