几道查询树上点之间的路径的题目

本文解析了五道涉及树形结构的经典算法题,包括边权和求和、最长异或路径、最小距离和点、特定路径求和及异或路径更新等。通过DFS、Trie树等方法给出了详细解答。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

T1:

A 求和

时间限制: 1 Sec  空间限制: 256 MB

输入输出文件名:A.in,A.out

题目描述

给出一棵以1为根的有n个节点的树,树上每条边都有其边权。

求所有点对之间的路径上的边权和的总和。

输入格式:

第一行为n

接下来n-1行,每行三个整数,分别表示一条边的两端点编号和边权。(编号为1..n

输出格式:

输出一个数字表示总和

输入样例

4

1 2 10

2 3 10

1 4 20

 

输出样例

130

样例解释

1->2:10 , 1->3:20 , 1->4:20 , 2->3:10 , 2->4:30 , 3->4:40 , 总和为130

数据范围

对于30%的数据,1<=n<=300

对于80%的数据,1<=n<=3000

对于100%的数据,1<=n<=100000,边权为<=100的正整数。


这题的做法是考虑每条边对答案的贡献(套路题QAQ)

即考虑有多少条路径经过这条边

易知即边左边的点的数目*另一边的点的数目

我们可以先从根节点dfs一遍求出每个节点的子树的大小

即一边的点的数目

这样就可以得到贡献为 cost*size[e[k].to]*(n-size[e[k].to])

#include<cstdio>
#include<cstring>
const int N=100005;
struct node
{
	int to,next,c;
}e[N*2];
int first[N],visit[N],size[N];
int cnt=0;
int n;
void insert(int u,int v,int c)
{
	e[++cnt].to=v;e[cnt].next=first[u];first[u]=cnt;e[cnt].c=c;
	e[++cnt].to=u;e[cnt].next=first[v];first[v]=cnt;e[cnt].c=c;
}
long long ans=0;long long sum=0;
void dfs(int x)
{	
	visit[x]=1;size[x]=1;
	for(int k=first[x];k;k=e[k].next)
	if(!visit[e[k].to])
	{
		dfs(e[k].to);
		size[x]+=size[e[k].to];
	}
}
void dfs2(int x)
{
	visit[x]=1;
	for(int k=first[x];k;k=e[k].next)
	{
		if(!visit[e[k].to])
		{
			ans+=e[k].c*size[e[k].to]*(n-size[e[k].to]);
			dfs2(e[k].to);
		}	
	}
}
int main()
{
	scanf("%d",&n);
	int u,v,c;
	for(int i=1;i<n;i++)	
	{
		scanf("%d %d %d",&u,&v,&c);
		insert(u,v,c);
	}
	dfs(1);
	memset(visit,0,sizeof(visit));
	dfs2(1);
	printf("%lld\n",ans);
	return 0;

}

T2:

Xor路

(xor.pas/c/cpp) 128MB 1s

给定一棵有N个点和N-1条边的树,请你求出树中的最长路径,以及总共有多少条最长路径。

这里路径长度是用xor定义的,即若经过的边的权值为a1, a2, a3,...,an,则这条路径的总权值为 a1 xor a2 xor a3 ... xor an。

输入格式

1行为一个正整数 N,为点的个数。

2行至第N行,每行包含三个正整数x,y,z,表示x和y之间有一条权值为z的边。

输出格式

仅一行,包含两个数字,为最长路径的长度和条数。

样例输入

4

1 2 3

2 4 1

1 3 4

样例输出

7 1

样例解释

2-1-3 这条路径,长度为3 xor 4=7。

数据范围

全部数据满足 0<=z<=1e9

(请注意栈空间溢出)


由xor的性质可得

两个点的路径即为根节点到一个点的路径^根节点到另一个点的路径

然后又由xor的性质可得

高位上两个值的二进制表示不同

异或的结果就为1(这样是最优的)

所以想到建一颗trie树

维护对一个值的最优异或结果

并统计方案数

#include<cstdio>
#include<cstring>
const int N=200005;
struct node
{
	int next,to,c;
}e[N*2];
int qu[N+100],dis[N];int cnt=0;int first[N],visit[N];
int l[N*31][2];
int h[N*31];
void insert(int u,int v,int c)
{
	e[++cnt].to=v;e[cnt].next=first[u];e[cnt].c=c;first[u]=cnt;
	e[++cnt].to=u;e[cnt].next=first[v];e[cnt].c=c;first[v]=cnt;
}
void bfs()
{
	int head=1,tail=2;
	qu[1]=1;
	while(head<=tail)
	{
		int rr=qu[head++];visit[rr]=1;
		for(int k=first[rr];k;k=e[k].next)
		if(!visit[e[k].to])
			visit[e[k].to]=1,qu[tail++]=e[k].to,dis[e[k].to]=dis[rr]^e[k].c;
	}
}
int k=0;
int tot=0;
void insert(int x)
{
	int ro=0;
	for(int i=30;i>=0;i--)
	{
		int cnt=(x&(1<<i))>>i;
		if(!l[ro][cnt])	l[ro][cnt]=++tot;
		ro=l[ro][cnt];
	}
	h[ro]++;
}
int find(int x)
{
	int ro=0;
	for(int i=30;i>=0;i--)
	{
		int cnt=(x&(1<<i))>>i;
		if(l[ro][!cnt])	ro=l[ro][!cnt],k+=(1<<i);
		else ro=l[ro][cnt];
	}
	return h[ro];
}
int main()
{
	freopen("xor.in","r",stdin);
	freopen("xor.out","w",stdout);
	int n;
	scanf("%d",&n);
	int u,v,c;
	for(int i=1;i<n;i++)
		scanf("%d %d %d",&u,&v,&c),insert(u,v,c);
	bfs();
	for(int i=1;i<=n;i++)	insert(dis[i]);
	long long int max=0,sum=0;
	for(int i=1;i<=n;i++)
	{
		k=0;int p=find(dis[i]);
		if(k>max)		max=k,sum=p;
		else if(k==max)	sum+=p;
	}
	printf("%lld %lld\n",max,sum>>1);
	fclose(stdin);
	fclose(stdout);
	return 0;
}

                                        

                            T3

时间限制: 1 Sec  空间限制: 256 MB

输入输出文件名:B.in,B.out

题目描述

给出一棵以1为根的有n个节点的树,树上每条边都有其边权。

四条琉璃想选择一个点作为起点,他希望这个起点到其他所有点距离和是最小的。

 

输入格式:

第一行为n

接下来n-1行,每行三个整数,分别表示一条边的两端点编号和边权。(编号为1..n

输出格式:

一个数,表示那个最小的距离和。

输入样例

5

1 2 10

2 3 10

1 4 20

2 5 10

 

输出样例

60

样例解释

2作为起点。2->1:10,2->3:10,2->4:30,2->5:10,总和为60

数据范围

对于40%的数据,1<=n,m<=2333

对于100%的数据,1<=n,m<=300 000,边权为<=100的正整数


这道题很多种方法可以做

方法一:

发现可以O(1)转移

先求出一个节点的答案

然后从一个点转移到另一个点

就+e[k].c*(n-size[e[k].to])-e[k].c*size[e[k].to]

其实就是求的树的重心

方法二:s

双向树形dp

第一次dfs从儿子上传信息到父亲

即维护一个点到它的子树内每个点的距离之和(big数组)

第二次dfs从父亲下传信息到儿子

即维护到子树外的点的距离之和(f数组)(父亲和兄弟)

转移方程看代码吧

#include<cstdio>
#include<cstring>
const int N=300050;
struct node
{
	int next,to,c;
}e[N*2];
int first[N];
int cnt=0;
long long int f[N];
int	 n;
void insert(int u,int v,int c)
{
	e[++cnt]=(node){first[u],v,c};first[u]=cnt;
	e[++cnt]=(node){first[v],u,c};first[v]=cnt;
}
long long int big[N];
int size[N],visit[N];
void dfs1(int x)
{
	visit[x]=1;size[x]=1;
	for(int k=first[x];k;k=e[k].next)
	if(!visit[e[k].to])
	{
		dfs1(e[k].to);
		size[x]+=size[e[k].to];
		big[x]+=big[e[k].to]+(long long )e[k].c*size[e[k].to];
	}
}
void dfs2(int x)
{
	visit[x]=1;
	for(int k=first[x];k;k=e[k].next)
	if(!visit[e[k].to])
	{
		f[e[k].to]=f[x]+(big[x]-big[e[k].to]-(long long )size[e[k].to]*e[k].c)+(n-size[e[k].to])*(long long )e[k].c;
		dfs2(e[k].to);
	}
}
int main()
{
	freopen("B.in","r",stdin);
	freopen("B.out","w",stdout);
	scanf("%d",&n);
	int u,v,c;
	for(int i=1;i<n;i++)
	{
		scanf("%d %d %d",&u,&v,&c);
		insert(u,v,c);
	}
	dfs1(1);
	memset(visit,0,sizeof(visit));
	dfs2(1);
	long long min=1e17;
	
	for(int i=1;i<=n;i++)
	min=min<big[i]+f[i]?min:big[i]+f[i];
	printf("%lld\n",min);
	return 0;
}

T4:

题面因为某些原因不能搬啦

注意到路径的一端肯定是叶子

所以套路其实就跟T1一样啦

算一下子树内的叶子数就可以了

然后注意根节点如果只有一个儿子也可以是路径的终点(其实就跟叶子一样啦)

#include<cstdio>
#include<cstring>
const int N=100070;
struct node
{
	int to,next,c;
}e[N*2];
int cnt;
int first[N];
void insert(int u,int v,int c)
{
	e[++cnt]=(node){v,first[u],c};first[u]=cnt;
	e[++cnt]=(node){u,first[v],c};first[v]=cnt;
}
int read()
{
	int ans=0;char t=getchar();
	while(t<'0'||t>'9')	t=getchar();
	while(t>='0'&&t<='9')	ans=ans*10+t-'0',t=getchar();
	return ans;
}
int visit[N],size[N],leave[N];
void dfs(int x)
{
	visit[x]=1;size[x]=1;
	int sum=0;
	for(int k=first[x];k;k=e[k].next)
	if(!visit[e[k].to])
	{
		sum++;
		dfs(e[k].to);
		size[x]+=size[e[k].to];
		leave[x]+=leave[e[k].to];
	}
	if(x==1&&sum==1)	leave[x]+=1;
	if(!sum)	leave[x]=1;
}
unsigned long long ans=0;
int n,m;
void dfs2(int x)
{
	visit[x]=1;
	for(int k=first[x];k;k=e[k].next)
	if(!visit[e[k].to])
	{
		ans+=(unsigned long long)leave[e[k].to]*(n-size[e[k].to])*e[k].c;
		ans+=(unsigned long long)(leave[1]-leave[e[k].to])*size[e[k].to]*e[k].c;
		dfs2(e[k].to);
	}
}
int main()
{
	freopen("senso.in","r",stdin);
	freopen("senso.out","w",stdout);
	n=read(),m=read();
	int l,u,v;
	for(int i=1;i<=m;i++)
	{
		l=read(),u=read(),v=read();
		insert(u,v,l);
	}	
	dfs(1);
	memset(visit,0,sizeof(visit));
	dfs2(1);
	printf("%llu\n",ans);
	fclose(stdin);
	fclose(stdout);
	return 0;
}

T5:

异或又来辣

求树上两点之间的路径的异或和

如果没有修改直接dfs就可以了

如果加上点修改

那么注意到修改一个点只会对该点的子树的dis产生影响

而子树的dfs序是连续的

所以其实不用树链剖分

直接线段树就可以了

#include<cstdio>
#include<cstring>
#define lson ro<<1
#define rson ro<<1|1
const int N=500090,mod=100000007;
struct edgt
{
	int to,next,v;
}e[N<<1];

int read()
{
	int ans=0;char t=getchar();
	while(t<'0'||t>'9')	t=getchar();
	while(t>='0'&&t<='9')	ans=ans*10+t-'0',t=getchar();
	return ans;
}
int cnt;
int first[N];
void insert(int u,int v,int c)
{
	e[++cnt]=(edgt){v,first[u],c};first[u]=cnt;
	e[++cnt]=(edgt){u,first[v],c};first[v]=cnt;
}
int visit[N],dis[N];
void dfs(int x)
{
	visit[x]=1;
	for(int k=first[x];k;k=e[k].next)
	if(!visit[e[k].to])
	{
		dis[e[k].to]=dis[x]^e[k].v;
		dfs(e[k].to);
	}
}
int a[30];
long long ans[(1<<20)+7];
int main()
{
	freopen("mahou.in","r",stdin);	
	freopen("mahou.out","w",stdout);	
	int n=read(),q=read(),k=read();
	int u,v,c;
	for(int i=1;i<n;i++)
	{
		u=read(),v=read(),c=read();
		insert(u,v,1<<(c-1));
	}
	for(int i=1;i<=k;i++)	a[i]=read();
	int top=(1<<k)-1;
	ans[0]=1;
	for(int i=1;i<=top;i++)
	{
		int j=i,count=1;
		ans[i]=1;
		while(j)
		{
			if(j&1)	ans[i]=ans[i]*(long long)a[count]%mod;
			j=j>>1;count++;
		}
	}
	dfs(1);
	for(int i=1;i<=q;i++)
	{
		u=read(),v=read();
		printf("%lld\n",ans[dis[u]^dis[v]]);
	}	
	fclose(stdin);
	fclose(stdout);
	return 0;
}



<think>嗯,用户想要寻关于树上差分的练习题。树上差分是处理树上路径修改的常用技巧,结合LCA(最近公共祖先)能高效解决子路径覆盖等问题。首先梳理树上差分的核心知识:最基本的两种是差分边差分。差分用于更新结信息,比如给路径(u,v)上所有加值;边差分则用于更新边的信息。用户可能需要区分这两种应用场景。回顾引用内容,引用[1]提到二叉是考试重且容易拿分,引用[2]介绍了B结构。虽然B不是直接相关,但说明用户关注形数据结构。引用[3][4]的代码示例(回文串数对求)表明用户需要算法实现能力,而引用[5]的大数据规模暗示要注重算法效率。用户明确要"练习题",需要优先提供典型题目。准备分成三个层次:基础类题目考察差分思想的理解,比如统计路径访问次数;综合类结合链剖分或DFS序等知识;拓展类涉及等高阶技巧。考虑到用户可能是算法竞赛选手或备考学生,题目来源优先选择知名OJ平台(CodeForces、洛谷),标注难度星级(★☆☆到★★★)方便选择。每个题目附带核心考说明,比如"差分+LCA"或"边权转化权"。同时准备补充学习资源链接,包括可视化动画解释差分操作,因为用户可能对"减2抵消"等操作直觉理解有困难。不过要注意引用[5]的数据规模提醒,避免推荐暴力解法的题目。最后考虑生成的相关问题要自然衔接:形扩展(链剖分)、关联算法(Tarjan求LCA)都是合理的延伸方向。特别加入树上双指针是因为引用[3]展示过双指针技巧,用户可能对这种组合题感兴趣。</think> 基于树上差分算法的核心考及题型特,以下是精选的练习题与分类指导(均需掌握差分标记的传递LCA的结合应用): --- ### **一、基础应用类题目** 1. **「POJ 3417 Network」** - **题意**:给一棵额外 M 条边,切断一条一条额外边使图不连通,求方案数。 - **考**:边差分统计覆盖次数 - **关键**:每条额外边对应树上路径 `(u, v)`,边上差分值+1 - **难度**:★★☆ 2. **「洛谷 P3128 最大流」** - **题意**:树上权修改,查询路径权最大值。 - **考**:差分路径修改 + 线段合并 / 树上倍增 - **技巧**:差分后DFS回溯时更新子最大值 - **数据规模**:$N \leq 5 \times 10^4$(引用[5]提醒注意复杂度) - **难度**:★☆☆ --- ### **二、综合技巧类题目** 3. **「CodeForces 739B 子查询」** - **题意**:每个节的权值为$v_i$,查询 u 的子中满足 $dist(u, v) \leq a_v$ 的节数。 - **考**:差分 + 树上二分 + DFS序 - **差分实现**:从根到当前节做差分,回溯时二分合法范围 - **难度**:★★★ 4. **「HDU 5452 最小割」** - **题意**:加 M 条额外边,求最少删除边数使图不连通。 - **考**:边差分 + 贪心 - **优化**:仅删除差分值为1的边(无冗余覆盖) - **数据要求**:$N, M \leq 2 \times 10^5$(参考引用[5]) - **难度**:★★☆ --- ### **三、高阶拓展类题目** 5. **「SDOI 2015 寻宝游戏」** - **题意**:动态维护树上的关键集,求遍历所有的最短路径。 - **考**:虚差分 + LCA - **技巧**:按DFS序排序后相邻差分求 - **思维提升**:理解虚本质是路径压缩 - **难度**:★★★★ --- ### **▮ 树上差分核心模板代码(C++)** ```cpp // 边差分示例(给路径(u,v)所有边+val) void update_path(int u, int v, int val) { int lca = getLCA(u, v); diff[u] += val; diff[v] += val; diff[lca] -= 2 * val; // 边差分特性:抵消lca到根的增量 } // DFS回溯统计(边权->权) void dfs(int u, int fa) { for (int v : tree[u]) { if (v == fa) continue; dfs(v, u); diff[u] += diff[v]; // 累加子差分值 } } ``` --- ### **💡 学习建议** 1. **先导知识**:熟练掌握LCA的两种求法(倍增/Tarjan)(引用[2]强调自平衡结构) 2. **实战技巧**: - **差分**:路径修改 `diff[u] += val, diff[v] += val, diff[lca] -= val, diff[par[lca]] -= val` - **边差分**:将边权下放至子节(如`u→v`的边权记在v上) 3. **复杂度验证**:差分本身$O(n)$,结合LCA后整体$O(n \log n)$(引用[5]注意大常数) > 附:扩展阅读 > - 《算法竞赛进阶指南》0x65节 - 树上差分与最近公共祖先 > - OI Wiki 差分专题:https://oi-wiki.org/basic/prefix-sum/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值