【学习笔记】树上差分总结(点差分/边差分)

一.树上差分的基本概念

1.树上差分的定义

树上差分,顾名思义,意思就是在树上做差分

至于什么是差分呢?如果不会的同学,可以先看看我的这篇博客:一维,二维差分の详解(简单易懂)_一维差分-CSDN博客

2.树上差分能解决的问题

树上差分有什么作用?举个例子,如果题目要求对树上的一段路径进行操作,并询问某个点或某条边被经过的次数,树上差分就可以派上用场了。

类比于差分数组,树上差分利用的思想也是前缀和思想。(在这里应该是子树和思想

树上差分,就是利用差分的性质,对路径上的重要节点进行修改(而不是暴力全改),作为其差分数组的值,最后在求值时,利用dfs遍历求出差分数组的前缀和得出答案,就可以达到降低复杂度的目的。

树上差分时需要求LCA,不会的同学可以先看看我的这篇博客:详解最近公共祖先(LCA)-CSDN博客

树上差分一般有两种类型的题目,一种是对边进行差分,另一种就是对点进行差分

下面我将会分别讲解一下这两种问题。

二.点差分

1.思路

直接去dfs暴力加点权的话,肯定会TLE,但是我们现在在讲啥?树上差分啊!

假设需要将两点u,v之间路径上的所有点权增加x,l是lca(u,v),p是l的父亲节点,则差分操作如下:

sum[u] += x;
sum[v] += x;
sum[l] -= x;
sum[p] -= x;

举个栗子(其中假设x=1):

其中s和t就是题目中树上点权需要加1的节点的起始点,绿色的数字代表点权(已经加1了) 

则操作后有:

至于为什么要这么操作呢?别急,继续往下看。

做完上述的差分操作后,我们就要统计答案了。

当我们dfs搜索到s,向上回溯。

下面以u表示当前dfs搜索到的节点

对于每个u统计它的子树大小(要用前缀和的思想记录每个点的点权了),顺着路径标起来。

(即sum[u] += sum[son])

我们会发现第一次从s回溯到s与t的LCA时候,sum[LCA(s,t)] += sum[son[LCA(s,t)]]

此时sum[LCA(s,t)]=0(-1+1=0)。这时我们不禁会有一个疑问: "不是LCA(s,t)会被经过一次嘛,为什么是0!"

别急,我们继续搜另一边。.

继续:我们搜索到t,向上回溯。

依旧统计每个u的子树大小sum[u]+=sum[son]

再度回到LCA(s,t),依旧是sum[LCA(s,t)]+=sum[son[LCA(s,t)]]

这个时候 sum[LCA(s,t)]=1 这就达到了我们要的效果 (是不是特别优秀φ(゜▽゜*)♪)

但是我们还要思考一个问题:万一我们再从LCA(s,t)向上回溯的时候使得其父亲节点的子树和为1怎么办?这样我们不就使得其父亲节点被多经过了一次?

其实很简单,我们只需要在前面差分操作时将sum[fa[lca(s,t)]]-=x就行了。

这样就达到了标记我们路径上的点的要求! 是否有一种恍然大悟的感觉呢?

 

 2.例题Max flow

问题

参考代码

#include<bits/stdc++.h>
using namespace std;
int n,q,mx[300001][41],deep[300001],sum[300001],ans;
vector<int> vec[300001];
void dfs(int x,int fa)//lca的初始化
{
  deep[x] = deep[fa] + 1;
  mx[x][0] = fa;
  for(int i = 0;i < vec[x].size();i++)
    if(vec[x][i] != fa)
      dfs(vec[x][i],x);
}
int lca(int x,int y)//倍增法求lca
{
  if(deep[x] < deep[y]) swap(x,y);
  for(int i = 40;i >= 0;i--)
    if(deep[mx[x][i]] >= deep[y])
      x = mx[x][i];
  if(x == y) return x;
  for(int i = 40;i >= 0;i--)
    if(mx[x][i] != mx[y][i])
    {
      x = mx[x][i];
      y = mx[y][i];
	}
  return mx[x][0];
}
void dfss(int x,int fa)//统计答案的最大值
{
  for(int i = 0;i < vec[x].size();i++)
  {
  	int t = vec[x][i];
    if(t == fa) continue;
    dfss(t,x);
    sum[x] += sum[t];//在树上进行类似于前缀和的操作
  }
  ans = max(ans,sum[x]);//取最大值
}
signed main()
{
  cin>>n>>q;
  for(int i = 1;i < n;i++)
  {
  	int u,v;
  	scanf("%d%d",&u,&v);
  	vec[u].push_back(v);
  	vec[v].push_back(u);
  }
  dfs(1,0);
  for(int j = 1;j <= 40;j++)
    for(int i = 1;i <= n;i++)
      mx[i][j] = mx[mx[i][j - 1]][j - 1];
  while(q--)
  {
  	int u,v;
  	cin>>u>>v;
  	int l = lca(u,v);
	sum[u]++;//树上差分
	sum[v]++;
	sum[l]--;
	if(l != 1) sum[mx[l][0]]--;//如果l有父节点就进行后面的操作
  }
  dfss(1,0);
  cout<<ans;
  return 0;
}

三.边差分

1.思路

思想其实和点差分一样的,我来讲一下操作。

将两点s,t之间路径上的所有边权增加xl=LCA(s,t)以每条边两端深度较大的节点存储该边的差分数组,则操作如下:

sum[s] += x;
sum[t] += x;
sum[l] -= 2 * x;

举个栗子(其中假设x=1):

红色边为需要经过的边,绿色的数字代表经过次数

但是由于我们不能储存边权,所以只能把边权塞给了点权,因此我们的图应该是这样的

 

这样的话我们只要把sum[s]++,sum[t]++,sum[lca(s,t)]-=2就可以实现差分操作了。

同样地,只要dfs一遍,遍历时统计以每个节点为根的树的节点的权值和,就是当前节点到父亲节点的边的最终权值了!

 是不是很厉害

至于为什么点差分和边差分的操作不一样,很简单,请读者自己思考。

树上差分主要还是学习思想吧!

2.例题树上必经边--边差分

问题

 参考代码

#include<bits/stdc++.h>
using namespace std;
int n,q,mx[300001][41],deep[300001],sum[300001],ans,k;
vector<int> vec[300001];
void dfs(int x,int fa)
{
  deep[x] = deep[fa] + 1;
  mx[x][0] = fa;
  for(int i = 0;i < vec[x].size();i++)
    if(vec[x][i] != fa)
      dfs(vec[x][i],x);
}
int lca(int x,int y)
{
  if(deep[x] < deep[y]) swap(x,y);
  for(int i = 40;i >= 0;i--)
    if(deep[mx[x][i]] >= deep[y])
      x = mx[x][i];
  if(x == y) return x;
  for(int i = 40;i >= 0;i--)
    if(mx[x][i] != mx[y][i])
    {
      x = mx[x][i];
      y = mx[y][i];
	}
  return mx[x][0];
}
void dfss(int x,int fa)
{
  for(int i = 0;i < vec[x].size();i++)
  {
  	int t = vec[x][i];
    if(t == fa) continue;
    dfss(t,x);
    sum[x] += sum[t];
  }
}
signed main()
{
  cin>>n>>q>>k;
  for(int i = 1;i < n;i++)
  {
  	int u,v;
  	scanf("%d%d",&u,&v);
  	vec[u].push_back(v);
  	vec[v].push_back(u);
  }
  dfs(1,0);
    for(int j = 1;j <= 40;j++)
  for(int i = 1;i <= n;i++)
      mx[i][j] = mx[mx[i][j - 1]][j - 1];
  while(q--)
  {
  	int u,v;
  	cin>>u>>v;
  	int l = lca(u,v);
	  sum[u]++;
	  sum[v]++;
	  sum[l] -= 2;
  }
  dfss(1,0);
  for(int i = 2;i <= n;i++)
    if(sum[i] == k)
      ans++;
  cout<<ans;
  return 0;
}

四.BB in last 

如果这篇博客对您有帮助的话,别忘了点赞收藏加关注支持一下吖~(小声BB)(o゚v゚)ノ

  • 34
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
PyTorch提供了一个名为gradcheck的函数,它可以用来检查反向传播是否正确计算了函数的导数。通过将张量的元组作为输入,gradcheck函数会评估这些张量的梯度,并与使用小的有限差分的数值近似进行比较。如果梯度与数值近似值非常接近,则返回True。 除了使用gradcheck函数外,PyTorch还提供了autograd模块,它可以自动收集梯度。您可以使用requires_grad参数将张量标记为需要计算梯度的张量,然后在计算过程中,autograd会自动记录计算图并计算梯度。 如果您已经可以根据PyTorch的内置操作编写函数,并且autograd已经能够记录该函数的后向图,那么您不需要自己实现反向传播功能。只需使用普通的PyTorch函数即可完成反向传播。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [pytorch 笔记: 扩展torch.autograd](https://blog.csdn.net/qq_40206371/article/details/122707278)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [【深度学习】Pytorch torch.autograd 自动差分引擎](https://blog.csdn.net/weixin_52058417/article/details/125971914)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值