树上差分——自创例题分析+运输计划

本文介绍了如何使用树上差分解决一类具有加法和乘法操作的树形数据结构问题,具体是关于银杏树上白果生长与掉落的模拟。通过对树的节点进行加权和乘法的差分处理,实现了高效计算每个白果最终状态,判断是否掉落。文章详细阐述了加法和乘法操作的区别,以及如何利用差分数组进行离线处理,并给出了相应代码实现。
摘要由CSDN通过智能技术生成

百果之王(Kinggo.cpp)

【要求】

时间<1s
内存<256MB

【题目描述】

在一棵银杏树上有许多摇摇欲坠的 n n n 颗白果王,每个白果的初始质量都为 w w w,而且很巧的是这棵银杏树正好是一棵树。白果们日以继夜地生长,并会在质量到达一定限度 m m m w > m w > m w>m)时从树上落下。每个果子 i i i 所发之臭都会使部分白果加速生长。请你求出在从 1 1 1~ n n n颗白果的香味都发生作用后有多少白果从树上落下。

白果的香味作用有两种:

o p t = 1 opt = 1 opt=1:使 ( u , v ) (u,v) (u,v)的简单路径上的白果(包括 u , v u,v uv)质量增加 x x x

o p t = 2 opt = 2 opt=2:使 ( u , v ) (u,v) (u,v)的简单路径上的白果(包括 u u u v v v)质量变为 x x x倍。

【输入格式】

从文件: k i n g g o . i n kinggo.in kinggo.in读入数据

第一行两个整数 n , w , m n,w,m n,w,m表示树的白果的数量以及所有白果的初始质量和质量上限。

2 2 2行到第 n n n行,每行输入两个 u , v u,v u,v,表示两个节点相连(整个图是连通图)

n + 1 n + 1 n+1行到第 2 n 2n 2n行 第 n + i n + i n+i行输入四个数 o p t , u , v , x opt , u ,v, x opt,u,v,x分别表示第 i i i个白果会进行的操作类型,操作的路径的起点和终点,以及要加或乘上的数。

n n n个操作依次进行(从 1 → 1\rightarrow 1n)。

【输出格式】

输出数据到文件: k i n g g o . o u t kinggo.out kinggo.out

一行 n n n个数,每个数之间有空格.

如果第 i i i个白果掉下来,那么输出的第 i i i个数为 1 1 1;否则输出第 i i i个白果的最终质量.

【数据范围】

对于“100%”的数据都满足:

1 ≤ n ≤ 1 × 1 0 5 .    1 ≤ u , v ≤ n .    o p t ∈    1\leq n \leq1 \times 10^5.\;1 \leq u,v\leq n.\;opt\in \; 1n1×105.1u,vn.opt{ 1 , 2 1,2 1,2}且** o p t = 2 opt=2 opt=2的情况不超过 10 10 10次** . 1 ≤ x ≤ 10.    1 ≤ w ≤ m ≤ 2 × 1 0 5 \\1 \leq x \leq 10.\; 1\leq w \leq m\leq 2\times 10^5 1x10.1wm2×105

你说得对,但是:

有20%满足 1 ≤ n ≤ 5 × 1 0 3 1\leq n \leq 5\times 10^3 1n5×103

另外30% 满足所有白果的 o p t = 1 opt = 1 opt=1

以及剩下的50%满足另外50%满足的条件。

保证:

保证白果的香味不会引发池沼爆炸,也不会使任意一个白果的质量超过 2 × 1 0 9 2\times 10^9 2×109

【样例1输入】

3 3 7

1 2

2 3

1 2 3 2

2 1 1 2

1 1 3 2

【样例1输出】

1 7 7

【样例1解释】

所有果子初始质量为3,会在质量大于7时掉落。

1号果子会使2号果子到3号果子上所有点的质量增加2,2号质量变为5,3号质量变为5

2号使1号节点的质量乘2变为6

3号节点使1,2,3号节点质量都加2,最后:

w 1 = 8 , w 2 = 7 , w 3 = 7 w_1 = 8,w_2 = 7,w_3 = 7 w1=8,w2=7,w3=7

1号果子掉落输出1.而2号3号果子剩下,质量为7.


K i n g g o Kinggo Kinggo题解

本题前言:

本题可抽象为:对树上带点群的简单修改问题。

即对任意两个点形成的简单路径上的点进行加和乘的修改。由于我们只要求得出最后的结果,所以本题可以算是离线操作。

考虑 n n n的范围 5 × 1 0 5 5\times 10^5 5×105,容易想到 O ( n l o g n ) O(nlogn) O(nlogn)做法,但是这道题要用此种做法的话需要用到树链剖分,感兴趣的同学可以自己去了解一下,这里讲的是适用于本题的比较简单的做法——树上差分

基础知识:差分

常见差分:1 2 3 4 5,对[1,3]进行+2处理,可在端点1处使差分值+2,右端点3的后一点4处使差分值-2

然后得到差分数组:2 0 0 -2 0,从左到右求前缀和,你会发现最后得到的数组就是所有数的变化值

Δ \Delta Δ = 2 2 2 0 0。多次操作也是同理。

↓ \downarrow

树上差分:我们使两个端点差分值 + v a l +val +val, 使他们的 最近公共祖先 l c a lca lca及其父节点 f a [ l c a ] [ 0 ] fa[lca][0] fa[lca][0]的差分值 − v a l -val val, 然后从叶子节点遍历上去求子树权值和即可得到整棵树的变化值。

差分的好处在于一次处理的复杂度= O ( 1 ) O(1) O(1)。可以多次操作后(可以发现结果是不变的),在最后的时候再更新整棵树——只需要一次 O ( n ) O(n) O(n)处理即可。而一般的处理就是 O ( n 2 ) O(n^2) O(n2),复杂度很劣。

思路点拨:

首先考虑比较简单的加法,而之所以说其简单是因为:
我们如果站在加法的意义上考虑变化值的话,会发现加法可以忽视具体的值而对一个区间进行等同效果(在加法意义上的变化)的处理。而在加法的基础上纳入乘法则需要考虑到点本身的值,如节点权值为3,乘5后的变化值( Δ \Delta Δ=15-3=12)与3本身有关,不好进行区间处理,因为区间内的数权值可能不同。

回到加法本身上来,由于其变化值在加法意义上不与权值本身相关,我们便能想到用比较常用的差分知识,进行懒标记处理。

接下来考虑乘法,我们发现乘法的话用差分难以实现,只好从头到尾遍历一次再暴力更新乘法区间。之所以考虑暴力是因为观察到乘法的次数很少

时间复杂度 T = O ( 10 × n ) T = O(10 \times n) T=O(10×n)(10其实可以忽略不计)

一点拓展:

如果乘法和加法的次数没有限制的话,本题便基本上只能用一开始提到的树链剖分完成

同理,我们如果站在乘法的意义上考虑变化值,此时变化值就是倍数差异:

那么乘法就可以进行差分操作:我们使两个端点差分值 ∗ v a l *val val使他们的最近公共祖先 l c a lca lca及其父节点 f a [ l c a ] [ 0 ] fa[lca][0] fa[lca][0]的差分值 ÷ v a l \div val ÷val,接下来就和加法差分很类似了,但还是考虑到本题乘法操作偏少,且如果用乘法意义的话,加法会使倍数变为分数: 3 + 2 = 3 × 5 3 3 + 2 = 3 \times \frac{5}{3} 3+2=3×35,所以我们本题还是要运用加法意义上的差分。

【代码细节】

其他细节见参考代码 k i n g g o . c p p kinggo.cpp kinggo.cpp的注释

先预处理出每个节点的任意级祖先。

然后求出节点的 l c a —— f f lca——ff lca——ff

接着分读入类型进行两种操作:

o p t = 1 opt = 1 opt=1,进行差分数组的处理

del[x] += val,del[y] += val;//在x,y节点上加上权值,在其lca和lca的父节点上减去相同权值 
del[ff] -= val,del[fa[ff][0]] -= val;//lca及其父节点的差分值-val 

o p t = 2 opt =2 opt=2,将差分数组转移到原树的数据上进行修改,然后再暴力修改乘法部分

void dfs2(int u)//往下遍历
{
	for(int e = fs[u];e;e = nex[e])
	{
		int v = to[e];if(v == fa[u][0]) continue;
		dfs2(v);
		del[u] += del[v];//从子节点传上来差分值 
		del[v] = 0;
	}
	w[u] += del[u]; 
}
void dfs3(int u)//暴力更新乘法 
{
	if(dep[u] <= dep[ff]) return;//到达lca后返回 
	w[u] *= val; 
	if(fa[u][0]) dfs3(fa[u][0]);
}
........
dfs2(1),dfs3(x),dfs3(y),w[ff] *= val;

最后记得再遍历一次将差分数组转移到原树上修改即可。

另外本题实际数据其实较小,如果考虑到可能超 i n t int int,要开 l o n g l o n g longlong longlong
参考代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int n,fs[N],nex[N<<1],to[N<<1],cnt,w[N],m,del[N],mul[N];//del[]记录差分的值 
int x,y,dep[N],fa[N][24],son[N],opt,val,ff;//son[]用来找叶子结点 
void add(int x,int y){nex[++cnt]=fs[x];fs[x]=cnt;to[cnt]=y;}
void dfs(int u)//求每个点的深度和父节点
{
	for(int e = fs[u];e;e = nex[e])
	{
		int v = to[e];if(v == fa[u][0]) continue;
		dep[v] = dep[u] + 1,fa[v][0] = u,son[u] = v;
		dfs(v);
	}
}
int lca(int u,int v)                       //找lCA 
{
	if(u == v) return u;
	if(dep[u] < dep[v]) swap(u,v);           //保证u深度更大 
	for(int i = 22;i >= 0;i--)                 //使它们俩跳至深度相同 
	{
		if(dep[fa[u][i]] >= dep[v]) u = fa[u][i];
		if(u == v) return u;                 //u、v在一条链上,v恰是u和v的最近公共祖先  
	}
	for(int i=22;i>=0;i--)                 //在u和v深度相同的情况下
	{
		if(fa[u][i] != fa[v][i])               //目标位置不相等,u和v就往上跳 
		{
			u = fa[u][i];                     //u往上跳
			v = fa[v][i];                     //v往上跳
		}
	}
	return fa[u][0];                        //最后肯定一起跳到了lca的下面一个 
}
void dfs2(int u)//往下遍历
{
	for(int e = fs[u];e;e = nex[e])
	{
		int v = to[e];if(v == fa[u][0]) continue;
		dfs2(v);
		del[u] += del[v];//从子节点传上来差分值 
		del[v] = 0;
	}
	w[u] += del[u]; 
}
void dfs3(int u)//暴力更新乘法 
{
	if(dep[u] <= dep[ff]) return;//到达lca后返回 
	w[u] *= val; 
	if(fa[u][0]) dfs3(fa[u][0]);
}
int main()
{
	freopen("kinggo.in","r",stdin);
	freopen("kinggo.out","w",stdout);
	scanf("%d%d%d",&n,&x,&m);
	for(int i = 1;i <= n; i++) w[i] += x;
	for(int i = 1;i < n; i++)
	{
		scanf("%d%d",&x,&y);add(x,y),add(y,x);//加边 
	}
	dfs(1);
	for(int j = 1;j <= 22; j++) 
		for(int i = 1;i <= n; i++)
			fa[i][j] = fa[fa[i][j-1]][j-1];//倍增找祖先
	dep[0] = -1;
	for(int i = 1;i <= n; i++)
	{
		scanf("%d%d%d%d",&opt,&x,&y,&val);ff = lca(x,y);
		if(opt == 1)
		{
			del[x] += val,del[y] += val;//在x,y节点上加上权值,在其lca和lca的父节点上减去相同权值 
			del[ff] -= val,del[fa[ff][0]] -= val;//lca及其父节点的差分值-val 
		}
		else
		{
			dfs2(1),dfs3(x),dfs3(y),w[ff] *= val;//为乘法,只能遍历整个树进行更新后再暴力做乘法运算 
		}
	}
	x = y = 0;
    dfs2(1);//最后一次更新差分值 
	for(int i = 1;i <= n; i++) 
	{
		if(w[i] > m)          cout << 1 << ' ';
		else 			      cout << w[i]<< ' ';
	}
	return 0;
}

洛谷P2680 运输计划

运输计划传送门

参考代码:
//16.22-16.35
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 10;
int fs[N],cnt,nex[N<<1],to[N<<1],w[N<<1],top[N],fa[N],son[N],sz[N],dep[N],del[N],dis[N];
void add(int x,int y,int z){nex[++cnt]=fs[x];fs[x]=cnt;to[cnt]=y;w[cnt]=z;}
struct line{
	int x,y,dis;
}l[N];
int n,m,mm,ml;
int tot,num = 0;
void dfs1(int u)
{
	sz[u]  = 1;
	for(int e = fs[u];e;e = nex[e])
	{
		int v = to[e];if(v == fa[u]) continue;
		dep[v] = dep[u] + 1,fa[v]=u,dis[v] = dis[u] + w[e],dfs1(v);
		sz[u] += sz[v];
		if(sz[son[u]] <sz[v]) son[u] = v;
	}
}
void dfs2(int u)
{
	top[u] = son[fa[u]] == u? top[fa[u]] : u;
	for(int e = fs[u];e;e = nex[e])
	{
		int v = to[e];if(v == fa[u]) continue;
		dfs2(v);
	}
}
void dfs3(int u)
{
	for(int e = fs[u];e;e = nex[e])
	{
		int v = to[e];if(v == fa[u]) continue;
		dfs3(v),del[u]  += del[v];
	}
}
int lca(int x,int y)
{
	while(top[x] != top[y])
	{
		if(dep[top[x]] < dep[top[y]]) y = fa[top[y]];
		else x = fa[top[x]];//用if else 更好
	}
	return dep[x] < dep[y] ? x : y; 
}
int time(int x,int y)
{
	return dis[x] + dis[y] - (dis[lca(x,y)] << 1);
}
bool check(int x)
{
	num = 0;int hh = 0;
	for(int i = 1;i <= n; i++)	del[i] = 0;
	for(int i = 1;i <= m; i++)
	{
		if(l[i].dis > x)
		{
			num++,del[l[i].x] ++,del[l[i].y] ++ ,del[lca(l[i].x,l[i].y)]-=2;
		}
		hh = max(hh,l[i].dis - x);
	}
	if(!num) return 1;
	dfs3(1);
	for(int i = 2;i <= n; i++)
	{
		if(del[i] == num and dis[i] - dis[fa[i]] >= hh) return 1;//注意后一个条件
	}
	return 0;
}
void bisect()
{
	int l = mm - ml,r = mm,mid;//左边界为最大距离减最大边,右边界为最大距离
	while(l < r)
	{
		mid = l + r >> 1;
		if(check(mid)) r = mid;
		else l = mid + 1;
	}
	cout << l;
}
int main()
{
	scanf("%d%d",&n,&m);
	int x,y,z;
	for(int i = 1;i < n; i++)
	{
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z),add(y,x,z);
		ml = max(ml,z);
	}
	dfs1(1),dfs2(1);
	for(int i = 1;i <= m; i++)
	{
		scanf("%d%d",&x,&y);
		int d = time(x,y);
		mm = max(d,mm);
		l[++tot] = line{x,y,d};
	}
	bisect();
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值