浅谈树上差分的具体应用

树上差分利用前缀和的思想,利用树上的前缀(也就是子树和),记录树上的一些信息,因为它可以进行离线操作,复杂度O(n),也是非常的好用。

最大流
FJ给他的牛棚的N(2≤N≤50,000)个隔间之间安装了N-1根管道,隔间编号从1到N。所有隔间都被管道连通了。
FJ有K(1≤K≤100,000)条运输牛奶的路线,第i条路线从隔间si运输到隔间ti。一条运输路线会给它的两个端点处的隔间以及中间途径的所有隔间带来一个单位的运输压力,你需要计算压力最大的隔间的压力是多少。

这道题需要让我们求出每个点的覆盖次数(~~这显然可以用树链剖分来做~~),但这也是一个树上差分的经典题。

抛开这道题,先考虑对于一位的一个空间,我们如何差分,没错,对于区间(l-r)来说,在l-1出加上1,在r处减去1,再求前缀和即可。

那在树上如何操作,我们可以把树抽象的想象成自下而上的一个数组,对于一段连续的链,在下面的端点加上1,在上面的端点的父节点减去1,求子树和。

那么左右端点不在一条直链上怎么办?lCA,对于两个点的LCA来说,LCA也只出现了一次,所以在两个端点分别加1,LCA-1,LCA的父亲减1,求子树和,这道题就水过了。

#include<iostream>
#include<cstdio>
#define N 50009
using namespace std;
int ans,head[N],ji[N],tot,deep[N],p[N][22],n,m,a,b,c,fa[N];
struct de
{
int n,to;
}an[N<<1];
inline void add(int u,int v)
{
an[++tot].n=head[u];
an[tot].to=v;
head[u]=tot;
}
void dfs(int u,int f)
{
deep[u]=deep[f]+1;
fa[u]=f;
p[u][0]=f;
for(int i=1;(1<<i)<=deep[u];++i)
p[u][i]=p[p[u][i-1]][i-1];
for(int i=head[u];i;i=an[i].n)
if(an[i].to!=f)
{
int v=an[i].to;
dfs(v,u);
}
}
inline int getlca(int a,int b)
{
if(deep[a]<deep[b])swap(a,b);
for(int i=20;i>=0;--i)
if(deep[a]-(1<<i)>=deep[b])a=p[a][i];
if(a==b)return b;
for(int i=20;i>=0;--i)
if(p[a][i]!=p[b][i])a=p[a][i],b=p[b][i];
return p[a][0];
}
void dfs2(int u)
{
for(int i=head[u];i;i=an[i].n)
if(an[i].to!=fa[u])
{
int v=an[i].to;
dfs2(v);
ji[u]+=ji[v];
}
ans=max(ans,ji[u]);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<n;++i)
scanf("%d%d",&a,&b),add(a,b),add(b,a);
dfs(1,0);
for(int i=1;i<=m;++i)
{
scanf("%d%d",&a,&b);
c=getlca(a,b);
ji[c]--;
ji[fa[c]]--;
ji[a]++;ji[b]++;
}
dfs2(1);
cout<<ans;
return 0;
}

再来道难一点的。

NOIP2015运输计划
公元 20442044 年,人类进入了宇宙纪元。
L 国有 nn 个星球,还有 n-1 条双向航道,每条航道建立在两个星球之间,这 n-1 条航道连通了 L国的所有星球。
小 P 掌管一家物流公司, 该公司有很多个运输计划,每个运输计划形如:有一艘物流飞船需要从 u 号星球沿最快的宇航路径飞行到 v 号星球去。显然,飞船驶过一条航道是需要时间的,对于航道 j ,任意飞船驶过它所花费的时间为 t ,并且任意两艘飞船之间不会产生任何干扰。
为了鼓励科技创新, L国国王同意小 P的物流公司参与 L 国的航道建设,即允许小 P 把某一条航道改造成虫洞,飞船驶过虫洞不消耗时间。
在虫洞的建设完成前小 P 的物流公司就预接了 m 个运输计划。在虫洞建设完成后,这 m 个运输计划会同时开始,所有飞船一起出发。当这 m 个运输计划都完成时,小 P 的物流公司的阶段性工作就完成了。
如果小 P可以自由选择将哪一条航道改造成虫洞, 试求出小 P 的物流公司完成阶段性工作所需要的最短时间是多少?

问题来了,刚才我们要求点的覆盖次数,这回要求边,我们都知道求点的覆盖可以用树链剖分来做,
但把点换成边好像无从下手,那么我们该如何处理?
还是树上差分,但我们把定义换一下,每个子树记录的是这个点向上连的那条边出现的次数,左右端点分别加1,LCA减2,就可以搞了。

然后咧?

我们发现直接求很困难,所以就考虑把求最值改成二分答案+验证找最值,先二分最终的答案,在把需要删边的链来一波差分,找出这些链的最长公共链,判断最长链减去最长公共链是否小于等于答案,然后这题就做完了。

#include<iostream>
#include<cstdio>
#include<cstring>
#define N 300009
using namespace std;
int tot,head[N],ji[N],jii[N],num,ll,lca[N],u[N],v[N],n,deep[N],d[N],fa[N],m,L[N],p[N][22];
int a,b,c,l,r;
struct de
{
int n,to,l;
}an[N<<1];
inline void add(int u,int v,int l)
{
an[++tot].n=head[u];
an[tot].to=v;
head[u]=tot;
an[tot].l=l;
}
void dfs2(int u,int fa)
{
for(int i=head[u];i;i=an[i].n)
if(an[i].to!=fa)
{
int v=an[i].to;
dfs2(v,u);
ji[u]+=ji[v];

}
if(ji[u]==num)ll=max(ll,jii[u]);
}
bool ch(int pos)
{
int pp=0;
num=0;ll=0;
memset(ji,0,sizeof(ji));
for(int i=1;i<=m;++i)
if(L[i]>pos)
{
ji[lca[i]]-=2;
ji[u[i]]++;
ji[v[i]]++;
pp=max(pp,L[i]);
num++;
}
dfs2(1,0);
if(pp-ll<=pos)return 1;
else return 0;
}
void dfs(int u,int f)
{
fa[u]=f;
deep[u]=deep[f]+1;
p[u][0]=f;
for(int i=1;(1<<i)<=deep[u];++i)
p[u][i]=p[p[u][i-1]][i-1];
for(int i=head[u];i;i=an[i].n)
if(an[i].to!=f)
{
int v=an[i].to;
d[v]=d[u]+an[i].l;
jii[v]=an[i].l;
dfs(v,u);
}
}
inline int getlca(int a,int b)
{
if(deep[a]<deep[b])swap(a,b);
for(int i=20;i>=0;--i)
if(deep[a]-(1<<i)>=deep[b])a=p[a][i];
if(a==b)return b;
for(int i=20;i>=0;--i)
if(p[a][i]!=p[b][i])a=p[a][i],b=p[b][i];
return p[a][0];
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<n;++i)
scanf("%d%d%d",&a,&b,&c),add(a,b,c),add(b,a,c);
dfs(1,0);
for(int i=1;i<=m;++i)
{
scanf("%d%d",&u[i],&v[i]);
lca[i]=getlca(u[i],v[i]);
L[i]=d[u[i]]+d[v[i]]-2*d[lca[i]];
r=max(r,L[i]);
}
int ans=0;
while(l<=r)
{
int mid=(l+r)>>1;
if(ch(mid))
{
ans=mid;
r=mid-1;
}
else l=mid+1;
}
cout<<ans;
return 0;
}

  

来看最后一道 NOIP2016 天天爱跑步

小c同学认为跑步非常有趣,于是决定制作一款叫做《天天爱跑步》的游戏。《天天爱跑步》是一个养成类游戏,需要玩家每天按时上线,完成打卡任务。
这个游戏的地图可以看作一一棵包含 n个结点和 n-1 条边的树, 每条边连接两个结点,且任意两个结点存在一条路径互相可达。树上结点编号为从 1 到 n 的连续正整数。
现在有 m个玩家,第 ii 个玩家的起点为 S终点为 T。
每天打卡任务开始时,所有玩家在第 00 秒同时从自己的起点出发, 以每秒跑一条边的速度, 不间断地沿着最短路径向着自己的终点跑去, 跑到终点后该玩家就算完成了打卡任务。 (由于地图是一棵树, 所以每个人的路径是唯一的)
小c想知道游戏的活跃度, 所以在每个结点上都放置了一个观察员。 在结点 j的观察员会选择在第 Wj 秒观察玩家, 一个玩家能被这个观察员观察到当且仅当该玩家在第 Wj秒也理到达了结点 j 。 小C想知道每个观察员会观察到多少人?
注意: 我们认为一个玩家到达自己的终点后该玩家就会结束游戏, 他不能等待一 段时间后再被观察员观察到。 即对于把结点 j 作为终点的玩家: 若他在第 Wj秒前到达终点,则在结点 j 的观察员不能观察到该玩家;若他正好在第 Wj秒到达终点,则在结点 j 的观察员可以观察到这个玩家。

这是我见过的最难的一道树上差分题目,它的解法十分的巧妙。

在第w[j]秒观察到,难道我还让它动态的往上跳吗?

当然不用,让我们列一波式子。

因为这是一颗树,它具有一些非常有用(恶心)的性质,就是链上的LCA,当w在s到LCA的路上时,有以下式子成立

deep[S]=w[x]-deep[x]

当w在LCA到t的路上时,有以下式子成立
deep[s]-2*d[lca(s,t)]=w[x]-deep[x]

由于二式不等价,所以我们要分开处理,由于在S到LCA的路上一式成立,在LCA到T时二式成立,但我们在处理两种情况时要注意不能重复。

所以我们开两个映射,第一个表示在一式情况下左边的式子的结果对应了几个i,第二个表示二式下左边的式子的结果对应了几个i。

然后怎么做?

还考虑树上差分,我们可以理解为又有一种数在s出现,在lca的父亲处消失,另一种树在t出现,在LCA处消失,这两种数对应了上述两种式子,那我们遍历整棵树时,到达一个节点就把这个位置对应的结果加入映射,对于每个询问的答案,就是遍历以这个点为根的子树前后右边的式子的结果对应的映射的差。

#include<iostream>
#include<cstdio>
#include<vector>
#include<map>
#define N 300009
using namespace std;
map<int,int>A,B;
struct pai
{
int tag,tag2,num;
};
vector<pai>ji[N];
int n,m,head[N],p[N][22],deep[N],fa[N],tot,a,b,w[N],ans[N];
struct dwd
{
int n,to;
}an[N<<1];
inline void add(int u,int v)
{
an[++tot].n=head[u];
an[tot].to=v;
head[u]=tot;
}
void dfs(int u,int f)
{
fa[u]=f;
deep[u]=deep[f]+1;
p[u][0]=f;
for(int i=1;(1<<i)<=deep[u];++i)
p[u][i]=p[p[u][i-1]][i-1];
for(int i=head[u];i;i=an[i].n)
{
int v=an[i].to;
if(v!=f)dfs(v,u);
}
}
inline int getlca(int a,int b)
{
if(deep[a]<deep[b])swap(a,b);
for(int i=20;i>=0;--i)
if(deep[a]-(1<<i)>=deep[b])a=p[a][i];
if(a==b)return b;
for(int i=20;i>=0;--i)
if(p[a][i]!=p[b][i])a=p[a][i],b=p[b][i];
return p[a][0];
}
void dfs2(int u,int fa)
{
int p=A[deep[u]+w[u]],q=B[w[u]-deep[u]];//gai
for(int i=head[u];i;i=an[i].n)
if(an[i].to!=fa)
{
int v=an[i].to;
dfs2(v,u);
}
for(int i=0;i<ji[u].size();++i)
{
if(ji[u][i].tag==1)A[ji[u][i].num]+=ji[u][i].tag2;
else B[ji[u][i].num]+=ji[u][i].tag2;
}
ans[u]=B[w[u]-deep[u]]+A[deep[u]+w[u]]-q-p;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<n;++i)
scanf("%d%d",&a,&b),add(a,b),add(b,a);
dfs(1,0);
for(int i=1;i<=n;++i)
scanf("%d",&w[i]);
for(int i=1;i<=m;++i)
{
int s,t,lca;
scanf("%d%d",&s,&t);
lca=getlca(s,t);
ji[s].push_back(pai{1,1,deep[s]});
ji[fa[lca]].push_back(pai{1,-1,deep[s]});
ji[t].push_back(pai{2,1,deep[s]-2*deep[lca]});
ji[lca].push_back(pai{2,-1,deep[s]-2*deep[lca]});
}
dfs2(1,0);
for(int i=1;i<=n;++i)
printf("%d ",ans[i]);
return 0;
}

原文地址:https://www.cnblogs.com/ZH-comld/p/9384690.html

时间: 2024-08-30 11:31:18

浅谈树上差分的具体应用的相关文章

浅谈树上差分

浅谈树上差分 [引子] 我们遇到一些关于树的问题时,往往需要我们统计一些树上的信息,比如子树和,路径边覆盖.点覆盖(目前没见过别的类型).暴力的解法当然是遍历逐个点对其权值进行修改. 类比序列问题,其在进行区间修改时,可以用差分将\(O(n)\)复杂度降为\(O(1)\).在树上我们是对一条链进行处理,那差分在树上可不可用呢?答案是肯定的. [从序列到树] 在一个序列上进行差分的操作,相信各位都十分熟悉:假设当前我们要对一个序列的\(l\sim r\)区间的每个数执行\(+k\)操作,那么对于差

浅谈对差分隐私的理解

在听完第五组的报告之后,浅谈一下对差分隐私的认识,主要针对差分隐私的思想做一个大致的梳理. 为什么会产生差分隐私? 由于有些“聪明”的用户为了知道某些信息,可以通过两次查询结果的差异进行对比,从而在两次数据的对比中找到有用的信息.正如在杨顼组的报告中提到的查询二等兵约瑟夫阿伦是否阵亡的信息,可以通过查询D5和D6两次数据结果,将两次数据结果进行对比就可以知道约瑟夫阿伦是否阵亡的消息. 差分隐私的主要思想: 差分隐私是基于噪音的安全计算方法,它的思想是:对计算过程用噪音干扰,让原始数据淹没在噪音中

浅谈差分约束系统——图论不等式的变形

浅谈差分约束系统——图论不等式的变形 ----yangyaojia 版权声明:本篇随笔版权归作者YJSheep(www.cnblogs.com/yangyaojia)所有,转载请保留原地址! 一.定义 如若一个系统由n个变量和m个不等式组成,并且这m个不等式对应的系数矩阵中每一行有且仅有一个1和-1,其它的都为0,这样的系统称为差分约束( difference constraints )系统. 二.分析 简单来说就是给你n个变量,给m个形如x[i]-x[j]≥k①或x[i]-x[j]≤k②.求两

浅谈未来网站的构建

前言: 话说"合久必分,分久必合",从过去的几年来看我们现在正处于一个资源.信息技术和服务等整合的时代,从行业到技术,从电子商务到企业资源,那么对于现存的网站如何进行整合,未来的网站将以什么形式展现在人们面前,与现在相比又给人们带来哪些不同的用户体验呢,本文作者站在个人角度,结合近年来出现的技术,对于未来的网站发表下个人见解. 随着各行业信息化进程的加快,各种功能的网站应运而生,工作.购物.学习.娱乐.医疗.金融和社交等网站都层出不穷,由于这些网站的出现,给人们生活带来很大的方面, 人

【转】浅谈React、Flux 与 Redux

本文转自<浅谈React.Flux 与 Redux>,转载请注明出处. React React 是一个 View 层的框架,用来渲染视图,它主要做几件事情: 组件化 利用 props 形成单向的数据流 根据 state 的变化来更新 view 利用虚拟 DOM 来提升渲染性能 前面说到 React 能够根据 state 的变化来更新 view,一般来说引起 state 变化的动作除了来自外部(如服务器),大部分都来自于页面上的用户活动,那页面上的用户活动怎样对 state 产生作用呢?Reac

浅谈算法和数据结构: 七 二叉查找树 八 平衡查找树之2-3树 九 平衡查找树之红黑树 十 平衡查找树之B树

http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html 前文介绍了符号表的两种实现,无序链表和有序数组,无序链表在插入的时候具有较高的灵活性,而有序数组在查找时具有较高的效率,本文介绍的二叉查找树(Binary Search Tree,BST)这一数据结构综合了以上两种数据结构的优点. 二叉查找树具有很高的灵活性,对其优化可以生成平衡二叉树,红黑树等高效的查找和插入数据结构,后文会一一介绍. 一 定义 二叉查找树(B

XJOI CBH的发展计划(树上差分)

这是qzh的第二题 题目大意: 给你一棵树和一些连接祖先和孩子的边(非树枝边,类似于有向图返祖边) 让你求出删掉其中一条树枝边和一条非树枝边使图不联通的方案数 我们思考对树枝边统计答案 如图 对于红边统计答案,它的子树中有一条向外连的边,必须删掉红边和这条边才能使图不连通,所以这条树枝边贡献为1 如图 对于红边统计答案,它的子树中有0条向外连的边,删掉红边和任意一条虚线边能使图不连通,所以这条树枝边贡献为非树枝边的数量 如图 对于红边统计答案,它的子树中有两条及以上向外连的边,删掉红边和任意一条

【转载】李航博士的《浅谈我对机器学习的理解》 机器学习与自然语言处理

李航博士的<浅谈我对机器学习的理解> 机器学习与自然语言处理 [日期:2015-01-14] 来源:新浪长微博  作者: 李航 [字体:大 中 小] 算算时间,从开始到现在,做机器学习算法也将近八个月了.虽然还没有达到融会贯通的地步,但至少在熟悉了算法的流程后,我在算法的选择和创造能力上有了不小的提升.实话说,机器学习很难,非常难,要做到完全了解算法的流程.特点.实现方法,并在正确的数据面前选择正确的方法再进行优化得到最优效果,我觉得没有个八年十年的刻苦钻研是不可能的事情.其实整个人工智能范畴

浅谈可持久化数据结构

Preface 由于我真的是太弱了,所以真的是浅谈. 神奇的数据结构其实我也很虚啊! 值域线段树 简单的说,值域线段树区间里面存的是在这个区间内的数的个数有多少个. 有没有感觉很简单,考虑一下如果我们有一棵这样的线段树,查找排名为rk的数时只需要看一下左子树的大小就可以判断在左边还是右边了. 有没有感觉很像BST 动态开点与可持久化 根据上面的值域线段树,我们可以得出一种naive的做法: 对于每一个前缀\([1,i](i\in[1,n])\)都开一棵值域线段树,然后查询区间的时候直接每个节点的