《树上差分》小题两则

边差分:352. 闇の連鎖

题意
给定 n-1 条 主要边 构成一棵树,然后给定 m 条 附加边,每条附加边连接树中两个节点。
每次需要选择一个主要边和一个附加边删掉,将整个图分成两部分。
问,一共有多少种选择方案?

N ≤ 1 0 5 ,   M ≤ 2 ∗ 1 0 5 ,数据保证答案不超过 2 31 − 1 N≤10^5,\ M≤2*10^5,数据保证答案不超过2^{31}−1 N105, M2105,数据保证答案不超过2311

思路
对于一条附加边来说,只有其所在环上的一条主要边被砍掉,还要保证这条主要边只在这一个环上,或者是不在环中的一条主要边被砍掉,这个附加边才有贡献,不好处理。
正难则反!
反过来考虑主要边:
如果一条主要边砍掉后能够将整个图分成两部分,那么还要砍掉其所在环中的所有附件边。

  • 如果其只在一个环中,那么将其附加边砍掉即可,贡献为1;
  • 如果其在多个环上,那么需要将这些环的所有附加边都砍掉,但是只能砍掉一个,所以该主要边没有贡献。
  • 如果其不在环上,那么图已经不连通,随意砍掉一个附加边,贡献为附加边个数 m。

如何判断一个主要边在几个环中呢?

所有主要边构成了一棵树,正因为加了附加边才有了环,只要加附加边就会构成环。
环上的主要边为主要边构成的树中,附加边两端点到其 lca 的这条路径。每次都将环上所有边的标记数+1,这样最后就能知道每个边在多少环中。

而将树上一条链中所有边的权值+1,用到树上差分。
这里是边差分,将每条边都附着在下面的点上,将两端点的差分值分别+1,然后将其 lca 的差分值-2。然后求所有点所在子树的差分值之和恢复原值,就得到了附着在该点上的新边权。

Code

#include<bits/stdc++.h>
using namespace std;

#define Ios ios::sync_with_stdio(false),cin.tie(0)

const int N = 200010, mod = 1e9+7;
int T, n, m;
int a[N], b[N];
int dep[N], f[N][30], k;
vector<int> e[N];
int ans;

void bfs()
{
	dep[0] = 0, dep[1] = 1;
	queue<int> que;
	que.push(1);
	
	while(que.size())
	{
		int x = que.front(); que.pop();
		
		for(int tx : e[x])
		{
			if(tx == f[x][0]) continue;
			
			dep[tx] = dep[x] + 1;
			que.push(tx);
			
			f[tx][0] = x;
			for(int i=1;i<=k;i++){
				int anc = f[tx][i-1];
				f[tx][i] = f[anc][i-1];
			}
		}
	}
}

void dfs(int x, int fa)
{
	for(int tx : e[x])
	{
		if(tx == fa) continue;
		dfs(tx, x);
		b[x] += b[tx];
	}
	
	if(x == 1) return;
	if(!b[x]) ans += m;
	else if(b[x] == 1) ans ++;
}

int lca(int x, int y)
{
	if(dep[x] < dep[y]) swap(x, y);
	
	for(int i=k;i>=0;i--)
	{
		if(dep[f[x][i]] >= dep[y]) x = f[x][i];
	}
	
	if(x == y) return x;
	
	for(int i=k;i>=0;i--)
	{
		if(f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
	}
	return f[x][0];
}

signed main(){
	Ios;
	cin >> n >> m;
	k = log(n) / log(2);
	
	for(int i=1;i<n;i++){
		int x, y;cin >> x >> y;
		e[x].push_back(y);
		e[y].push_back(x);
	}
	
	bfs();
	
	for(int i=1;i<=m;i++)
	{
		int x, y; cin >> x >> y;
		b[x] ++, b[y]++;
		b[lca(x, y)] -= 2;
	}
	
	dfs(1, 0);
	
	cout << ans;
	
	return 0;
}

点差分:Eezie and Pie

题意
给定一棵 n 个节点的树,每一个节点有属性 d i s i dis_i disi
对于一个节点 x 来说,其能捕获到其子树的所有节点 tx 中,所有到节点 x 的距离不超过自身属性 d i s t x dis_{tx} distx 的节点。
求出每个节点能够捕获的节点数量。

1 ≤ N ≤ 2 × 1 0 6 ,   0 ≤ d i ≤ N 1≤N≤2×10^6,\ 0≤d_i≤N 1N2×106, 0diN

思路
正着来想,对于一个节点来说,要遍历其子树中的所有节点,判断每个节点是否能够被自己捕获。如果每次都遍历的话,时间复杂度太高。然后想,从下到上回溯的过程中,把当前节点放到 vector 中,存两个权值,一个是属性,一个是深度,往上走的过程中,把深度差大于其属性的节点从中删掉。
虽然往上走的过程中深度差在不断加大,但还有一个属性限制,所以旧加入的点不一定先清理掉,所以不能像双端队列那样,不满足的都集中在后面每次判断后面就行。所以,这里的清除只能遍历所有节点,复杂度就又上升了。

当正着走的思路行不通时,及时止损,换一种思路!

反过来想!
考虑每个节点对上面节点的贡献,看这个节点会被上面的哪些节点所捕获。
很明显,每个节点 x 会对其上面的 d i s i dis_i disi 个节点有贡献,那么就将上面的这 d i s i dis_i disi 个节点的答案 +1。
将树上的一段链上点的权值都 +1,用到树上差分。

这里是点差分,将该链两端点的点权都 +1,将其 lca 的点权 -1,将 lca 父节点的点权 -1。求和恢复原值,以节点 x 为根的子树点权之和便是更新后节点 x 的点权。

在这题里是一条竖着链,所以不用求 lca,将最顶端节点的父节点点权 -1,最底端节点点权 +1 即可。

但还有一个问题,如何找到链最顶端的点?
能够确定链最顶端点的深度 dep,这个深度可能有多个点,我们要的只是从根节点到节点 x 这条路径上,深度为 dep 的点。为了找到这个点,还需要用倍增。
对于每个节点,维护 f[i, j] 表示从该节点出发,往上跳 2 j 2^j 2j 步到达的节点。
然后只要深度大于等于 dep 就跳,一直跳到深度 dep 或者 根节点,复杂度 logm,m为树深。
注意特判不要跳出根节点。

#include<bits/stdc++.h>
using namespace std;

#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define int long long

const int N = 2000010, mod = 1e9+7;
int T, n, m;
int a[N];
int dep[N], f[N][30];
vector<int> e[N];
int k, sum[N], d[N];

void bfs()
{
	queue<int> que;
	que.push(1);
	dep[0] = 0, dep[1] = 1;
	
	while(que.size())
	{
		int x = que.front(); que.pop();
		
		for(int tx : e[x])
		{
			if(tx == f[x][0]) continue;
		
			dep[tx] = dep[x] + 1;
			f[tx][0] = x;
			que.push(tx);
			for(int i=1;i<=k;i++)
				f[tx][i] = f[f[tx][i-1]][i-1];
		}
	}
}

void dfs(int x, int fa)
{
	sum[x] = d[x];
	
	for(int tx : e[x])
	{
		if(tx == fa) continue;
		if(!sum[tx]) dfs(tx, x);
		sum[x] += sum[tx];
	}
}

signed main(){
	scanf("%lld", &n);
	
	k = log(n)/log(2);
	
	for(int i=1;i<n;i++)
	{
		int x, y; scanf("%lld%lld", &x, &y);
		e[x].push_back(y);
		e[y].push_back(x);
	}
	for(int i=1;i<=n;i++) scanf("%lld", &a[i]);
	
	bfs();
	
	for(int i=1;i<=n;i++)
	{
		int x = i;
		for(int j=k;j>=0;j--)
		{
			if(dep[i] - dep[f[x][j]] <= a[i] && f[x][j] > 0) x = f[x][j];
		}
		d[i] ++, d[f[x][0]]--;
	}
	
	dfs(1, 0);
	
	for(int i=1;i<=n;i++) printf("%lld ", sum[i]);
	
	return 0;
}

经验

  • 树上边差分,将树上一条链上所有边的权值都 +c。
    把每条边都附着在其下端点上。
    将链两端点的点权分别 +c,将其 lca 的点权 -2c。
    最后求和恢复原值,以节点 x 为根的子树点权之和便是节点 x 上面那条边的新边权。
  • 树上点差分,将树上一条链上所有点的权值都 +c。
    将链两端点的点权分别 +c,将其 lca 的点权 -c,lca 父节点的点权 -c。
    最后求和恢复原值,以节点 x 为根的子树点权之和便是节点 x 的新点权。
  • 倍增找点,logn 找到从根节点到当前节点路径中,满足条件的节点。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值