树基础
无根树
图论中的树和现实生活中的树长得一样,只不过我们习惯于处理问题的时候把树根放到上方来考虑。这种数据结构看起来像是一个倒挂的树,因此得名
一个没有固定根结点的树称为 无根树 。无根树有几种等价的形式化定义:
有 n 个结点,n-1 条边的连通无向图
无向无环的连通图
任意两个结点之间有且仅有一条简单路径的无向图
任何边均为桥的连通图
没有圈,且在任意不同两点间添加一条边之后所得图含唯一的一个圈的图
连通分量中经常出现割点和桥 的定义
在无根树的基础上,指定一个结点称为 根 ,则形成一棵 有根树
注意:一般有根树在很多时候仍以无向图表示,只规定了结点之间的上下级关系,无根树就是节点之间正反关系都存
树的定义
适用于无根树和有根树
森林 :每个连通分量(连通块)都是树的图。按照定义,一棵树也是森林
生成树 :一个连通无向图的生成子图,同时要求是树。也即在图的边集中选择 n-1 条,将所有顶点连通
结点的深度 :到根结点的路径上的边数
树的高度 :所有结点的深度的最大值
无根树的叶结点 :度数不超过 1 的结点
因为无根树根没有确定,所以度数不超过 1 的结点都有可能是叶节点
只适用于有根树
父亲 :对于除根以外的每个结点,定义为从该结点到根路径上的第二个结点。 根结点没有父结点
祖先 :一个结点到根结点的路径上,除了它本身外的结点。 根结点的祖先集合为空
子结点 :如果 u
是 v
的父亲,那么 v
是 u
的子结点。子结点的顺序一般不加以区分,二叉树是一个例外
兄弟 :同一个父亲的多个子结点互为兄弟
后代 :子结点和子结点的后代
或者理解成:如果 u
是 v
的祖先,那么 v
是 u
的后代
子树 :删掉与父亲相连的边后,该结点所在的子图
特殊的树
链 :满足与任一结点相连的边不超过 1 条的树称为链
菊花/星星 :满足存在 u
使得所有除 u
以外结点均与u
相连的树称为菊花
有根二叉树 :每个结点最多只有两个儿子(子结点)的有根树称为二叉树。常常对两个子结点的顺序加以区分,分别称之为左子结点和右子结点,大多数情况下, 二叉树 一词均指有根二叉树
完整二叉树 :每个结点的子结点数量均为 0 或者 2 的二叉树。换言之,每个结点要么是树叶,要么左右子树均非空
完全二叉树 :只有最下面两层结点的度数可以小于 2 ,且最下面一层的结点都集中在该层最左边的连续位置上
完美二叉树 :所有叶结点的深度均相同的二叉树称为完美二叉树,又称满二叉树
存储方法
只记录父节点
只用一个 fa[]
来记录每个节点的父亲节点——获取信息较少不便于进行自顶向下的遍历,常用于自底向上的递推问题
邻接表
对于无根树就给每一个节点开一个 vector
记录与其相连的所有点
对于有根树为了确保上下关系所以每个节点的 vector
只记录与其相连的子节点,为了方便遍历还可以另开一个数组用来给每个节点记录其父节点
1 2 vector <int > child[N];int fa[N];
左孩子右兄弟表示法
对于有根树,存在一种简单的表示方法。首先,给每个结点的所有子结点任意确定一个顺序。后为每个结点记录两个值:其 第一个子结点 child[u]
和其 下一个兄弟结点 sib[u]
。若没有子结点,则 child[u]
为空;若该结点是其父结点的最后一个子结点,则 sib[u]
为空。
遍历一个结点的所有子结点可由如下方式实现:
1 2 3 4 5 6 7 int v = child[u];while (v != EMPTY_NODE){ v = sib[v]; }
树上DFS
以有根二叉树为例
先序遍历
1 2 3 4 5 6 7 void PreOrderTraverse (BiTree T) { if (T==NULL )return ; printf ("%c" , T->data); PreOrderTraverse(T->lchild); PreOrderTraverse(T->rchild); }
中序遍历
1 2 3 4 5 6 7 void InOrderTraverse (BiTree T) { if (T==NULL )return ; InOrderTraverse(T->lchild); printf ("%c" , T->data); InOrderTraverse(T->rchild); }
后序遍历
1 2 3 4 5 6 7 void InOrderTraverse (BiTree T) { if (T==NULL )return ; InOrderTraverse(T->lchild); InOrderTraverse(T->rchild); printf ("%c" , T->data); }
层序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void LevelOrder (TreeNode *root) { std ::queue <TreeNode *> q; TreeNode *front; if (root==NULL )return ; q.push(root); while (!q.empty()){ front=q.front(); q.pop(); if (front->left) q.push(front->left); if (front->right) q.push(front->right); printf ("%c " ,front->data); } }
树上BFS
无根树
树的遍历一般为深度优先遍历,这个过程中最需要注意的是避免重复访问结点
由于树是无环图,因此只需记录当前结点是由哪个结点访问而来,此后进入除该结点外的所有相邻结点,即可避免重复访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void dfs (int u,int from) { for (int v:vec[u]) if (v!=from){ dfs(v, u); } } signed main () { int EMPTY_NODE=0 ; int root=1 ; dfs(root,EMPTY_NODE); }
有根树
因为每个节点的vector
存的就是子节点不存在重复访问的问题所以直接遍历节点的vector
就可以了
DFS序
1 2 3 4 5 6 7 8 int dfs_order[maxn],tot=0 ;vector <int > G[maxn];void dfs (int x) { dfs_order[tot++]=x; for (auto u:G[x]) dfs(i); }
这个代码只是给出一种DFS序遍历树的形式,做题要具体问题具体分析需要在看懂了这几行代码的基础上自己写DFS
欧拉序
从根节点出发,按照DFS顺序并绕回根节点的顺序(即回溯再次经过的点也要记录)输出节点:1 2 4 7 7 8 8 4 2 3 5 9 9 5 6 6 3 1
欧拉序的主要作用体现下文在树链剖分中
最近公共祖先( LCA
)
最近公共祖先简称 LCA 。两个节点的最近公共祖先就是这两个点的公共祖先里面离根最远的那个。 为了方便,我们记某 S=v1 ,v2 ,…,vn 点集的最近公共祖先为 LCA(v1 ,v2 ,…,vn ) 或 LCA(S)
性质
LCA(u)=u
u
是 v
的祖先,当且仅当 LCA(u,v)=u
如果 u
不为 v
的祖先并且 v
不为u
的祖先,那么 u
,v
分别处于 LCA(u,v)
的两颗不同子树中
前序遍历中,LCA(S)
出现在所有 S
中元素之前,后序遍历中 LCA(S)
则出现在所有S中元素之后
两点集并的最近公共祖先的最近公共祖先:LCA(AUB)=LCA(LCA(A),LCA(B))
两点的最近公共祖先必定处在树上两点间的最短路上
length(u,v)=dis(u)+dis(v)-2*dis(LCA(u,v))
,其中 length
是树上两点间的距离,dis
代表某点到树根的距离
朴素算法
可以每次找深度比较大的那个点,让它向上跳。显然在树上,这两个点最后一定会相遇,相遇的位置就是想要求的 LCA
。 或者先向上调整深度较大的点,令他们深度相同,然后再共同向上跳转,最后也一定会相遇。
复杂度分析
朴素算法预处理时需要 DFS 整棵树,时间复杂度为 O(n) ,单次查询时间复杂度为 O(n) 。但由于随机树高为 O(logn) ,所以朴素算法在随机树上的单次查询时间复杂度为 O(logn)
倍增算法
倍增算法是最经典的 LCA 求法,他是朴素算法的改进算法
通过预处理pre[x][i]
数组,游标可以快速移动,大幅减少了游标跳转次数。pre[x][i]
表示点 x
的第 2i 个祖先。 pre[x][i]
数组可以通过 DFS 预处理出来
现在我们看看如何优化这些跳转:在调整游标的第一阶段中,我们可以计算出 u
,v
两点的深度之差,设其为y
。通过将y
进行二进制拆分,我们将 y
次游标跳转优化为 count_one_in_binary_representation(y)
次游标跳转。 在第二阶段中,我们从最大的 i
开始循环尝试,一直尝试到 0(包括 0 ),如果 pre[u][i]!=pre[v][i]
,则令 u=pre[u][i];v=pre[v][i]
,那么最后的 LCA 为 pre[u][0]
复杂度分析
倍增算法的预处理时间复杂度为 O(nlogn),单次查询时间复杂度为 O(logn) 。 另外倍增算法可以通过交换 pre
数组的两维使较小维放在前面。这样可以减少 cache miss
次数,提高程序效率
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 #include <bits/stdc++.h> using namespace std ;const int maxn=4e4 +5 ;struct Edge { int to,next,val; }e[maxn<<1 ]; int head[maxn],tot;int t,n,m;int dep[maxn],pre[maxn][21 ];void init () { tot=0 ; memset (head,-1 ,sizeof head); } void add (int x,int y,int val) { e[tot].to=y,e[tot].val=val,e[tot].next=head[x],head[x]=tot++; } void dfs (int x,int from) { dep[x]=dep[from]+1 ; pre[x][0 ]=from; for (int i=1 ;(1 <<i)<=dep[x];i++){ pre[x][i]=pre[pre[x][i-1 ]][i-1 ]; } for (int i=head[x];~i;i=e[i].next){ int y=e[i].to; if (y!=from) { dfs(y,x); } } } int lca (int x,int y) { if (dep[x]<dep[y])swap(x, y); for (int i=19 ;i>= 0 ;i--) { if (dep[x]-(1 <<i)>=dep[y]){ 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 ]; } signed main () { int t;cin >>t; while (t--){ init(); cin >>n>>m; for (int i=1 ;i<n;i++){ int x,y,val; cin >>x>>y>>val; add(x,y,val),add(y,x,val); } dfs(1 ,0 ); int x,y,node; for (int i=1 ;i<=m;i++){ cin >>x>>y; node=lca(x,y); cout <<node<<endl ; } } return 0 ; }
Tarjan算法
参考博客 详细讲解演示看这篇转载的博客讲的炒鸡好
Tarjan算法是一种离线算法 ,需要使用并查集 记录某个结点的祖先结点。做法如下:
首先接受输入(链式前向星)、查询(存储另一个数组)即离线操作
然后对其进行一次 DFS 遍历,同时使用 vis
数组进行记录某个结点是否被访问过、fa
记录当前结点的父亲结点
其中涉及到了回溯思想,我们每次遍历到某个结点的时候,认为这个结点的根结点就是它本身。让以这个结点为根节点的 DFS 全部遍历完毕了以后,再将这个结点的根节点设置为这个结点的父一级结点
回溯的时候,如果以该节点为起点 查询边的另一个结点也恰好访问过了,则直接更新查询边的 LCA 结果
将存储的答案输出
注意:因为在 Tarjan 过程中尝试寻找查询边的 LCA ,所以必须先存储下来查询了哪些边,最后经得到的 LCA 顺序存储在 ans 数组中输出就是答案顺序(离线操作)
复杂度分析
Tarjan算法需要初始化并查集,所以预处理的时间复杂度为 O(n) ,Tarjan 算法处理所有询问的时间复杂度为O(n+m)。但是 Tarjan 算法的常数比倍增算法大 。Tarjan 算法中使用的并查集性质比较特殊,在仅使用路径压缩优化的情况下,单次调用 find()
函数的时间复杂度为均摊 O(1),而不是 O(logn)
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 #include <bits/stdc++.h> using namespace std ;const int N=4e4 +5 ;inline int read () { int x=0 ,f=0 ; char c=getchar(); for (;c<'0' ||c>'9' ;c=getchar())if (c=='-' )f=1 ; for (;c>='0' &&c<='9' ;c=getchar())x=(x<<1 )+(x<<3 )+(c^'0' ); return f?-x:x; } int n,m,u,v,val,s;int tot=0 ;int head[N],to[N<<1 ],nex[N<<1 ],fa[N],ans[205 ],vis[N];struct note { int node,id; }; vector <note> ques[N];inline void add (int u,int v,int val) { to[++tot]=v,nex[tot]=head[u],head[u]=tot; } inline int getfa (int x) { return fa[x]==x?x:fa[x]=getfa(fa[x]); } void init () { for (int i=1 ;i<=n;i++){ fa[i]=i; ques[i].clear(); } tot=0 ; memset (head,-1 ,sizeof head); } void tarjan (int u,int from) { for (int i=head[u];i;i=nex[i]) if (to[i]!=from) tarjan(to[i],u),fa[to[i]]=u; int len=ques[u].size(); for (int i=0 ;i<len;i++) if (vis[ques[u][i].node]){ ans[ques[u][i].id]=getfa(ques[u][i].node); } vis[u]=1 ; } void solve () { n=read(),m=read(); init(); for (int i=1 ;i<n;i++){ u=read(),v=read(),val=read(); add(u,v,val),add(v,u,val); } for (int i=1 ;i<=m;i++){ u=read(),v=read(); ques[u].push_back((note){v,i}),ques[v].push_back((note){u,i}); } s=1 ; tarjan(s,0 ); for (int i=1 ;i<=m;i++) printf ("%d\n" ,ans[i]); } signed main () { int t;cin >>t; while (t--){ solve(); } return 0 ; }
其实为什么要将查询的连个顶点正反两次存储:因为在上面过程我们不能保证遍历 u
时 v
已经回溯那么这时就不能进行查找 LCA ,存正反两次正好完美解决了上述情况,经过了这次 u
被回溯后遍历到 v
一定能查找LCA
利用欧拉序转换为RMQ问题
将树看成一个无向图,u
和 v
的公共祖先一定在 u
与 v
之间的最短路径上:
DFS : 从树T
的根开始, 进行深度优先遍历(将树T
看成一个无向图), 并记录下每次到达的顶点, 以及这个点的深度, 第一个的结点是 root(T)
, 每经过一条边都记录它的端点。由于每条边恰好经过2次,因此一共记录了2n-1
个结点,用dfn[1,…,2n-1]
来表示
计算 first
: 用 first[i]
表示 dfn
数组中第一个值为 i
的元素下标,即如果 first[u]<first[v]
时, DFS
访问的顺序是 。虽然其中包含 u
的后代,但深度最小的还是 u
与 v
的公共祖先
计算 ST 表:用 f[2*N]][M]
记录以 dfn[]
中 i
位置对应的节点为第一个节点,和长度2j 为的区间另一端点对应的节点的最近公共祖先在 dfn[]
中的位置
RMQ 查询:在线查询两个端点的最近公共祖先—— first(LCA(u,v))=min{first[k]|k属于first[u]...first[v]}
不难理解:从 u
走到 v
的过程中一定会经过 LCA(u,v)
,但不会经过 LCA(u,v)
的祖先。因此,从u
走到 v
的过程中经过的欧拉序最小的结点就是 LCA(u,v)
注意:先预处理所有的可能,然后在线每次以复杂度为 O(1) 查询两点的最近 LCA ,所以是在线操作,无法中途修改图的关系
复杂度分析
用 DFS 计算欧拉序列的时间复杂度是 O(n) ,且欧拉序列的长度也是 O(n) ,所以 LCA 问题可以在 O(n) 的时间内转换为等规模的 RMQ 问题,使用 ST 表解决 RMQ 问题不支持在线修改,需处理的时间是 O(nlogn)
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 #include <bits/stdc++.h> using namespace std ;const int N=1e5 +5 ;const int M=20 ;int n,m;int pcnt=0 ,tot=0 ;int first[N];int dfn[2 *N];int depth[2 *N];int f[2 *N][M];int head[N];struct node { int nxt,v; }edge[2 *N]; void init () { pcnt=0 ; tot=0 ; memset (head,-1 ,sizeof head); } void add (int x,int y) { tot++; edge[tot].nxt=head[x]; head[x]=tot; edge[tot].v=y; } void dfs (int u,int from,int dep) { dfn[++pcnt]=u; first[u]=pcnt; depth[pcnt]=dep; for (int i=head[u];i+1 ;i=edge[i].nxt){ int v=edge[i].v; if (v==from) continue ; dfs(v,u,dep+1 ); dfn[++pcnt]=u; depth[pcnt]=dep; } } void ST () { for (int i=1 ;i<=pcnt;i++) f[i][0 ]=i; for (int j=1 ;j<M;j++) for (int i=1 ;i+(1 <<j)-1 <=pcnt;i++){ int a=f[i][j-1 ],b=f[i+(1 <<(j-1 ))][j-1 ]; if (depth[a]<depth[b])f[i][j]=a; else f[i][j]=b; } } int RMQ (int l,int r) { int k=log ((double )(r-l+1 ))/log (2.0 ); int a=f[l][k],b=f[r-(1 <<k)+1 ][k]; if (depth[a]<depth[b])return a; else return b; } int LCA (int L,int R) { int l=first[L]; int r=first[R]; if (l>r)swap(l, r); int pos=RMQ(l, r); return dfn[pos]; } signed main () { while (cin >>n>>m){ init(); for (int i=1 ;i<=n-1 ;i++){ int x,y;cin >>x>>y; add(x,y),add(y,x); } dfs(1 ,0 ,1 ); ST(); for (int i=1 ;i<=m;i++){ int a,b;cin >>a>>b; cout <<LCA(a,b)<<endl ; } } return 0 ; }
树链剖分解决
有点杀鸡用牛刀的感觉了,下面树链剖分有讲,基本不用
模板
模板题 :n
个点 m
次询问无根树中两点的最短路径(其实就是找 LCA 并且维护距离)
倍增算法
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 #include <bits/stdc++.h> using namespace std ;const int maxn=4e4 +5 ;struct Edge { int to,nex,val; }e[maxn<<1 ]; int head[maxn],tot;int t,n,m;int dis[maxn],dep[maxn],pre[maxn][21 ];void init () { memset (head,-1 ,sizeof head); memset (dis,0x3f ,sizeof dis); dis[1 ]=dis[0 ]=0 ; tot=0 ; } void add (int x,int y,int val) { e[tot].to=y,e[tot].val=val,e[tot].nex=head[x],head[x]=tot++; } void dfs (int x,int from) { dep[x]=dep[from]+1 ; pre[x][0 ]=from; for (int i=1 ;(1 <<i)<=dep[x];i++){ pre[x][i]=pre[pre[x][i-1 ]][i-1 ]; } for (int i=head[x];~i;i=e[i].nex){ int y=e[i].to; if (y!=from) { dis[y]=dis[x]+e[i].val; dfs(y,x); } } } int lca (int x,int y) { if (dep[x]<dep[y])swap(x, y); for (int i=19 ;i>= 0 ;i--) { if (dep[x]-(1 <<i)>=dep[y]){ 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 ]; } signed main () { int t;cin >>t; while (t--){ init(); cin >>n>>m; for (int i=1 ;i<n;i++){ int x,y,val; cin >>x>>y>>val; add(x,y,val),add(y,x,val); } dfs(1 ,0 ); int x,y,node; for (int i=1 ;i<=m;i++){ cin >>x>>y; node=lca(x,y); cout <<dis[x]+dis[y]-2 *dis[node]<<endl ; } } return 0 ; }
Tarjan算法
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 #include <bits/stdc++.h> using namespace std ;const int N=4e4 +5 ;inline int read () { int x=0 ,f=0 ; char c=getchar(); for (;c<'0' ||c>'9' ;c=getchar())if (c=='-' )f=1 ; for (;c>='0' &&c<='9' ;c=getchar())x=(x<<1 )+(x<<3 )+(c^'0' ); return f?-x:x; } int n,m,u,v,val,s;int tot=0 ;int st[N],to[N<<1 ],nex[N<<1 ],graph[N<<1 ],fa[N],ans[205 ],vis[N],dis[N];struct note { int node,id; }; vector <note> ques[N];inline void add (int u,int v,int val) { to[++tot]=v,nex[tot]=st[u],graph[tot]=val,st[u]=tot; } inline int getfa (int x) { return fa[x]==x?x:fa[x]=getfa(fa[x]); } void init () { for (int i=1 ;i<=n;i++){ fa[i]=i; ques[i].clear(); } tot=0 ; memset (st,-1 ,sizeof st); memset (dis,0x3f ,sizeof dis); dis[1 ]=dis[0 ]=0 ; } void tarjan (int u,int from) { for (int i=st[u];i;i=nex[i]) if (to[i]!=from) dis[to[i]]=dis[u]+graph[i],tarjan(to[i],u),fa[to[i]]=u; int len=ques[u].size(); for (int i=0 ;i<len;i++) if (vis[ques[u][i].node]){ int tot=getfa(ques[u][i].node); ans[ques[u][i].id]=dis[u]+dis[ques[u][i].node]-2 *dis[tot]; } vis[u]=1 ; } void solve () { n=read(),m=read(); init(); for (int i=1 ;i<n;i++){ u=read(),v=read(),val=read(); add(u,v,val),add(v,u,val); } for (int i=1 ;i<=m;i++){ u=read(),v=read(); ques[u].push_back((note){v,i}),ques[v].push_back((note){u,i}); } s=1 ; tarjan(s,0 ); for (int i=1 ;i<=m;i++) printf ("%d\n" ,ans[i]); } signed main () { int t;cin >>t; while (t--){ solve(); } return 0 ; }
树的重心与直径
树的重心
参考博客
定义 :树上一节点最大子树的节点数最小
性质 :
删除重心后所得的所有子树,节点数不超过原树的 1/2 ,一棵树最多有两个重心
树中所有节点到重心的距离之和最小,如果有两个重心,那么他们距离之和相等
两个树通过一条边合并,新的重心在原树两个重心的路径上
树删除或添加一个叶子节点,重心最多只移动一条边
定义求解
先选择一个节点作为根节点然后 dfs 其所有子树并记录最大子树中的节点数,因为根节点的子树又可以作为其子树的根节点所以可以递归求解。每 dfs 一次就得到了当前节点的最大子树中的节点数所以尝试更新答案
无权节点常用模板
一般也就用这个模板,涉及点权的树的重心比较难
模板题 :有 T
组数据,求出每组数据所构成的树的重心,输出这个树的重心的编号,并且输出重心删除后得到的最大子树的节点个数,如果个数相同取编号小的
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 #include <bits/stdc++.h> using namespace std ;const int N=1e5 +5 ;const int inf=0x3f3f3f3f ;struct Edge { int to,nex; }e[N]; int n,tot;int siz[N],head[N],ans,pos;void init () { ans=inf,pos=0 ; tot=0 ; memset (head,-1 ,sizeof head); } void add (int x,int y) { tot++; e[tot].nex=head[x]; head[x]=tot; e[tot].to=y; } void dfs (int x,int from) { siz[x]=1 ; int max_part=0 ; for (int i=head[x];~i;i=e[i].nex){ int y=e[i].to; if (y==from)continue ; dfs(y,x); siz[x]+=siz[y]; max_part=max(max_part,siz[y]); } max_part=max(max_part,n-siz[x]); if (max_part<ans){ ans=max_part,pos=x; } } signed main () { int t;cin >>t; while (t--){ init(); cin >>n; for (int i=1 ;i<n;i++){ int a,b; cin >>a>>b; add(a,b),add(b,a); } dfs(1 ,0 ); cout <<pos<<" " <<ans<<endl ; } return 0 ; }
带权节点常用模板
siz[i]
同上,dp[i]
表示以 i
为根节点的最大子树大小,val[i]
为 i
节点的点权
节点的点权影响了最大子树的选择,因为点权为 5 的点相当于 5 个点权为 1 的点,我们可以看成在节点位置及后面节点是 5 个完全相同的路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 inline void dfs (int now,int from) { siz[now]=val[now]; dp[now]=0 ; for (int i=head[now];i;i=a[i].nxt){ int t=a[i].to; if (t==from) continue ; dfs(t,now); siz[now]+=siz[t]; dp[now]=max(dp[now],siz[t]); } dp[now]=max(dp[now],n-dp[now]); if (dp[now]<dp[ans]) pos=now }
如果把权值数组 val[]
初始化为 1 那么就相当于无权节点找树的重心模板了
性质求解
一般来说定义求解就够用了,但在某些时候性质求解更方便实用,这里就以带权节点为例
根据性质2:我们可以处理出所有节点到某一节点的距离,取最小值
怎么求出每个节点到某一节点的距离呢?在 dfs
过程中向下处理是很容易的,所以我们可以先处理出所有节点到根节点的距离
siz[i]
同上,f[i]
表示节点 i
的所有子节点到i
的距离和,val[i]
同上, a[i].val
为边权,设定 1 号节点为根节点
1 2 3 4 5 6 7 8 9 10 11 12 inline void dfs1 (int now,int fa,int deep) { siz[now]=val[now]; dep[now]=deep; for (int i=head[now];i;i=a[i].nxt) { int t=a[i].to; if (t==fa)continue ; dfs1(t,now,deep+a[i].val); siz[now]+=siz[t]; f[now]+=f[t]+siz[t]*a[i].val; } }
对于 f
数组的处理的理解:t
的所有子节点到 t
的距离 +now
的当前子树所有节点到 now
的距离。
这样就求得了根节点的距离和,我们再通过根节点递推其他节点的距离和,有如下公示:f[now]=f[from]+(siz[根]-2*siz[now])*边权;(now!=根节点)
对于 now
的子节点,每个节点的距离减少了一个边权,总距离减少 siz[now]*边权
,对于非 v
子节点,每个节点距离增加了一个边权,总距离增加 (siz[根]-siz[now])*边权
1 2 3 4 5 6 7 8 9 inline void dfs2 (int now,int fa) { if (now^root) f[now]=f[fa]+siz[1 ]-2 *siz[now]; if (f[now]<sum) res=now,sum=f[now]; for (int i=head[now];i;i=a[i].nxt){ int t=a[i].to; if (t==fa)continue ; dfs2(t,now); } }
这种方法还可以优化——观察式子:显然一个节点的所有子树中,只有子节点数最多的一个可能成为重心,所以我们可以加以改进,在 dfs2
中只走子树节点最多的一个
这样复杂度整体虽然还是 O(n) 的,但是查询复杂度变为了 O(树高) 在某些题目中(下面例题中)有奇效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 inline void dfs1 (int now,int fa,int deep) { siz[now]=val[now]; dep[now]=deep; int maxson=-1 ; for (int i=head[now];i;i=a[i].nxt){ int t=a[i].to; if (t==fa) continue ; dfs1(t,now,deep+a[i].val); siz[now]+=siz[t]; f[now]+=f[t]+siz[t]*a[i].val; if (siz[t]>maxson) maxson=siz[t],son[now]=t; } } inline void dfs2 (int now,int fa) { if (now^1 ) f[now]=f[fa]+siz[1 ]-2 *siz[now]; if (f[now]<sum) res=now,sum=f[now]; if (son[now]) dfs2(t,now); }
模板
模板题 :第一行为 N
,1<N<=50000
,表示树的节点数目,树的节点从 1 到 N
编号。 接下来 N-1
行,每行两个整数 U
,V
,表示 U
与 V
之间有一条边。 再接下 N
行,每行一个正整数,其中第 i
行的正整数表示编号为 i
的节点权值为 W(I)
,树的深度 <=100 。将最小的 S(x,y)
输出到 median.out ,结果保证不超过 1e9
先考虑暴力枚举 x
,y
,那么对于每一对 x
,y
分界都是一条树上的边。那么我们不如枚举断边,再找出重心。先 O(n) 求出 f[root]
的值,枚举断边,再通过上述第二种优化过的方法求距离和,总复杂度 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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 #include <bits/stdc++.h> using namespace std ;typedef long long ll;const ll inf=0x3f3f3f3f3f3f3f3f ;inline ll read () { ll x=0 ,f=1 ; char ch; for (ch=getchar();(ch<'0' ||ch>'9' )&&ch!='-' ;ch=getchar()); if (ch=='-' ) f=0 ,ch=getchar(); while (ch>='0' &&ch<='9' ){x=(x<<1 )+(x<<3 )+ch-'0' ;ch=getchar();} return f?x:-x; } ll n,res,cut; ll val[100010 ],siz[100010 ],f[100010 ],dep[100010 ]; ll from[100010 ]; ll son1[100010 ],son2[100010 ]; ll head[100010 ],cnt=1 ; struct poll { ll nxt,to; }a[100010 ]; inline void add (ll x,ll y) { a[++cnt].nxt=head[x]; a[cnt].to=y; head[x]=cnt; } inline void dfs1 (ll now,ll fa,ll deep) { siz[now]=val[now]; dep[now]=deep; from[now]=fa; for (ll i=head[now];i;i=a[i].nxt){ ll t=a[i].to; if (t==fa) continue ; dfs1(t,now,deep+1 ); siz[now]+=siz[t]; f[now]+=(f[t]+siz[t]); if (siz[t]>siz[son1[now]]){ son2[now]=son1[now]; son1[now]=t; } else if (siz[t]>siz[son2[now]]) son2[now]=t; } } inline void dfs3 (ll now,ll sum,ll all,ll &ans) { ans=min(ans,sum); ll t=son1[now]; if (t==cut||siz[son2[now]]>siz[son1[now]]) t=son2[now]; if (!t) return ; if (2 *siz[t]>all) dfs3(t,sum+all-2 *siz[t],all,ans); } inline void dfs2 (ll now) { for (ll i=head[now];i;i=a[i].nxt){ ll t=a[i].to; if (t==from[now])continue ; cut=t; for (ll x=now;x;x=from[x]) siz[x]-=siz[t]; ll A=inf,B=inf; dfs3(1 ,f[1 ]-f[t]-dep[t]*siz[t],siz[1 ],A); dfs3(t,f[t],siz[t],B); res=min(res,A+B); for (ll x=now;x;x=from[x]) siz[x]+=siz[t]; dfs2(t); } } signed main () { n=read(); for (ll x,y,i=1 ;i<n;++i){ x=read(),y=read(); add(x,y); add(y,x); } for (ll i=1 ;i<=n;++i) val[i]=read(); res=inf; dfs1(1 ,0 ,0 ); dfs2(1 ); printf ("%lld\n" ,res); return 0 ; }
树的直径
参考博客
定义 :我们将一棵树 T=(V,E)
的直径定义为 maxδ (u,v)(u,v∈V)
,也就是说,树中所有最短路径距离的最大值 即为树的直径
以一道为讲解模板题 :给一棵树,找出树的直径(即两点最短距离中最大的那条)
两次 DFS(或者 BFS )求解
方法 :建立无向图,先从任意一点P
出发,找离它最远的点 Q
,再从点 Q
出发,找离它最远的点 W
,W
到 Q
的距离就是是的直径
简易证明 :
从图中可以看出任选一个点 P
找到最远的 Q
一定是直径的一端,再从 Q
反向找最远的点则必定保证了是直径的另一端
详细证明 :
若 P
已经在直径上,根据树的直径的定义可知 Q
也在直径上且为直径的一个端点
若 P
不在直径上,我们用反证法,假设此时 WQ
不是直径,AB
是直径
若 AB
与 PQ
有交点 C
,由于 P
到 Q
最远,那么 PC+CQ>PC+CA
,所以 CQ>CA
,易得 CQ+CB>CA+CB
,即 CQ+CB>AB
,与 AB
是直径矛盾不成立,如下图(其中 AB
,PQ
不一定是直线,画成直线是为了方便)
若 AB
与 PQ
没有交点, M
为 AB
上任意一点, N
为 PQ
上任意一点。首先还是 NP+NQ>NQ+MN+MB
,同时减掉 NQ
,得 NP>MN+MB
,易知 NP+MN>MB
,所以 NP+MN+MA>MB+MA
,即 NP+MN+MA>AB
,与 AB
是直径矛盾,所以这种情况也不成立,如下图
模板
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 #include <bits/stdc++.h> using namespace std ;const int N=100005 ;int n,m,tot;int q,ans;int dis[N];int head[N],to[N],val[N],nex[N];void init () { tot=0 ; memset (head,-1 ,sizeof head); } void add (int x,int y,int z) { tot++; nex[tot]=head[x]; head[x]=tot; to[tot]=y; val[tot]=z; } void dfs (int x,int from) { if (ans<dis[x]){ ans=dis[x]; q=x; } for (int i=head[x];~i;i=nex[i]){ int j=to[i]; if (j==from)continue ; dis[j]=dis[x]+val[i]; dfs(j,x); } } void find (int x) { ans=0 ; dis[x]=0 ; dfs(x,0 ); } signed main () { while (cin >>n>>m){ init(); int x,y,z;char ch; for (int i=1 ;i<=m;++i){ cin >>x>>y>>z>>ch; add(x,y,z);add(y,x,z); } find(1 ); find(q); printf ("%d\n" ,ans); } return 0 ; }
树形DP求解
对于每个节点我们要记录两个值:
对于一个节点我们只用其子节点的 f1
更新它到叶子结点 距离的最大值和次大值来保证两条路径完全不一样(或者说其子节点不会既在最长路径还在次大路径上)
若 j
是 i
的儿子,那么(下面的 w[i][j]
表示 i
到 j
的路径长度):
若 f1[i]<f1[j]+w[i][j]
,f2[i]=f1[i],f1[i]=f1[j]+w[i][j]
否则若 f2[i]<f1[j]+w[i][j]
,f2 [i]=f1[j]+w[i][j]
这样做就是先看能否更新最大值,若能它的次大值就是原先的最大值,再更新它的最大值
若不能就看能不能更新次大值,若能就更新,不能就不管它
这样的话,最后的答案 ans=max{f1[i]+f2[i]}
,对于无向图任选一个节点作为根节点 i
然后不断用子节点 j
维护f1[i]
和 f2[i]
,又因为找 f1[j]
和 f2[j]
和上述一样所以可以递归求解,对于有向图则需要找到根节点
模板
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 #include <bits/stdc++.h> using namespace std ;const int N=100005 ;int n,m;int tot,ans;int f1[N],f2[N];int head[N],to[N],val[N],nex[N];void init () { tot=0 ; memset (head,-1 ,sizeof head); } void add (int x,int y,int z) { tot++; nex[tot]=head[x]; head[x]=tot; to[tot]=y; val[tot]=z; } void dp (int x,int from) { for (int i=head[x];~i;i=nex[i]){ int j=to[i]; if (j==from)continue ; dp(j,x); if (f1[x]<f1[j]+val[i]){ f2[x]=f1[x]; f1[x]=f1[j]+val[i]; } else if (f2[x]<f1[j]+val[i]) f2[x]=f1[j]+val[i]; ans=max(ans,f1[x]+f2[x]); } } signed main () { while (cin >>n>>m){ init(); int x,y,z;char ch; for (int i=1 ;i<=m;++i){ cin >>x>>y>>z>>ch; add(x,y,z); add(y,x,z); } dp(1 ,0 ); printf ("%d\n" ,ans); } return 0 ; }
这俩做法差不多,两次 DFS 在效率和空间都要比树形 DP 表现的要好一点点差距不大
树链剖分
解决问题 :树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息 。具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息
分类 :重链剖分、长链剖分和实链剖分
大多数情况下树链剖分就是指重链剖分,这里讲解也先只讲解重链剖分
重链剖分
引入 :考虑一下两道题
将树从 x
到 y
节点最短路径上所有节点的值就加上 z
——我们可以通过树上差分以 O(n+m) 复杂度解决
求数从 x
到 y
节点最短路径上所有节点值之和——我们可以借助 LCA 解决,先 O(n) 预处理求出所有的节点到根节点的的距离 dis ,然后找到两节点的 LCA ,最后结果就是 dis[x]+dis[y]-dis[LCA]
,总的时间复杂度是 O(mlogn+n)
那么如果两个问题合并为同一个问题呢,每次执行操作一之后就必须重新预处理操作二的 dis ,即每次询问前都要再次 DFS 求 dis ,方法不够优秀
我们知道线段树是一种动态处理区间查询修改的线性结构,那么如果我们将树的节点分割成多条链然后利用线段树维护不就可以成功解决上述问题了吗?
当然根据情况不同可以使用其他数据结构来代替线段树解决其他相关问题
注意:重链剖分本质上是一种优化暴力的衍生算法
定义
把落单的结点也当作重链,那么整棵树就被剖分成若干条重链
操作
树剖的实现分两个 DFS 的过程:
第一次 DFS 记录每个结点的父节点( fa
)、深度( dep
)、子树大小( siz
)、重子节点( son
)
第二次 DFS 记录所在链的链顶( top
,应初始化为结点本身)、重边优先遍历时的 DFS
序( dfn
)、DFS
序对应的节点编号( rnk
)
数组
意义
fa[x]
节点 x
在树上的父亲
dep[x]
节点 x
在树上的深度
siz[x]
节点 x
为根节点的树的节点个数(包括自己)
son[x]
节点 x
的重儿子
top[x]
节点 x
所在重链的顶部节点(深度最小)
dfn[x]
节点 x
的DFS序,也是其在线段树中的编号
rnk[x]
DFS序所对应的节点编号,有 rnk(dfn(x))=x
具体实现
第一次 DFS 首先求出每个节点所在子树的大小,找到它的重儿子(借助求出的 siz
找并用 son
记录),同时顺便记录其父亲和深度(获得 fa[]
和 dep[]
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void dfs1 (int u,int from,int depth) { fa[u]=from; dep[u]=depth; siz[u]=1 ; for (int i=head[u];~i;i=e[i].nex){ int v=e[i].to; if (v==from)continue ; dfs1(v,u,depth+1 ); siz[u]+=siz[v]; if (siz[v]>siz[son[u]]) son[u]=v; } } signed main () { dfs1(root,0 ,1 ); }
第二次 DFS 连接出重链同时给每个节点标上 DFS 序,并且为了便于用其他数据结构来维护重链,我们在 DFS 时保证一条重链上各个节点 DFS 序连续(即优先遍历节点的重儿子在走其他的儿子)并获得 top[]
、dfn[]
和rnk[]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void dfs2 (int u,int t) { top[u]=t; dfn[u]=++cnt; rnk[cnt]=u; if (!son[u])return ; dfs2(son[u],t); for (int i=head[u];~i;i=e[i].nex){ int v=e[i].to; if (v!=son[u]&&v!=fa[u]) dfs2(v,v); } }
性质
树上每个节点都属于且仅属于一条重链
重链开头的结点不一定是重子节点(因为重边是对于每一个结点都有定义的)
所有的重链将整棵树完全剖分
在剖分时优先遍历重儿子 ,最后重链的DFS序就会是连续的
在剖分时重边优先遍历 ,最后树的DFN序上,重链内的DFN序是连续的。按DFN序排序后的序列即为剖分后的链
一颗子树内的DFN序是连续的
可以发现,当我们向下经过一条轻边 时,所在子树的大小至少会除以二,因此对于树上的任意一条路径,把它拆分成从 LCA 分别向两边往下走,分别最多走 O(logn) 次,因此树上的每条路径都可以被拆分称不超过 O(logn) 条重链
常见应用
因为树链剖分后可以获得多条线性的链并且链上的节点的DFN序是连续编号,所以我们可以按照节点的DFN序顺序存入线段树(或者树状数组)维护完成下述操作
路径上维护
将树从 x
到 y
节点最短路径上所有节点值都加上 z
查询树从 x
到 y
节点最短路径上所有节点值的加和
查询任意两点间路径和操作时:设所在链顶端的深度更深的那个点为 x
点
res
加上 x
点到 x
所在链顶端 这一段区间的点权和
把 x
跳到 x
所在链顶端的那个点的上面一个点、
不断执行上述操作直到两个点处于同一条链上,这时再加上此时两个点的区间和即可(相当于是先分别计算两点所处不同链上路径和,在计算同一个链上经过的路径和)每次查询时间复杂度为 O(log2 n)
1 2 3 4 5 6 7 8 9 10 11 12 int queryRange (int x,int y) { int res=0 ; while (top[x]!=top[y]){ if (dep[top[x]]<dep[top[y]]) swap(x,y); res+=query(1 ,dfn[top[x]],dfn[x]); x=fa[top[x]]; } if (dep[x]>dep[y])swap(x,y); res+=query(1 ,dfn[x],dfn[y]); return res%mod; }
更改操作和上述类似
1 2 3 4 5 6 7 8 9 10 void updateRange (int x,int y,int z) { while (top[x]!=top[y]){ if (dep[top[x]]<dep[top[y]]) swap(x,y); update(1 ,dfn[top[x]],dfn[x],z); x=fa[top[x]]; } if (dep[x]>dep[y])swap(x,y); update(1 ,dfn[x],dfn[y],z); }
子树维护
将以 x
为根节点的树内所有节点值都加上z
查询以 x
为根节点的树内所有节点值的加和
和路径维护的操作一样,关键是如何确定修改的节点区间:因为 siz[]
记录了每个节点为根的树大小,并且其每个子树的DFN序都是连续的,所以修改区间为 [dfn[x],dfn[x]+siz[x]-1]
1 2 3 void updateSon (int x,int z) { update(1 ,dfn[x],dfn[x]+siz[x]-1 ,z); }
同理,查询操作和上述类似
1 2 3 int querySon (int x) { return query(1 ,dfn[x],dfn[x]+siz[x]-1 ); }
模板
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 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 #include <bits/stdc++.h> using namespace std ;const int maxn=1e5 +5 ;int n,m,root;int tot,cnt;int a[maxn],head[maxn];int fa[maxn],dep[maxn],siz[maxn],son[maxn];int dfn[maxn],rnk[maxn],top[maxn];struct Edge { int to,nex; }e[maxn]; void init () { tot=cnt=0 ; memset (head,-1 ,sizeof head); memset (son,0 ,sizeof son); } void add (int x,int y) { tot++; e[tot].nex=head[x]; head[x]=tot; e[tot].to=y; } void dfs1 (int u,int from,int depth) { fa[u]=from; dep[u]=depth; siz[u]=1 ; for (int i=head[u];~i;i=e[i].nex){ int v=e[i].to; if (v==from)continue ; dfs1(v,u,depth+1 ); siz[u]+=siz[v]; if (siz[v]>siz[son[u]]) son[u]=v; } } void dfs2 (int u,int t) { top[u]=t; dfn[u]=++cnt; rnk[cnt]=u; if (!son[u])return ; dfs2(son[u],t); for (int i=head[u];~i;i=e[i].nex){ int v=e[i].to; if (v!=son[u]&&v!=fa[u]) dfs2(v,v); } } struct segtree { int l,r; int val; int add; }t[maxn<<2 ]; void build (int root,int l,int r) { t[root].l=l,t[root].r=r,t[root].add=0 ; if (l==r){ t[root].val=a[rnk[l]]; return ; } int mid=(l+r)>>1 ; build(root*2 ,l,mid); build(root*2 +1 ,mid+1 ,r); t[root].val=t[root<<1 ].val+t[root<<1 |1 ].val; } void spread (int p) { if (t[p].add){ t[p*2 ].val+=t[p].add*(t[p*2 ].r-t[p*2 ].l+1 ); t[p*2 +1 ].val+=t[p].add*(t[p*2 +1 ].r-t[p*2 +1 ].l+1 ); t[p*2 ].add+=t[p].add; t[p*2 +1 ].add+=t[p].add; t[p].add=0 ; } } void update (int root,int l,int r,int x) { if (l<=t[root].l&&r>=t[root].r){ t[root].val+=x*(t[root].r-t[root].l+1 ); t[root].add+=x; return ; } spread(root); int mid=(t[root].l+t[root].r)/2 ; if (l<=mid)update(root*2 ,l,r,x); if (r>=mid+1 )update(root*2 +1 ,l,r,x); t[root].val=t[root*2 ].val+t[root*2 +1 ].val; } int query (int root,int l,int r) { if (l<=t[root].l&&r>=t[root].r) return t[root].val; spread(root); int mid=(t[root].l+t[root].r)>>1 ; int ans=0 ; if (l<=mid)ans+=query(root*2 ,l,r); if (r>=mid+1 )ans+=query(root*2 +1 ,l,r); return ans; } void updateRange (int x,int y,int z) { while (top[x]!=top[y]){ if (dep[top[x]]<dep[top[y]]) swap(x,y); update(1 ,dfn[top[x]],dfn[x],z); x=fa[top[x]]; } if (dep[x]>dep[y])swap(x,y); update(1 ,dfn[x],dfn[y],z); } int queryRange (int x,int y) { int res=0 ; while (top[x]!=top[y]){ if (dep[top[x]]<dep[top[y]]) swap(x,y); res+=query(1 ,dfn[top[x]],dfn[x]); x=fa[top[x]]; } if (dep[x]>dep[y])swap(x,y); res+=query(1 ,dfn[x],dfn[y]); return res; } void updateSon (int x,int z) { update(1 ,dfn[x],dfn[x]+siz[x]-1 ,z); } int querySon (int x) { return query(1 ,dfn[x],dfn[x]+siz[x]-1 ); } signed main () { while (cin >>n>>m>>root){ init(); for (int i=1 ;i<=n;i++)cin >>a[i]; for (int i=1 ;i<=n-1 ;i++){ int x,y;cin >>x>>y; add(x,y),add(y,x); } dfs1(root,0 ,1 ); dfs2(root,root); build(1 ,1 ,n); int choice; while (m--){ cin >>choice; if (choice==1 ){ int x,y,z; cin >>x>>y>>z; updateRange(x,y,z); } else if (choice==2 ){ int x,y; cin >>x>>y; cout <<queryRange(x,y)<<endl ; } else if (choice==3 ){ int x,z; cin >>x>>z; updateSon(x,z); } else { int x;cin >>x; cout <<querySon(x)<<endl ; } } } return 0 ; }
模板题 :N
个节点组成的数,分别执行上述四种操作并将查询结果对 mod
取模
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 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 #include <bits/stdc++.h> #define ing long long int using namespace std ;const int maxn=1e5 +5 ;int n,m,root,mod;int tot,cnt;int a[maxn],head[maxn];int fa[maxn],dep[maxn],siz[maxn],son[maxn];int dfn[maxn],rnk[maxn],top[maxn];struct Edge { int to,nex; }e[maxn]; void init () { tot=cnt=0 ; memset (head,-1 ,sizeof head); memset (son,0 ,sizeof son); } void add (int x,int y) { tot++; e[tot].nex=head[x]; head[x]=tot; e[tot].to=y; } void dfs1 (int u,int from,int depth) { fa[u]=from; dep[u]=depth; siz[u]=1 ; for (int i=head[u];~i;i=e[i].nex){ int v=e[i].to; if (v==from)continue ; dfs1(v,u,depth+1 ); siz[u]+=siz[v]; if (siz[v]>siz[son[u]]) son[u]=v; } } void dfs2 (int u,int t) { top[u]=t; dfn[u]=++cnt; rnk[cnt]=u; if (!son[u])return ; dfs2(son[u],t); for (int i=head[u];~i;i=e[i].nex){ int v=e[i].to; if (v!=son[u]&&v!=fa[u]) dfs2(v,v); } } struct segtree { int l,r; int val; int add; }t[maxn<<2 ]; void build (int root,int l,int r) { t[root].l=l,t[root].r=r,t[root].add=0 ; if (l==r){ t[root].val=a[rnk[l]]%mod; return ; } int mid=(l+r)>>1 ; build(root*2 ,l,mid); build(root*2 +1 ,mid+1 ,r); t[root].val=t[root<<1 ].val+t[root<<1 |1 ].val; } void spread (int p) { if (t[p].add){ t[p*2 ].val+=t[p].add*(t[p*2 ].r-t[p*2 ].l+1 ); t[p*2 +1 ].val+=t[p].add*(t[p*2 +1 ].r-t[p*2 +1 ].l+1 ); t[p*2 ].add+=t[p].add; t[p*2 +1 ].add+=t[p].add; t[p].add=0 ; } } void update (int root,int l,int r,int x) { if (l<=t[root].l&&r>=t[root].r){ t[root].val+=x*(t[root].r-t[root].l+1 ); t[root].add+=x; return ; } spread(root); int mid=(t[root].l+t[root].r)/2 ; if (l<=mid)update(root*2 ,l,r,x); if (r>=mid+1 )update(root*2 +1 ,l,r,x); t[root].val=t[root*2 ].val+t[root*2 +1 ].val; } int query (int root,int l,int r) { if (l<=t[root].l&&r>=t[root].r) return t[root].val; spread(root); int mid=(t[root].l+t[root].r)>>1 ; int ans=0 ; if (l<=mid)ans+=query(root*2 ,l,r); if (r>=mid+1 )ans+=query(root*2 +1 ,l,r); return ans; } void updateRange (int x,int y,int z) { z%=mod; while (top[x]!=top[y]){ if (dep[top[x]]<dep[top[y]]) swap(x,y); update(1 ,dfn[top[x]],dfn[x],z); x=fa[top[x]]; } if (dep[x]>dep[y])swap(x,y); update(1 ,dfn[x],dfn[y],z); } int queryRange (int x,int y) { int res=0 ; while (top[x]!=top[y]){ if (dep[top[x]]<dep[top[y]]) swap(x,y); res+=query(1 ,dfn[top[x]],dfn[x]); res%=mod; x=fa[top[x]]; } if (dep[x]>dep[y])swap(x,y); res+=query(1 ,dfn[x],dfn[y]); return res%mod; } void updateSon (int x,int z) { update(1 ,dfn[x],dfn[x]+siz[x]-1 ,z); } int querySon (int x) { return query(1 ,dfn[x],dfn[x]+siz[x]-1 )%mod; } signed main () { while (cin >>n>>m>>root>>mod){ init(); for (int i=1 ;i<=n;i++)cin >>a[i]; for (int i=1 ;i<=n-1 ;i++){ int x,y;cin >>x>>y; add(x,y),add(y,x); } dfs1(root,0 ,1 ); dfs2(root,root); build(1 ,1 ,n); int choice; while (m--){ cin >>choice; if (choice==1 ){ int x,y,z; cin >>x>>y>>z; updateRange(x,y,z); } else if (choice==2 ){ int x,y; cin >>x>>y; cout <<queryRange(x,y)<<endl ; } else if (choice==3 ){ int x,z; cin >>x>>z; updateSon(x,z); } else { int x;cin >>x; cout <<querySon(x)<<endl ; } } } return 0 ; }
求最近公共祖先
因为树链剖分也得到了DFN序,所以也可以顺便求出任意连点的 LCA :不断向上跳重链,当跳到同一条重链上时,深度较小的结点即为 LCA ,向上跳重链时需要先跳所在重链顶端深度较大的那个
要是树链剖分的题有问到可以直接用,要是只是求个 LCA 倍增算法和 Tarjan 算法足矣
1 2 3 4 5 6 7 8 int LCA (int u,int v) { while (top[u]!=top[v]){ if (dep[top[u]]>dep[top[v]]) u=fa[top[u]]; else v=fa[top[v]]; } return dep[u]>dep[v]?v:u; }
长链剖分