• 树的知识点


    • 树的存储(邻接表)和深度优先遍历
    //数组的邻接表存储
    vector<int> q[maxn]; //这个是一般的
    int fi[maxn]; //存储节点的儿子个数
    int to[maxn]; //存储节点的具体每个儿子
    int ne[maxn]; //指向该节点的下一个儿子
    void link(int x,int y){  //这个函数一定要记住 一条由x指向y的边 
    	to[++m]=y;   //具体的儿子 
    	ne[m]=fi[x];   //下一个(上一个儿子)倒着的 
    	fi[x]=m;   //儿子数量更新 
    } 
    //如果是vector<int> 的话,直接push_back就可以了
    
    //树的深度优先遍历(有树根) 
    void dfs(int x){
    	viste(x); //do something
    	for(int i=fi[x];i;i=ne[i])  dfs(to[i]);   //这个书写顺序也要记住
    	//.... 
    } 
    //无根树
    void dfs1(int x,int fa){   
    	viste(x); //do something
    	for(int i=fi[x];i;i=ne[i]) if(to[i]!=fa) dfs(to[i],x); //要排除数组里面的父亲 
    	//....
    } 
    
    //例:求树中每棵子树的大小和每个节点的深度(基础,用子树的状态去更新根节点的状态,以及深度都是很有用的东西)
    //每个点的子树大小等于它的所有儿子的子树大小之和+1,深度为父亲的深度+1 
    int size[maxn];  //树的大小 
    int de[maxn];    //树的深度
    void getsi(int x,int fa){
    	si[x]=1;
    	de[x]=de[fa]+1;
    	for(int i=fi[x];i;i=ne[i]){
    		if(to[i]!=fa) {
    			dfs(to[i],x);
    			size[x]+=si[to[i]];
    		}
    	}
    } 
    
    • 求两个节点的LCA,这里讲了四种算法

    第一种、暴力

    时间复杂度:与两点之间的距离有关,但是实现简单,随机产生的树期望高度是O(logN)的,所以可能能够使用,最大的优点是允许树动态改变

    //LCA问题,最近公共祖先
    //算法一、两个点逐渐向根移动(暴力)
    //时间复杂度高,但是它允许树动态改变,只需要知道每个点的父亲和深度,(另一个可以处理动态情况的数据结构式动态树) 
    int getlac(int x,int y){
    	while(de[x]>=de[y])  x=fa[x];  //谁深度大,谁就往上移 
    	else y=fa[y];
    	return x;
    } 
    

    第二种、倍增法

    //算法二、倍增法求LCA,让节点每次向上走2的幂次步
    int anc[maxn][maxlog];
    //step 1.求出倍增数组anc[maxn][maxlog] maxlog是最小的满足2^x>=maxdeep的x,anc[x][y]为节点i向上走2^j步能走到节点,j=0,anc[i][j]就是节点的父亲 
            //anc[i][j]=anc[anc[i][j-1]][j-1]
    //step 2.把两个点移到同一深度,这个有两种方法,第一种是把de[y]-de[x]表示为2进制,i号位上面是1,就移动2^i次,第二种是从大到小扫描i,
           //如果anc[x][j]的深度大于y,那么我们就跳x,i
    //step 3.求出LCA,这个做法可以看成是通过二分法求解走最大L-1步的过程,直到完全不能走了为止,再让x,y各向上走一步,则为LCA
    void dfs(int x,int fa){//求出倍增数组anc[maxn][maxlog]
    	de[x]=de[fa]+1;
    	anc[x][0]=fa;
    	for(int i=1;i<maxlog;i++) anc[x][i]=anc[anc[x][i-1]][i-1];  
    	for(int i=fi[x];i;i=ne[i]){
    		if(to[i]!=fa) dfs(to[i],x);
    	}
    } 
    int getlac(int x,int y){
    	if(de[x]<de[y]) swap(x,y); //x在下面
    	for(int i=maxlog-1;i;i--){
    		if(de[anc[x][i]]>=de[y]) x=anc[x][i]; //走到同一深度 
    	} 
    	if(x==y) return x;
    	for(int i=maxlog-1;i;i--){
    		if(anc[x][i]!=anc[y][i]){
    			x=anc[x][i];
    			y=anc[y][i];  //都往上走 
    		}
    	}
    	return anc[x][0]; //直到完全不能走了为止,再让x,y各向上走一步,则为LCA
    }
    

    第三种、转RMQ问题

    转化为欧拉序列上的RMQ问题,采用ST算法

    欧拉序列:每经过一次结点,都进行一次统计产生的DFS序列

    RMQ:指一类查询连续区间的最大(小)值问题

    ST算法:求解RMQ问题的快速方法

     

    要有欧拉序列和深度序列,求LCA(x,y)时,在欧拉序列中的位置分别位pos[x],pos[y],那么它们的LCA就是【pos[x],pos[y]】区间里面深度小的点

    模板:https://blog.csdn.net/diogenes_/article/details/80794838 【模板】ST表求解RMQ问题 Sparse Table 算法给出一个 n个元素的数组 A1,A2,…,An,求解 min(l,r): 计算 min{Al,Al+1,…,ArAl}其实就是用了2分的思想,倍增法的思想,把求解转变成0(1)的问题

    //令 d(i,j) 为从 i开始的, 长度为2^j 的一段元素中的最小值。根据倍增思想,d(i,j) 可以通过 d(i,j-1) 和 d(i+2^j-1,j-1)
    //转移得到,具体操作就是对两个数取 min 。预处理的时间复杂度是 O(nlog2n) 。
    //我们令k为满足 2^k<=R+L+1的最大整数。则可知 k=log2R+L+1.则以l开头,以R结尾的两个长度为2^k的区间合并起来就覆盖了[L,R] 。由于是求范围最小值,有元素被重复计算也没问题。
    //则 Query(L,R)=min(d(L,k),d(R-2^k+1,k))Query(L,R)=min(d(L,k),d(R+2^k+1,k)) 。
    void inti(int n){
    	for(int i=1;i<=n;i++) d[i][0]=a[i];
    	for(int j=1;(1<<j)<=n;j++){
    		for(int i=1;i+(1<<j)-1<=n;i++){
    			d[i][j]=min(d[i][j-1],d[i+(1<<(j-1))][j-1]);
    		}
    	}
    }
    int query(int l,int r){
    	int k=log2(r-l-1);
    	return min(d[l][k],d[r-(1<<k)+1][k]);
    }
    

      

    第四种、Tarjan算法

    //算法四、Tarjan算法
    //离线算法,适合时间限制很严格,倍增法会超时的情况 
    
    //1、DFS整棵树,节点x是属于自身的集合(并查集)Sx 
    //2、DFS(X)时,每次访问玩子树y,把Sy合并到Sx
    //x的所有子节点访问完,标记x已经访问
    //遍历所有关于x的询问(x,y),如果y已经被访问,那么询问的答案为find(y)
    int vis[maxn];
    void dfs(int x){
    	for(int i=0;i<q[x].size();i++){
    		dfs(q[x][i]);
    		onion(q[x][i],x);
    	}
    	vis[x]=1;
    	for(int i=0;i<query[x].size();i++){
    		int y=query[x][i];
    		if(vis[y]) ans[x][y]=find(y); 
    	}
    } 
    //例:求树上两个点之间的距离 
    //求出两个点的LCA,距离等于de[x]+de[y]-2de[LCA] 
    

    一个例子、括号与树的相互转化

    //括号序列与树的相互转化,每一对括号看成是节点,直接套住的括号对看作是子节点,
    int f[maxn],now;
    char s[maxn*2]; 
    void sovle(int fa){
    	while(s[now]!=')'){  //扫过嵌套的内容,直到遇到右括号 
    		++now;
    		link(fa,++n);
    		sovle(n);
    	}
    	++now;
    }
    //对树的深度优先遍历序列输出树的括号序列
    void dfs1(int x){
    	cout<<"(";
    	for(int i=fi[x];i;i=ne[i]) dfs(to[i]);
    	cout<<")";
    } 
    
    • 树的直径、中心、重心

    直径:两点间不重复经过的边和点道路成为两点间的路径,树的直径时树中两点间的最长路径

    树的中心:该店到树中其他点的距离,最远距离最小(可能有多个) 

    树的重心:无根树,若某个节点的每个儿子的子树大小都小于等于n/2,则这个点叫做该数的重心

        一、直径

    //直径:两点间不重复经过的边和点道路成为两点间的路径,树的直径时树中两点间的最长路径
    //求解1、两次遍历找出书的一条直径
           //第一次。找出距离某个点最远的一个点x
    	   //第二次。找出距离结点x最远的一个点y
    	   //直径=x到y的简单路径
    //应该是无根树
    void getdu(int x,int fa){
    	de[x]=de[fa]+1;
    	for(int i=fi[x];i;i=ne[i]){
    		if(to[i]!=fa) dfs(to[i],x);
    	}
    }
    void wrok(){
    	getde(1,0); //根节点作为第一个节点 
    	int x=1;
    	for(int i=2;i<=n;i++){
    		if(de[i]>de[x]) x=i; //找出深度最大的点 
    	}
    	getdu(x,0); //深度最大的点x作为根节点,找出深度最大的点(改变de[]数组)
    	int y=1;
    	for(int i=2;i<=n;i++){
    		if(de[i]>de[y]) y=i;
    	} 
    	cout<<x<<" "<<y<<" "<<de[y]<<endl; //x--y,深度 
    } 
    //求解2、LCA求解
    //一条直径上的所有点都有一个共同的LCA,在DFS的过程中,我们考虑以他为LCA可能的直径,维护以每个点为顶端的最长链和次长链,然后用最长链加上次长链,维护直径
    int len=0;
    int dfs(int x,int fa){
    	int mx,mmx; //最长和次长
    	for(int i=fi[x];i;i=ne[i]){
    		if(to[i]!=fa) {
    			int temp=dfs(to[i],x);
    			if(temp>mx){
    				mmx=mx;
    				mx=temp;
    			}
    			else if(temp>mmx) mmx=temp;
    		}
    	} 
    	len=max(len,mmx+mx);
    	return len;
    } 
    //例子:
    //1、给定一棵树,对于每一个点,求出离他最远的点的距离:只需要求出任意一个直径,比较两个端点哪个离他比较远就可以了
    //2、动态加入叶子,求出直径:根据直径的属性,只需要通过求出(x,now_leaf),(y,new_leaf)去更新(x,y)就可以了
    

        二、中心

    //树的中心:该店到树中其他点的距离,最远距离最小(可能有多个) 
    //已经求出了深度de[],直径的两个端点x,y,z(lca(x,y)),可以利用树的直径求出树的中心
    //设直径长度为R,在x到y的路径上找到与x点距离为R/2的点
    int wrok(){
    	int len=de[x]+de[y]-2*de[z];
    	if(de[x]-de[z]>len/2){  //该店在x到z的路径上,从x往z找 
    		for(int i=1;i<=len/2;i++) x=fa[x];
    		return x;
    	}
    	for(int i=1;i<=len/2;i++) y=fa[y];//不然就在z到y的路上 
    	return y;
    } 
    

        三、重心

    重心的性质:

    (1)重心到其他点的距离和最小,如果有多个重心,那么距离和是一样的

    (2)一棵树增加或者减少一个节点,那么重心移动一条边的距离

    (2)两棵树通过某个点相连得到一棵新的树,新树的重心一定在连接原来两棵树的重心路径上

    //树的重心:无根树,若某个节点的每个儿子的子树大小都小于等于n/2,则这个点叫做该数的重心
    //1、先确定一个根,求出以每个点为根的子树大小
    //2、枚举每一个节点,看它往下的子树(以每个儿子为根的子树)与往上的子树大小是否都小于等于n/2
    int size[maxn],fa[maxn];
    int getsi(int x){
    	si[x]=1;
    	for(int i=fi[x];i;i=ne[i]){
    		if(to[i]!=fa[i]){
    			fa[to[i]]=x;
    			size[x]+=getsi(to[i]); 
    		}
    	}
    	return size[x];
    } 
    bool check(int x){
    	if(n-si[x]>n/2) return 0;
    	for(int i=fi[x];i;i=ne[i]){
    		if((fa[to[i]]==x)&&(size[to[i]]>n/2)) return 0;
    	}
    	return 1;
    } 
    //例: 给出一棵有根树,求出每棵子树的重心
    //根据重心的性质,从下往上对子树求解,O(N)
    bool check(int x,int y){ //检验x是否是子树y的重心 
    	if(size[y]-size[x]>size[y]/2) return 0;  //子树x向上的部分
    	for(int i=fi[x];i;i=ne[i]){
    		if(si[to[i]]>si[y]/2) return 0;  //子树x的每一个子树部分
    	}
    	return 1;
    } 
    int bar[maxn];
    void getbaryct(int x){
    	int p=-1;
    	for(int i=fi[x];i;i=ne[i]){
    		getbaryct(to[i]);  //递归,从下往上
    		if(size[to[i]]>size[x]/2) p=to[i]; //找到这个size大于x一半的子树,让p指向它 ,重心一定在p到x的路径上
    	}
    	if(p==-1) bar[x]=x; //叶子节点
    	else bar[x]=bar[p] ; //先指向这棵子树的重心,然后不断向上
    	while(!check(bar[x],x)) bar[x]=fa[bar[x]];  
    }
    • 树形dp的经典问题

    1.对于每个点的状态数等于这个点size的一类树形DP,主要就是通过DFS序的应用

    void dfs(int x,int fa){
    	size[x]=1;
    	for(int i=head[x];i;i=nex[i]){
    		if(to[i]!=fa){
    			dfs(to[i],x);
    			for(int u=0;u<=size[x];u++){
    				for(int v=0;v<=size[to[i]];v++){
    				//根据实际情况进行转移	
    			}
    		}
    		size[x]+=size[to[i]];
    		}
    	}
    }
    

      

    • 树的统计

    树上统计各类算法:

    1、技巧法

    2、启发式合并(dsu on tree) 把小的集合合并到大的集合

    3、类长链剖分

    4、点分治、边分治

    					//1、树上所有无序点对之间的距离和
    //一条边a,b被经过的次数等与以b为根的子树a的大小乘上以a为根的子树b的大小
    int ans=0;
    void dfs(int x,int fa){
    	ans+=size[x]*(n-size[x]);
    	for(int i=fi[x];i;i=to[i]) if(to[i]!=fa) dfs(to[i]);
    } 
    					//2、以1为根节点的树,每个点有一个颜色col[x],对每个点,有一个询问query[x],表示求出以x为根的子树中有多少个颜色为query[x]的点
    //延伸为,有n个集合,每个集合的大小为1,执行n-1次操作,每次操作任意指定两个大小为a,b的集合,花费Min(a,b)的代价,删去a,b,加入大小为a+b大小的集合
    //在DFS的过程中把一个点的所有儿子的信息合并到自身上即可
    unordered_map<int,int> mp[maxn]; //mp[x],x是序号,first是颜色,second是集合大小 
    void merge(int x,int y){
    	if(mp[x].size()<mp[y].size()) mp[x].swap(mp[y]); //交换,x为集合大的 
    	 for(auto u=mp[y]) mp[x][u.first]+=u.second; //把集合y放到集合x上(小的存到大的) 启发式合并 
    }
    void dfs(int x,int fa){
    	++mp[x][col[x]];
    	for(int i=fi[x];i;i=ne[i]){
    		if(to[i]!=fa) {
    			dfs(to[i],fa)
    			merge(x,to[i]);
    		}
    	}
    	ans[x]=mp[x][query[x]];
    } 
    
    					//3、以1为根节点的树,对每个点x,都有一个询问query[x],求出以x为根的子树中有多合距离为query[x]的点
    //花费min(x,y)的代价,删去a,b集合,加入大小为max(a,b)+1的集合
    //每个点的信息是它子树的信息合并起来加上自己的信息,点代表的集合可以看作是它子树中不同深度的个数,最大深度减去自己的深度
    unordered_map<int,int> mp[maxn];
    void merge(int x,int y){
    	if(mp[x].size()<mp[y].size()) mp[x].swap(mp[y]); //交换,x为集合大的 
    	 for(auto u=mp[y]) mp[x][u.first]+=u.second; //把集合y放到集合x上(小的存到大的) 启发式合并 
    }
    void dfs(int x,int fa,int de){
    	++mp[x][de]; //对应的深度个数++
    	for(int i=fi[x];i;i=ne[i]){
    		if(to[i]!=fa){
    			dfs(to[i],x,de+1);
    			merge(x,to[i]);
    		}
    	} 
    	ans[x]=mp[x][de+query[x]];
    }
    

     

  • 相关阅读:
    rabbitmq
    C#操作redis
    C#设计模式——桥接模式
    C#设计模式——代理模式
    C#设计模式——外观者模式
    WPF Item内TextBox获取焦点时如何选中当前Item
    解决异常System.Runtime.Interopservices.COMException RequestLock问题
    【转】WPF中Binding的技巧(一)
    【转】WPF中的Binding技巧(二)
    ASP.NET State Service服务
  • 原文地址:https://www.cnblogs.com/shirlybaby/p/12286021.html
Copyright © 2020-2023  润新知