首页 > 其他分享 >树上问题4题

树上问题4题

时间:2023-08-21 11:45:09浏览次数:33  
标签:fa int 路径 问题 dep 直径 树上 dis

P1600

首先肯定要用到 LCA,先用DFS预处理每个节点的第 \(2^k\) 代祖先和每个节点的深度,倍增计算LCA即可。

最直接的做法是模拟每个人跑步的过程,但这种方法的最差复杂度是 \(O(nm)\),肯定会超。

考虑优化模拟过程,将模拟玩家改为枚举观察员,计算有几名玩家会为观察员做贡献。

对于观察员 \(P\),如果它位于 \((s_i,t_i)\) 的跑步路径上,可以分情况讨论选手能不能为 \(P\) 作贡献。

  1. \(P\) 位于 \((s_i,LCA(s_i,t_i))\) 上:当 \(dep[s_i]-dep[P]=W[P]\) 时,可以做出贡献。
  2. \(P\) 位于 \((LCA(s_i,t_i),t_i)\) 上:当 \(d(s_i,t_i)-dep[t_i]-dep[P]=w[P]\) 时,可以做出贡献。

但无论是哪种情况,可以贡献的起点或终点一定在以 \(P\) 为根的子树上,这给我们一边回溯一边统计创造了遍历。

对于树上任意一个起点和终点,将其产生的贡献放入一个桶中,回溯时到桶里面查询贡献。但是注意:以 \(P\) 为根递归整棵子树过程中在桶内产生的差值才是对 \(P\) 真正的贡献。

还有一种特殊情况:如果路径的起点或终点为 \(LCA\),且可以被观察到,那么会重复计算一次,应该减去。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=6e5+9;
ll n,m,s[N],t[N],w[N],ans[N],dep[N],p[N][29],dist[N],cnts[N];
ll b1[N],b2[N];
vector<int> to[N];
vector<int> end_set[N],lca_set[N];
void dfs_pre(int x,int fa){
	dep[x]=dep[fa]+1;  p[x][0]=fa;
	for(int i=1;i<=28;i++)  p[x][i]=p[p[x][i-1]][i-1];
	for(int i=0;i<to[x].size();i++){
		int y=to[x][i];
		if(y==fa)  continue;
		dfs_pre(y,x);
	}
}
int LCA(int u,int v){
	if(dep[u]>dep[v])  swap(u,v);
	for(int i=28;i>=0;i--){
		if(dep[p[v][i]]>=dep[u])  v=p[v][i];
	}
	if(u==v)  return u;
	for(int i=28;i>=0;i--){
		if(p[u][i]!=p[v][i]){
			u=p[u][i];  v=p[v][i];
		}
	}
	return p[u][0];
}
void dfs(int x,int fa){
	int bkt1=b1[w[x]+dep[x]],bkt2=b2[w[x]-dep[x]];
	for(int i=0;i<to[x].size();i++){
		int y=to[x][i];
		if(y==fa)  continue;
		dfs(y,x);
	}
	b1[dep[x]]+=cnts[x];
	for(int i=0;i<end_set[x].size();i++){
		int y=end_set[x][i];
		b2[dist[y]-dep[t[y]]]++;
	} 
	ans[x]+=(b1[w[x]+dep[x]]+b2[w[x]-dep[x]]-bkt1-bkt2);
	for(int i=0;i<lca_set[x].size();i++){
		int y=lca_set[x][i];
		b1[dep[s[y]]]--;
		b2[dist[y]-dep[t[y]]]--;
	}
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n-1;i++){
		int u,v;  cin>>u>>v;
		to[u].push_back(v);  to[v].push_back(u);	
	}
	for(int i=1;i<=n;i++)  cin>>w[i];
	for(int i=1;i<=m;i++)  cin>>s[i]>>t[i];
	dfs_pre(1,0);
	for(int i=1;i<=m;i++){
		int lca_st=LCA(s[i],t[i]);
		dist[i]=dep[s[i]]+dep[t[i]]-2*dep[lca_st];
		cnts[s[i]]++;
		end_set[t[i]].push_back(i);
		lca_set[lca_st].push_back(i);
		if(dep[lca_st]+w[lca_st]==dep[s[i]])  ans[lca_st]--;
	}
	dfs(1,0);
	for(int i=1;i<=n;i++)  cout<<ans[i]<<" ";
	return 0;
}

P1967

题目大意:给出一张 \(n\) 点 \(m\) 边的带权无向图,求任意两点间路径最小边权最大值,无法到达输出 \(-1\)。

首先考虑无解的情况,可以使用并查集判定。每输入一条边,就把这条边的两个端点并入同一个集合。问询时如果起点和终点不在同一个集合内,就输出 \(-1\)。

对于有解的问询,解法较多。

  1. 最大生成树+倍增:对每一个连通块构造最大生成树,将原问题转化为 “树上 \(x\) 到 \(y\) 路径之间的最小权值”,可以使用倍增求解。

  2. Kruskal重构树:Kruskal重构树 详解。对于本题,在 Kruskal 的过程中,若当前 \(u,v\) 两点不在同一个并查集内,则新建一个节点 \(node\),其点权为 \((u,v)\) 的边权,接着 \(root(u)=node,root(v)=node\)。

    重构完成后,制定每个集合的根作为所在森林的根,则 \((u,v)\) 路径上的最小边权最大值就是 \(lca(u,v)\) 的点权。

  3. 最大生成树+set:要求x到y所有的路径中最小边长的最大值,可以贪心地加边,依照边从大往小的方式往里添加,然后合并并查集。 每次当查询分布在两个待合并的并查集的时候,当前的边长就是这次查询的答案。

    我们对每个并查集维护一个集合,集合中保存的内容就是一个点在这个并查集中,而另一个点不在这个并查集中的询问编号。

    当待合并的两个并查集所具有的集合里面拥有相同的询问编号时候,回答这个询问编号,然后把小的集合向大的集合合并,并将回答完的询问编号从集合中移除。

此处给出方法 \(2\) 的代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
struct edge{
	int u,v,w;
}E[100009];
bool vst[100009]; 
int n,m,q,tot,s[100009],t[100009],fa[100009],val[100009],dep[100009];
int p[100009][19];
vector<int> to[100009];
int root(int x){
	return fa[x]==x?x:fa[x]=root(fa[x]);
}
bool cmp(const edge&a,const edge&b){
	return a.w>b.w;
}
void dfs_fa(int x,int f){
	p[x][0]=f;  vst[x]=1;  dep[x]=dep[f]+1;
	for(int i=1;i<=18;i++)  p[x][i]=p[p[x][i-1]][i-1];
	for(int i=0;i<to[x].size();i++){
		if(to[x][i]==f)  continue;
		dfs_fa(to[x][i],x);
	}
	return ;
}
int lca(int u,int v){
	if(dep[u]>dep[v])  swap(u,v);
	for(int i=18;i>=0;i--){
		if(dep[p[v][i]]>=dep[u])  v=p[v][i];
	}
	if(u==v)  return u;
	for(int i=18;i>=0;i--){
		if(p[u][i]!=p[v][i]){
			u=p[u][i];  v=p[v][i];
		}
	}
	return p[u][0];
}
void kruskal(){
	for(int i=1;i<=n;i++)  fa[i]=i;
	for(int i=1;i<=m;i++){
		int fu=root(E[i].u),fv=root(E[i].v);
		if(fu==fv)  continue;
		val[++tot]=E[i].w;
		fa[tot]=fa[fu]=fa[fv]=tot;
		to[tot].push_back(fu);  to[fu].push_back(tot);
		to[tot].push_back(fv);  to[fv].push_back(tot);
	}
	for(int i=1;i<=tot;i++){
		if(vst[i])  continue;
		int fi=root(i);
		dfs_fa(fi,0);
	}
}
int main(){
	cin>>n>>m;  tot=n;
	for(int i=1;i<=m;i++)  cin>>E[i].u>>E[i].v>>E[i].w;
	cin>>q;
	for(int i=1;i<=q;i++)  cin>>s[i]>>t[i];
	sort(E+1,E+1+m,cmp);
	kruskal();
	for(int i=1;i<=q;i++){
		if(root(s[i])!=root(t[i]))  cout<<-1<<endl;
		else  cout<<val[lca(s[i],t[i])]<<endl;
	}
	return 0;
}

P1084

warning:如果你WA 60pts,可以检查倍增部分,应该先增加用时再往上跳

题目要我们最小化答案,因为答案符合单调性(如果能在 \(x\) 小时内控制,大于 \(x\) 小时一定也能控制),那么我们可以使用二分枚举答案+判定。

容易发现,军队越靠近根节点,可以控制的子节点就更多。那么根据贪心的思想,所有军队都尽量往根节点靠拢。

往上跳的过程一般使用倍增优化,那么我们可以先 DFS 一遍,将倍增用的值处理好(例如第 \(2^k\) 代祖先,跳到它需要的时间等)。

遍历每一个军队,如果当前军队可以到达根节点,那么记录它的编号的它到达根节点后可以走的路程,对于每一个子树 \(x\),记录可以到达根节点且剩余路程最短的点。反之,记录它能被“提”到的最高节点

如果一个节点建立了检查点或者它的所有子树都设立了检查点,则说明以这个节点为根的子树已经被“封死”。记录根节点的所有子树中,未被“封死”的子树。

将我们记录好的能到根节点的军队按剩余路程从大到小排序,将未被封死的子树按到根节点的距离从远到进排序。依次处理未被封死的子树要由哪支军队管辖。优先安排在同一颗子树中的军队,如果没有,再查看当前没有被使用的军队里剩余路程最大的是否可以到达这颗子树。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=50009;
bool vst[N],need[N];
ll n,m,LOG2N,id[N],fa[N][19],dis[N][19],dep[N],rst_need[N];
vector<ll> to[N],w[N];
struct node{ll id,rst;}abled_army[N];
ll rst_army[N];
bool cmp(const node&a,const node&b){
	return a.rst<b.rst;
}
void init(){
	memset(vst,0,sizeof(vst));
	memset(need,0,sizeof(need));
	memset(rst_need,0,sizeof(rst_need));
	memset(rst_army,0,sizeof(rst_army));
	memset(abled_army,0,sizeof(abled_army));
}
void dfs_pre(int x,int f){
	for(int i=1;i<=LOG2N;i++){
		fa[x][i]=fa[fa[x][i-1]][i-1];
		dis[x][i]=dis[x][i-1]+dis[fa[x][i-1]][i-1];
	}
	for(int i=0;i<to[x].size();i++){
		int y=to[x][i];
		if(y==f)  continue;
		fa[y][0]=x;  dis[y][0]=w[x][i];
		dfs_pre(y,x);
	}
}
bool dfs(int x,int fa){
	if(vst[x])  return 1;
	if(to[x].size()==1)  return 0;
	for(int i=0;i<to[x].size();i++){
		int y=to[x][i];
		if(y==fa)  continue;
		if(!dfs(y,x))  return 0;
	}
	return 1;
}
bool ok(int mid){
	int cnta=0,cnta2=0,cntb=0;
	init();
	for(int i=1;i<=m;i++){
		ll x=id[i],use=0;
		for(int j=LOG2N;j>=0;j--){
			if(fa[x][j]>1&&use+dis[x][j]<=mid){
				use+=dis[x][j];  x=fa[x][j];  
			}
		}
		if(fa[x][0]==1&&use+dis[x][0]<=mid){
			abled_army[++cnta].rst=mid-(use+dis[x][0]);
			abled_army[cnta].id=x;
		}
		else  vst[x]=1;
	}
	for(int i=0;i<to[1].size();i++){
		if(!dfs(to[1][i],1))  need[to[1][i]]=1;
	}
	sort(abled_army+1,abled_army+cnta+1,cmp);
	for(int i=1;i<=cnta;i++){
		int id_now=abled_army[i].id,rst_now=abled_army[i].rst;
		if(need[id_now]&&rst_now<dis[id_now][0])  need[id_now]=0;
		else  rst_army[++cnta2]=rst_now;
	}
	for(int i=0;i<to[1].size();i++){
		if(need[to[1][i]])  rst_need[++cntb]=dis[to[1][i]][0];
	}
	if(cnta2<cntb)  return 0;
	sort(rst_army+1,rst_army+1+cnta2);
	sort(rst_need+1,rst_need+1+cntb);
	int i=1,j=1;
	while(i<=cnta2&&j<=cntb){
		if(rst_army[i]>=rst_need[j])  j++;
		i++;
	}	
	if(j>cntb)  return 1;
	return 0;
}
int main(){
	cin>>n;
	LOG2N=log2(n)+1;
	for(int i=1;i<n;i++){
		int x,y,z;  cin>>x>>y>>z;
		to[x].push_back(y);  to[y].push_back(x);
		w[x].push_back(z);  w[y].push_back(z);
	}
	cin>>m;
	for(int i=1;i<=m;i++)  cin>>id[i];
	dfs_pre(1,0);
	int l=1,r=1e9,ans=-1;
	while(l<=r){
		int mid=l+(r-l)/2;
		if(ok(mid)){ans=mid;r=mid-1;}
		else  l=mid+1;
	}
	cout<<ans<<endl;
	return 0;
}

P1099

题目大意:给定一棵带边权无根树,在其直径上选择一段长度不超过 \(s\) 的路径,使其偏心距最小。

直径的性质

1.对于直径上任意一点 \(s\),到它本身距离最远的点一定是直径的两个端点 \(A_1,A_2\) 之一。

证明:反证法,假设在直径外存在一点 \(t\),使得 \(d(s,t)>d(s,A_1)\) 且 \(d(s,t)>d(s,A_2)\),那么 \(d(t,A_1)>d(A_1,A_2)\) 且 \(d(t,A_2)>d(A_1,A_2)\)。这违反了直径的定义。

2.对于一棵正边权的树,如果存在多条直径,一定交于一点。

证明:如果两直径 \((S_1,T_1),(S_2,T_2)\) 没有交于一点, 则对于任意的 \((a,b)\) ,除了 \(a,b\) 两点外,其它点均不在直径上(不说人话:\(∃a \in (S_1,T_1),b \in (S_2,T_2)\),且 \(∀p \in (a,b)-\{a,b\},p \notin (S_1,T_1),p \notin (S_2,T_2)\))。设 \(d_1=\max\{d(S_1,a),d(a,T_1)\}\),\(d_2=\max\{d(S_2,b),d(b,T_2)\}\),则 \(d_1,d_2 \geq \frac{d(S_1,T_1)}{2}\),则\(d_1+d_2+(a,b)\) 可以拼成一条比直径还长的路径,假设不成立。

3.对于一棵所有边权均为正的树,如果其存在多条直径,则各直径的中点(不一定恰好是某个节点,可能在某条边的内部)是唯一的。

4.若两条直径有重叠的部分,则于重叠部分同一端点引出的两条直径的非重叠的部分的长度相等。

5.若路径存在不位于直径上的部分,这条路径对应的偏心距一定不会比全部位于直径上的路径的偏心距的最小值更小。

多条直径的处理

如果一棵树有多条直径,任选一条即可。由于两条直径不可能相交,那么把相交部分看作一点,剩下部分关于这条直径对称。而如果选择的路径包含了分叉点,其偏心距就是恒定的,所以可以任选一条直径求解。

对偏心距造成影响的因素

设路径的两端点为 \(P_1,P_2\),直径的两端点为 \(A_1,A_2\),显然对答案造成影响的有 \(P_1,A_1\) 和 \(P_2,A_2\)。

但这不以为长度越长答案一定越优,因为路径上的其它点对答案也有贡献。路径上其它点 \(Q\) 对答案的贡献为 “不经过路径上其它任何点所能到达的最远距离”。这种贡献可以预处理。

解法

  1. 枚举:求出树的任意一条直径,然后在直径上枚举端点,接下来 DFS 遍历整棵树,按定义求出其它点到路径的距离,从而得到偏心距。时间复杂度 \(O(n^3)\)。
  2. 双指针枚举优化:在固定路径一端 \(s\) 的情况下,随着路径的增长,偏心距不会变大。于是可以考虑枚举一个端点,用双指针找到离 \(s\) 最远,且不超过路径上限的点,从而降低候选数量
  3. 二分:考虑二分偏心距,将最优化问题变成存在性问题。
  4. 双指针+前缀和:将解法2优化,发现其低效的原因主要在于每次求出最最优区间后都要 dfs一遍,只要在双指针过程中用前缀和维护即可。
#include<bits/stdc++.h> 
using namespace std;
const int N=5e5+9;
int n,m,k1,k2,d,id,ans=2e9,fa[N],dis[N];
bool vst[N];
vector<int> to[N];
vector<int> w[N];
void dfs(int x,int f){
	fa[x]=f;
	if(dis[x]>dis[k1])  k1=x;
	for(int i=0;i<to[x].size();i++){
		int y=to[x][i];
		if(y==f||vst[y])  continue;
		dis[y]=dis[x]+w[x][i];
		dfs(y,x);
	}
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n-1;i++){
		int x,y,z;
		cin>>x>>y>>z;
		to[x].push_back(y);
		to[y].push_back(x);
		w[x].push_back(z);
		w[y].push_back(z);
	}
	dis[1]=0;  dfs(1,0);
	dis[k1]=0;  dfs(k1,0);
	k2=k1;
	for(int i=k2,j=k2;i;i=fa[i]){
		while(dis[j]-dis[i]>m)  j=fa[j];
		d=max(dis[k2]-dis[j],dis[i]); 
		ans=min(ans,d);
	}
	for(int i=k2;i;i=fa[i])  vst[i]=1;
	for(int i=k2;i;i=fa[i]){
		k1=i,dis[k1]=0;
		dfs(i,fa[i]);
	}
	for(int i=1;i<=n;i++)  ans=max(ans,dis[i]);
	cout<<ans;
	return 0;
}

标签:fa,int,路径,问题,dep,直径,树上,dis
From: https://www.cnblogs.com/11jiang08/p/17645616.html

相关文章

  • tablestore依赖问题解决
    依赖引入最新版本<dependency><groupId>com.aliyun.openservices</groupId><artifactId>tablestore</artifactId><version>5.16.0</version></dependency>执行如下方法,报错下面2个错误信息,如下图:错误一:错误二:错误原因:JavaSDK依赖2......
  • net6的情况下遇到连接数据库问题
    最近做后端需要访问数据库,然后想用net6做一个webapimysql的话nuget上装mysql.data 这个sqlserver的话和以前的区别是以前用 System.Data.SqlClient,现在要nuget上装 这个 Microsoft.Data.SqlClient连接数据库用我比较熟悉的Dapper 目前用sqlserver数据库然后Con......
  • 项目开始,遇到的第一个问题
    一个新的技术开项目真的是不容易,这几天都在处理各种问题首先页面的问题,虽然学习了,但不熟悉vue的各种用法,只能想像项目的样子,然后布局页面,然后进行各种资料的查找学习(谢谢现在网络上信息丰富)第一就是对样式css极度不熟悉,然后想达到想要的效果不断的查资料,花了两天才弄完登陆页面,......
  • 小文件问题
    Hadoop小文件问题小文件是指比HDFS默认块大小明显小得多的文件。小文件导致了什么问题对于存储层来说,大量小文件会产生大量的元数据信息;当NN重启时,必须将元数据信息加载到内存中,大量元数据信息会导致NN重启速度非常慢;并且,太多小文件也会导致NN在DN耗尽磁盘空间之前就先耗尽内存......
  • 大抄线段树历史值问题
    历史值问题历史值:在维护序列\(A\)的同时,在每次操作后,序列\(A\)会对序列\(B\)的对应位置产生贡献。历史版本和:每次操作后,\(B_i\leftarrowB_i+A_i\)。历史最大值:每次操作后,\(B_i=\max(B_i,A_i)\)。历史版本和:给定操作:①区间加。②查询区间和。③查询区间......
  • LeetCode 周赛上分之旅 #40 结合特征压缩的数位 DP 问题
    ⭐️本文已收录到AndroidFamily,技术和职场问题,请关注公众号[彭旭锐]和BaguTreePro知识星球提问。学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭与你分享每场LeetCode周赛的解题报告,一......
  • Prim算法是一种用于解决最小生成树问题的贪心算法。它通过逐步选择边来构建最小生成树
    importjava.util.*;classPrimAlgorithm{privatestaticfinalintINF=Integer.MAX_VALUE;publicvoidprimMST(int[][]graph){intvertices=graph.length;int[]parent=newint[vertices];//用于存储最小生成树的父节点int......
  • 【8月摸鱼计划】Air780E、Luat开发平台、对应的lib库的问题
      Air780E是一款基于Luat开发平台的模组,支持LuatIDE进行任务开发。LuatIDE是专为Luat开发平台设计的集成开发环境,方便开发者进行代码编写、调试和下载。关于找不到对应的lib库的问题,可能是由于以下几个原因:1.库文件未导入:确保正确安装LuatIDE,并在项目中导入相应的库文件......
  • 西农OJ P1005 装载问题
    装载问题问题描述有两艘船,载重量分别是c1、c2,n个集装箱,重量是wi(i=1…n),且所有集装箱的总重量不超过c1+c2。确定是否有可能将所有集装箱全部装入两艘船。输入多个测例,每个测例的输入占两行。第一行一次是c1、c2和n(n<=10);第二行n个整数表示wi(i=1…n)。n等于0标志输入结束。......
  • 解决线程安全问题方式一
    1,同步代码块-格式:synchronized(对象){需要同步的代码;}-注意:这个对象,同步代码块可以解决线程安全问题的根本就在于这个对象。这个对象就好比是锁的功能。-这个对象可以是任意对象,但是多个线程必须是同一个对象。2,同步的好处:-解决了多线程中的线程安全问题3,同步的弊端-当线程很多的......