树--刷题笔记
- 树的直径
- 树的中心
- 树的重心
- 洛谷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;
}