树的基础常识学习指南
典题合集
树上路径长度为k的点对 | 树上DP | 点分治 |
---|---|---|
前置芝士
树的性质
一个有n个节点的树,一定有n-1条边。
深度优先搜索
邻接表存图+记录深度
const int N=100010;
vector<int> G[N];
int n,dis[N];
void dfs(int u,int f){
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
if(v==f) continue;
dis[v]=dis[u]+1;
dfs(v,u);
}
}
广度优先搜索
邻接表存图+记录深度由于广度优先遍历并不是依照父节点直接跳到子节点遍历的,所以需要开一个fa数组来记录每个节点的父节点是谁。
const int N=100010;
vector<int> g[N];
int n,dis[N],fa[N];
queue<int> q;
void bfs(int u){
q.push(u);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
if(v==fa[u]) continue;
fa[v]=u;
dis[v]=dis[u]+1;
q.push(v);
}
}
}
连通点集
对每个点来说,以该点为根,向外延伸,能形成多少棵子树。自己一个点也算一棵子树。
[连通点集的数量]
首先考虑树形dp求一个根的答案。dp1[u]记为以u为根,和他的儿子以及后代能形成的所有子树数。u的所有儿子v的dp1[v]+1的乘积。
void dfs1(int x,int fa){
for(int son:to[x]){
if(son==fa) continue;
dfs1(son,x);
dp1[x]=(dp1[x]*(dp1[son]+1))%mod;
}
}
树的直径
[定义]
树上任意两节点之间最长的简单路径即为树的「直径」。
[性质]
(1)直径两端点一定是两个叶子节点。
(2)距离任意点最远的点一定是直径的一个端点,这个基于贪心求直径方法的正确性可以得出。
(3)如果第一棵树直径两端点为(u,v),第二棵树直径两端点为(x,y),用一条边将两棵树连接,那么新树的直径一定是u,v,x,y中的两个点。
(4)对于一棵树,如果在一个点的上接一个叶子节点,那么最多会改变直径的一个端点。
(5)若树上所有边边权均为正,则树的所有直径中点重合。
树上DP解法
我们记录当1为树的根时,每个节点作为子树的根向下,所能延伸的最长路径长度d1与次长路径(与最长路径无公共边)长度d2,那么直径就是对于每一个点,该点d1 + d2能取到的值中的最大值。
树形 DP 可以在存在负权边的情况下求解出树的直径。
const int N=1e4+10;
int n,d=0;//n个节点,d为直径
int d1[N],d2[N];
vector<int> e[N];
void dfs(int u, int fa) {
d1[u] = d2[u] = 0;
for (int v : e[u]) {
if (v == fa) continue;
dfs(v, u);
int t = d1[v] + 1;
if (t > d1[u]) {
d2[u] = d1[u], d1[u] = t;
} else if (t > d2[u]) {
d2[u] = t;
}
}
d = max(d, d1[u] + d2[u]);
}
void solve(){
cin>>n;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1,0);
//pase
}
两次DFS解法
const int N=1e4+10;
int n,c=0;//n个节点,c一直为可能成为直径的端点
int d[N];
vector<int> e[N];
void dfs(int x,int fa){
for(int v:e[x]){
if(v==fa) continue;
d[v]=d[x]+1;
if(d[v]>d[c]) c=v;
dfs(v,x);
}
}
void solve(){
cin>>n;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1,0);
d[c]=0,dfs(c,0);
//pase
}
树的重心
[定义]
对于树上的每一个点,计算其所有子树中最大的子树节点数,这个值最小的点就是这棵树的重心。
(这里以及下文中的「子树」都是指无根树的子树,即包括「向上」的那棵子树,并且不包括整棵树自身。)
[性质]
(1)树的重心如果不唯DFS则至多有两个,且这两个重心相邻。
(2)以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
(3)树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。
(4)把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树的重心的路径上。
(5)在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
DFS解法
const int N=1e5+10;
const int M=2e5+10;
//sz:树的大小,w:无根树的最大子树,
// cid:中心节点,最多为2个
int sz[N],w[N],cid[2];
int h[N],e[M],ne[N],idx;//链式前向星
int n;
void add(int u,int v){
e[++idx]=v,ne[idx]=h[u],h[u]=idx;
}
void dfs(int x,int fa){
sz[x]=1;
w[x]=0;
for(int i=h[x];i!=-1;i=ne[i]){
int v=e[i];
if(v!=fa){
dfs(v,x);
sz[x]+=sz[v];
w[x]=max(w[x],sz[v]);}
}
w[x]=max(w[x],n-sz[x]);
if(w[x]<=n/2){
cid[cid[0]!=0]=x;
}
}
Distance in Tree
[problem description]
输入点数为\(N\)一棵树
求树上长度恰好为\(K\)的路径个数
[input]
第一行两个数字\(N,K\),如题意
接下来的\(N-1\)行中,每行两个整数\(u,v\)表示一条树边\((u,v)\)
[output]
一个整数\(ans\),如题意
在\(Codeforces\)上提交时记得用\(\%I64d\)哦.QwQ
[datas]
\(1 \leq n \leq 50000\)
\(1 \leq k \leq 500\)
[a.in]
5 2
1 2
2 3
3 4
2 5
[a.out]
4
In the first sample the pairs of vertexes at distance 2 from each other are (1, 3), (1, 5), (3, 5) and (2, 4).
[solved]
[树上DP]
const int N=50010;
struct edge{
int v,ne;
}e[N*2];
int h[N],idx;
int f[N][510];
int n,m;
ll res=0;
void add(int u,int v){
e[++idx]={v,h[u]};
h[u]=idx;
}
void dfs(int u,int fa){
f[u][0]=1;
for(int i=h[u];i;i=e[i].ne){
int v=e[i].v;
if(v==fa) continue;
dfs(v,u);
for(int j=0;j<m;j++){
res+=f[v][j]*f[u][m-j-1];
}
for(int j=0;j<m;j++){
f[u][j+1]+=f[v][j];
}
}
}
void solve() {
//freopen("a.in","r",stdin);
//freopen("a.out","w",stdout);
cin>>n>>m;
for(int i=1,x,y;i<n;i++){
cin>>x>>y;
add(x,y);
add(y,x);
}
dfs(1,0);
cout<<res<<endl;
}