树刷题笔记

  • 树的直径
  • 树的中心
  • 树的重心
  • 洛谷P3398(点和路径关系,LCA)
  • 洛谷P4281(枚举,LCA)
  • 洛谷P5588(树状数组,LCA,思维)
  • 洛谷P5536核心城市(树的中心,贪心)
  • 洛谷P1273有线电视网(树形DP,分组背包)
  • HDU6035Colorful tree(思维好题)

树的直径

定义:树中最长的链叫直径
求法:同时维护每个节点到子叶子结点的最长链和次长链,然后答案就是max(longest[i]+second_longest[i])(i=1,2...n)

int ml[N],sl[N];
int dfs(int son,int dad)
{
   for(auto v:eg[son])
      {
            if(v==dad) continue;
            int l=dfs(son,dad);
            if(l>ml[son]) swap(ml[son],l);
            if(l>sl[son]) swap(sl[son],l);
      }   
   ans=max(ans,sl[son]+ml[son]);
   return ml[son]+len(边长);
}

树的中心

定义:到所有点的最长距离最小的点叫做树的中心
一个结论:从起点开始遍历,这颗树的中心一定存在于重链上(同树链刨分)
求法:先给出一些定义:

struct Max_node{
      int v,w;
}sl[N],ml[N];

ml[i]记录到子叶节点的最长路,sl则是次长路,求法如代码

void dfs1(int x,int y)//预处理Mt1和Mt2的内容
{
    for(auto mv:eg[x])if(mv.first!=y){
        int v=mv.first,w=mv.second;
        dfs1(v,x);
        Max_node temp(v,ml[v].w+w);
        if(temp.w>ml[x].w) swap(ml[x],temp);
        if(temp.w>sl[x].w) swap(sl[x],temp);
    }
}
void dfs2(int x,int y,int len,int Max_up)//遍历更新答案
{
    int Max=max(Mt1[x].w,Max_up);//计算该节点的延伸出去的最长路径
    if(Max<ans_w) ans_v=x,ans_w=Max;//更新答案
    if(Max_up>Mt1[x].w) return ;//如果最长路径是往上的,说明没必要往下了,理由同上
    for(auto mv:eg[x])if(mv.first==Mt1[x].v) dfs2(mv.first,x,mv.second,max(sl[x].w,Max_up)+mv.second);
}

树的重心

定义:使得连接自身最大子树最小的点
求法:简单随便求

int siz[N];
void dfs(int s,int d)
{
      siz[s]=1;
      int Max=0;
      for(auto v:eg[s])if(v!=d)
      {
            dfs(v,s);
            siz[s]+=siz[v];
            Max=max(siz[v],Max);
      }
      Max=max(n-siz[s],Max);
      if(Max>Ans_w) Ans_w=Max,Ans_v=s;
}

洛谷P3398仓鼠找Suger

题意:n节点的树,q次询问,每次给出两条路径(给出路径的方式是给出起点和终点),问这两条是否有公共点。(n,q<=1e5)

解答:

一个结论:
如果俩路径相交,则 ab 的 LCA 在 cd 上或者 cd 的 LCA 在 ab 上
另一个结论:
如果一个点在一个路径上,则这个点和另外两个点的LCA分别是这个点和路径的LCA
此题结束

//验证点是否在路径上 
#include<bits/stdc++.h>
using namespace std;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define ll long long
#define N 100005
inline int read(){int x;scanf("%d",&x);return x;}
vector<int>eg[N];
int fa[N][25],deep[N];
void dfs(int s,int d)
{
	deep[s]=deep[d]+1;
	fa[s][0]=d;
	for(auto v:eg[s])if(v!=d){
		dfs(v,s);
	}
}
void init(int n)
{
	for(int j=1;j<=20;++j)
		for(int i=1;i<=n;++i)
			fa[i][j]=fa[fa[i][j-1]][j-1];
}
int LCA(int a,int b)
{
	if(deep[a]<deep[b]) swap(a,b);
	for(int j=20;j>=0;--j) if(deep[fa[a][j]]>=deep[b]) a=fa[a][j];
	if(a==b) return a;
	for(int j=20;j>=0;--j) if(fa[a][j]!=fa[b][j]) a=fa[a][j],b=fa[b][j];
	return fa[a][0];
}
bool In_Road(int x,int a,int b,int y)
{
	if(x==a||x==b||x==y) return true;
	int l1=LCA(x,a),l2=LCA(x,b);
	if(l1==x&&l2==y) return true;
	if(l1==y&&l2==x) return true;
	return false;
}
void solve()
{
	int n=read(),q=read();
	f(i,1,n-1) 
	{
		int a=read(),b=read();
		eg[a].push_back(b);
		eg[b].push_back(a);
	}
	dfs(1,0);
	init(n);
	f(i,1,q)
	{
		int a=read(),b=read(),c=read(),d=read();
		int la1=LCA(a,b),la2=LCA(c,d);
		if(In_Road(la1,c,d,la2)||In_Road(la2,a,b,la1)) printf("Y\n");
		else printf("N\n");
	}
}
int main()
{
	int T=1;
	while(T--) solve();
	return 0;	
}

洛谷P4281紧急集合

题意:给一颗n节点的树,m次询问,每次给出三个节点a,b,c求一点d,使得a,b,c到d距离总和最小(n,m<5e5)
解答:在枚举三个节点个各种情况后发现,可以简化成两种情况
1:两两求LCA后的三个LCA都一样,则这个LCA就是所求的点
2:求出的三个LCA两个一样,则不一样的那个LCA就是所求的点
每种情况求三次LCA,复杂度O(mlogn),常数为3

//验证点是否在路径上 
#include<bits/stdc++.h>
using namespace std;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define ll long long
#define N 500005
inline int read(){int x;scanf("%d",&x);return x;}
vector<int>eg[N];
int fa[N][25],deep[N];
void dfs(int s,int d)
{
	deep[s]=deep[d]+1;
	fa[s][0]=d;
	for(auto v:eg[s])if(v!=d){
		dfs(v,s);
	}
}
void init(int n)
{
	for(int j=1;j<=20;++j)
		for(int i=1;i<=n;++i)
			fa[i][j]=fa[fa[i][j-1]][j-1];
}
int LCA(int a,int b)
{
	if(deep[a]<deep[b]) swap(a,b);
	for(int j=20;j>=0;--j) if(deep[fa[a][j]]>=deep[b]) a=fa[a][j];
	if(a==b) return a;
	for(int j=20;j>=0;--j) if(fa[a][j]!=fa[b][j]) a=fa[a][j],b=fa[b][j];
	return fa[a][0];
}
bool In_Road(int x,int a,int b,int y)
{
	if(x==a||x==b||x==y) return true;
	int l1=LCA(x,a),l2=LCA(x,b);
	if(l1==x&&l2==y) return true;
	if(l1==y&&l2==x) return true;
	return false;
}
int Abs(int x){
	if(x<0) return -x;
	return x;
}
void solve()
{
	int n=read(),q=read();
	f(i,1,n-1) 
	{
		int a=read(),b=read();
		eg[a].push_back(b);
		eg[b].push_back(a);
	}
	dfs(1,0);
	init(n);
	f(i,1,q)
	{
		int a=read(),b=read(),c=read();
		int la1=LCA(a,b),la2=LCA(b,c),la3=LCA(a,c);
		if(la1==la2&&la2==la3)
			printf("%d %d\n",la1,deep[a]+deep[b]+deep[c]-3*deep[la1]);
		else{
			if(la1==la2) 
				printf("%d %d\n",la3,Abs(deep[la3]-deep[a])+Abs(deep[la3]-deep[c])+Abs(deep[la1]-deep[la3])+Abs(deep[la1]-deep[b]));
			else if(la2==la3) 
				printf("%d %d\n",la1,Abs(deep[la1]-deep[a])+Abs(deep[la1]-deep[b])+Abs(deep[la1]-deep[la3])+Abs(deep[la2]-deep[c]));
			else 
				printf("%d %d\n",la2,Abs(deep[la2]-deep[b])+Abs(deep[la2]-deep[c])+Abs(deep[la1]-deep[la2])+Abs(deep[la1]-deep[a]));
		}
	}
}
int main()
{
	int T=1;
	while(T--) solve();
	return 0;	
}

洛谷P5588小猪佩奇爬树

题意:给一颗n节点树,每个节点有一个颜色,求对每个颜色有多少条长度大于一的路径包含了所有该颜色
解答:定义一个cnt[i]数组,记录遍历当前点的时候i颜色出现的次数,假设颜色i的一个“极低点”代表该点颜色为i且的子节点中没有颜色i的点,稍加分析可以得出,对于颜色i
1:有1个极低点时,颜色i所有点都是极低点的祖先节点,形成一条链,只要记录最高点位置,就能轻易求出路径数
2:有两个极低点时,如果颜色i的最高点不在两个极低点的路径上时,就形成一条以两个极低点为端点的链,否则路径不存在
3:极低点大于3时,路径不存在

PS:只有一个点时要特判

//验证点是否在路径上 
#include<bits/stdc++.h>
using namespace std;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define ll long long
#define N 1000050
#define pb push_back
inline ll read(){ll x;scanf("%lld",&x);return x;}
vector<ll>eg[N],vex[N],temp;
ll fa[N][25],deep[N],co[N],cnt[N],Ans[N],hst[N],siz[N];
void dfs(ll s,ll d)
{
	siz[s]=1;
	deep[s]=deep[d]+1;
	if(!hst[co[s]]) hst[co[s]]=s;
	fa[s][0]=d;
	for(auto v:eg[s])if(v!=d){
		dfs(v,s);
		siz[s]+=siz[v];
	}
}
void init(ll n)
{
	for(int j=1;j<=20;++j)
		for(int i=1;i<=n;++i)
			fa[i][j]=fa[fa[i][j-1]][j-1];
}
ll LCA(ll a,ll b)
{
	if(deep[a]<deep[b]) swap(a,b);
	for(int j=20;j>=0;--j) if(deep[fa[a][j]]>=deep[b]) a=fa[a][j];
	if(a==b) return a;
	for(int j=20;j>=0;--j) if(fa[a][j]!=fa[b][j]) a=fa[a][j],b=fa[b][j];
	return fa[a][0];
}
bool In_Road(ll x,ll a,ll b,ll y)
{
	if(x==a||x==b||x==y) return true;
	ll l1=LCA(x,a),l2=LCA(x,b);
	if(l1==x&&l2==y) return true;
	if(l1==y&&l2==x) return true;
	return false;
}
ll find_fa(ll u,ll v)
{
	for(int j=20;j>=0;--j) if(deep[fa[u][j]]>deep[v]) u=fa[u][j];
	return u;
}
void dfs2(ll s,ll d)
{
	ll now=cnt[co[s]];
	for(auto v:eg[s])if(v!=d){
		dfs2(v,s);
	}
	if(now==cnt[co[s]]) vex[co[s]].pb(s);
	++cnt[co[s]];
}
void solve()
{
	ll n=read();
	f(i,1,n) co[i]=read();
	f(i,1,n-1)
	{
		ll a=read(),b=read();
		eg[a].pb(b);
		eg[b].pb(a);
	}
	dfs(1,0);
	init(n);
	dfs2(1,0);
	f(i,1,n)
	{
		if(vex[i].empty()) Ans[i]=(n-1)*n/2;
		else if(vex[i].size()==1)
		{
			ll v=hst[i];
			if(v==vex[i][0])
			{
				temp.clear();
				for(auto u:eg[v])if(u!=fa[v][0]) temp.pb(u);
				ll sum=0,sie=temp.size();
				f(i,0,sie-1) f(j,i+1,sie-1) sum+=siz[temp[i]]*siz[temp[j]];
				sum+=siz[v]*(n-siz[v]+1)-1;
				Ans[i]=sum;
			}
			else
			{
				ll u=find_fa(vex[i][0],v);
				Ans[i]=siz[vex[i][0]]*(n-siz[u]);
			}
		}
		else if(vex[i].size()==2)
		{
			ll u=hst[i];
			if(!In_Road(u,vex[i][0],vex[i][1],LCA(vex[i][0],vex[i][1]))) continue;
			Ans[i]=siz[vex[i][0]]*siz[vex[i][1]];
		}
		else Ans[i]=0;
	}
	f(i,1,n) printf("%lld\n",Ans[i]);
}
int main()
{
	int T=1;
	while(T--) solve();
	return 0;	
}

洛谷P5536核心城市

题意:给一个n节点的树,选k个核心点(k个核心点必须在同一个连通块),使得非核心点到最近的核心点最大的距离最小
解答:[贪心]先求出直径的中点,即中心,可以知道这个点一定在核心点中(假设不在,那最终非核心点距离最近的核心点一定大于L/2(L为直径),与中心是核心点时最大距离为L/2相悖,因此第一个点选这个点准没错)。
然后,设与中心相邻的点为vi,下一个点就选max(vi|Li最大)(Li代表vi可延伸出去的最大距离)。(证明:如果不选这个点,非核心点到核心点的距离一定大于max(Li),选了有可能会变小,因此选它准没错)

//验证点是否在路径上 
#include<bits/stdc++.h>
using namespace std;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define ll long long
#define N 100050
#define pb push_back
inline int read(){int x;scanf("%d",&x);return x;}
vector<int>eg[N];
struct Max_node{
	int v,w;
	Max_node(int vv=0,int ww=0):v(vv),w(ww){}
}ml[N],sl[N];
int mv[N],vis[N];
void dfs1(int s,int d)
{
	mv[s]=1;
	for(auto v:eg[s])if(v!=d)
	{
		dfs1(v,s);
		mv[s]=max(mv[v]+1,mv[s]);
		Max_node temp(v,mv[v]);
		if(temp.w>ml[s].w) swap(temp,ml[s]);
		if(temp.w>sl[s].w) swap(temp,sl[s]); 
	}
}
int Ans_v,Ans_w=9999999;
void dfs2(int s,int up)
{
	int Max=0;
	if(ml[s].w>up) 
	{
		if(ml[s].w<Ans_w) Ans_w=ml[s].w,Ans_v=s;
	} 
	else
	{
		if(up<Ans_w) Ans_w=up,Ans_v=s;return ;
	}
	if(ml[s].v) dfs2(ml[s].v,max(up+1,sl[s].w+1));
}
int lgst[N];
void dfs3(int s,int d)
{
	lgst[s]=1;
	for(auto v:eg[s])if(v!=d){
		dfs3(v,s);
		lgst[s]=max(lgst[s],lgst[v]+1);
	}
}
struct Node{
	int v,w;
	Node(int vv,int ww):v(vv),w(ww){}
	friend bool operator<(Node a,Node b){
		return a.w<b.w;
	}
};
priority_queue<Node>qn;
void solve()
{
	int n=read(),k=read();
	f(i,1,n-1)
	{
		int a=read(),b=read();
		eg[a].pb(b);
		eg[b].pb(a);
	}
	dfs1(1,0);
	dfs2(1,0);
	dfs3(Ans_v,0);
	qn.push(Node(Ans_v,lgst[Ans_v]));
	while(k--)
	{
		int v=qn.top().v;
		vis[v]=1;
		qn.pop();
		for(auto u:eg[v]) if(!vis[u]) qn.push(Node(u,lgst[u]));
	}
	printf("%d\n",qn.top().w);
}
int main()
{
	int T=1;
	while(T--) solve();
	return 0;	
}

洛谷P1273有线电视网

题意:给一颗n节点有权树(每条边都有个权值),树的每个叶子节点都有一个贡献,当选取几个叶子结点时,会得到的分数为(叶子结点贡献和)-(根节点到这些叶子结点的权值和),问满足分数不为负的情况下,最多能选择多少叶子结点。
题解:dp[i][j]代表以i为根节点的子树选j个叶子结点得到的最多分数。根节点与子节点形成分组背包,但是有一些细节处理,比如dp数组要初始化为负无穷。

#include<bits/stdc++.h>
using namespace std;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define ll long long
#define N 3505
#define INF 999999999
#define pb push_back
#define mp make_pair
#define fi first
#define se second
inline ll read(){ll x;scanf("%lld",&x);return x;}
vector<pair<int,int> >eg[N];
int siz[N],dp[N][N];
void dfs1(int s,int d)
{
	if(siz[s]==1) return ;
	for(auto pv:eg[s])
	{
		int v=pv.fi;
		dfs1(v,s);
		siz[s]+=siz[v];
	}
}
void dfs2(int s,int d)
{
	for(auto pv:eg[s])
	{
		int v=pv.fi,w=pv.se;
		dfs2(v,s);
		for(int i=siz[s];i>=0;--i)
			for(int k=0;k<=siz[v]&&k<=i;++k)
				dp[s][i]=max(dp[s][i],dp[s][i-k]+dp[v][k]-w);
	}
}
void solve()
{
	int n=read(),m=read();
	f(i,1,n-m)
	{
		int k=read();
		f(j,1,k) eg[i].pb(mp(read(),read()));
	}
	f(i,1,n) f(j,1,n) dp[i][j]=-INF;
	f(i,n-m+1,n) dp[i][1]=read(),siz[i]=1;
	dfs1(1,0);
	dfs2(1,0);
	for(int i=siz[1];i>=0;--i) if(dp[1][i]>=0){
		cout<<i<<endl;
		return ;
	}
}
int main()
{
	ll T=1;
	while(T--) solve();
	return 0;
} 

HDU6035Colorful tree

题意:给一颗n节点树,每个节点有一个颜色(范围在1-n),求所有路径经过颜色种类数的和,即求:
\(\sum_{i=0}^n\)\(\sum_{j=1}^n{Color(i,j)}\)(其中Color(i,j)代表路径ij上的颜色有几种)
题解:求每条路径的颜色数等价于求每种颜色有几条路径经过的数量和,再将每种颜色经过的路径数转化成每种颜色有几条路径没经过,一次dfs可解。

#include <bits/stdc++.h>
using namespace std;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define N 100005
#define ll long long
inline ll read(){ll x;scanf("%lld",&x);return x;}
ll sum[N],color[N],siz[N];
vector<ll>eg[N];
ll ans;
void DFS(ll s,ll d)
{
	siz[s]=1;
	for(ll v:eg[s]) if(v!=d)
	{
		ll last = sum[color[s]];
		DFS(v,s);
		siz[s]+=siz[v];
		ll now = sum[color[s]];
		ll d1=now-last,d2=siz[v]-d1;
		ans=(ans+d2*(d2-1)/2);
		sum[color[s]]+=d2; 
	}
	sum[color[s]]++;
}
int main() 
{
	ll n,Cas=0;
    while(~scanf("%lld",&n))
    {
    	++Cas;
    	ans=0;
    	memset(sum,0,sizeof sum);
    	f(i,1,n) color[i]=read(),eg[i].clear();
    	f(i,1,n-1)
    	{
    		ll x=read(),y=read();
    		eg[x].push_back(y);
    		eg[y].push_back(x);
		}
		DFS(1,0);
		f(i,1,n) ans+=(n-sum[i])*(n-sum[i]-1)/2;
		ans=n*n*(n-1)/2-ans;
		printf("Case #%lld: %lld\n",Cas,ans);
	}
    return 0;
}

原文地址:https://www.cnblogs.com/Tiork/p/14203246.html