差分 and 树上差分

差分数组

定义

百度百科中的差分定义
//其实这完全和要讲的没关系 qwq

进去看了之后是不是觉得看不懂?

那我简单概括一下qwq

差分数组de定义:记录当前位置的数与上一位置的数的差值.

栗子

容易发现的是,\(\sum_{1}^{i}{b_i}\)即代表\(a_i\) 的值. \((\sum_{1}^{i}\) 即代表从1累加到i.)

思想

看到前面的\(\sum_{1}^{i}\) 你一定会发现这是前缀和!

那你认为这是前缀和? 的确是qwq.

实际上这并不是真正意义上的前缀和.

前缀和的思想是 根据元素与元素之间的并集关系(和的关系),求出某些元素的和的值.对应的为$\sum_{1}^{i} a_i $

而差分的思想与此不同.

差分的思想是 根据元素与元素之间的逻辑关系(大小关系),求出某一位置元素的值.对应的为\(\sum_{1}^{i}b_i\)

What?不懂?看下面

继续捡起刚刚的栗子.

有没有发现不同之处 ( ? ?ω?? )?

差分数组有什么用?

先看一道题,

有n个数。

m次操作,每一次操作,给定l,r,del.将l~r区间的所有数增加del;

最后有q个询问,给你 l,r ,每一次询问求出l~r的区间和。

PS: 先进行m个修改操作,后进行查询操作.

如果你是一个巨佬,你会"woc,线段树裸题!" "woc,树状数组裸题!"

然而,今天我要BB的不是这些东西.

差分数组的运用!

有没有想法? 没有的话那就我来有也是我来

考虑我们差分数组记录的是什么,它记录的是当前位置的数与上一个数的差值.

如果我们在差分数组的 \(b_x\)减去\(del\) 在\(b_{y+1}\)位置处加上\(del\),就能达到整个区间修改的操作.

什么?不相信? 那我们来个栗子(要糖炒的

还是刚开始的栗子.

这样是不是达到了区间修改操作,是不是! "是!,tql!!"

这样我们就能做到\(O(1)\)修改啦!

我们再定义两个数组(先不要忘记我们的差分数组为\(b_i\)

\(s_i\)代表\(\sum_{1}^{i} b_i\) (其实就是代表\(a[i]\) qwq

\(sum_i\)代表\(\sum_{1}^{i}\) \(s_i\) 即代表前缀和. qwq

容易发现的是 \(sum_r -sum_{l-1}=\sum_{l}^{r} s_i\)

什么不理解为什么是\(l-1\)?

那我们把式子展开看 qwq

\(sum_r=s_1+s_2+\dots+s_{l-1}+s_{l}+s_{l+1}+\dots+s_r\)

\(sum_{l-1}=s_1+s_2+\dots+s_{l-2}+s_{l-1}\)

两个式子相减的话,我们得到的就是 \(s_l+\dots+s_r\) 即\(\sum_l^r s_i\)啦!

而我们如果减去\(sum_l\)的话,就会多减去一个s_l,得到的值就不是\(\sum_l^r s_i\) 了!

emmmm 差点跑去讲前缀和

所以说我们可以在修改操作完成之后,\(O(n)\)的计算我们的\(sum\)数组,然后在询问的时候\(O(1)\)的输出了.

具体这个题的代码是这样的↓

#include<bits/stdc++.h>
using namespace std;
int n,m,q,last,sum[10086],b[10086],s[10086];
int main()
{
    cin>>n;//n个数
    for(int i=1,x;i<=n;i++)
    {
        cin>>x;//这里实际上不需要a数组,视题而异
        b[i]=x-last;//得到差分数组
        last=x;//别忘了变化last变量
    }
    cin>>m;//m次操作
    for(int i=1,l,r,del;i<=m;i++)
    {
         cin>>l>>r>>del;//在[l,r] 加上del
         b[l]+=del,b[r+1]-=del;
    }
    for(int i=1;i<=n;i++)
    {
        s[i]=s[i-1]+b[i];//这里是处理我们的s数组
        sum[i]=sum[i-1]+s[i];//处理我们的sum数组.
    }
    cin>>q;//q个询问
    for(int i=1,l,r;i<=q;i++)
    {
        cin>>l>>r; //询问[l,r]的区间和.
        cout<<sum[r]-sum[l-1]<<endl; //输出即可.
    }
}

你以为差分只有这么多用处吗?

貌似真的只有这么多

例题

来一个 差分+二分 结合运用的题目 -->p1083 借教室

看完题目了没? 有没有思路?

想一下,

一个订单的起始天+要借的教室数量, 并在该订单结束的那一天的下一天减去要借的教室数量

这样我们再维护一下前缀和,这就样就能知道这些天中,我们借了多少教室! 是不是很巧妙qwq

然后我们再二分订单数量,尝试满足mid个订单,(这个不会证明 QAQ)

因此我们ok函数这样写 ↓

IL bool ok(int now)//尝试满足now个订单
{
    clear(delt);
    for(RI i=1;i<=now;i++)
    {
        delt[s[i]]+=d[i];//s[i]为第i个订单起始天
        delt[t[i]+1]-=d[i];//t[i]为第i个订单结束天.
    }
    for(RI i=1;i<=n;i++)
    {
        sum[i]=sum[i-1]+delt[i];//前缀和.
        if(sum[i]>could[i])return false;
    }
    return true;
}

相信你随随便便也能切掉这个题了.

小结

差分数组的话,一般并没有裸的考查,但是差分数组的思想啊,辅助啊,还是比较常用的qwq.

例如树状数组维护差分(到底是谁维护谁我也不是很清楚)qwq

(因为树状数组是维护的前缀和啊,所以可以一起用)

推荐一篇很好的文章(讲解树状数组与差分的结合使用)--->这里

Chanis写的也很好啊qwq

如果非要说差分数组的适用范围的话,

它适用于离线的区间修改问题,在线的话就去码 线段Tree 或 Tree状数组就好了 qwq

树上差分

其实主要是为了讲这个的qwq

2015,2016两年Noip对于树上差分都有考察 noip2015 运输计划 noip2016 天天爱跑步

这两个题涉及的知识点有着 树上差分+二分+LCA\(\dots\),这是一些进阶的考查 其实是我不太会qwq

所以在这里打算单纯地介绍一下树上差分并讲解一些例题.qwq

前置知识

需要知道的树的性质:

  1. 树上任意两个点的路径唯一.
  2. 任何子节点的父亲节点唯一.(可以认为根节点是没有父亲的)

如果你认为你知道了这些你就能秒切这些树上差分的题,那你就太低估这个东西了!

树上差分的两种基本操作用到了LCA,不了解LCA的话可以去这里面学一下

思想

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

当我们记录树上节点被经过的次数,记录某条边被经过的次数的时候.

如果每次强制dfs去标记的话,时间复杂度将高到爆炸!

因此我们引入了树上差分!

树上差分在一起的使用的是\(DFS\),因为在回溯的时候,我们可以计算出子树的大小.

(这个应该不用过多解释

定义数组

\(cnt_i\)为节点i被经过的次数.

基本操作

1.点的差分

这个比较简单,所以先讲这个qwq

例如,我们从 \(s-->t\) ,求这条路径上的点被经过的次数.

很明显的,我们需要找到他们的LCA,(因为这个点是中转点啊qwq.

我们需要让\(cnt_s++\),让\(cnt_t++\),而让他们的\(cnt_{lca}--\),\(cnt_{faher(lca)}--\);

可能读着会有些难理解,所以我准备了一个图qwq

绿色的数字代表经过次数.

直接去标记的话,可能会T到不行,但是我们现在在讲啥?树上差分啊!

根据刚刚所讲,我们的标记应该是这样的↓

考虑:我们搜索到s,向上回溯.

下面以\(u\)表示当前节点,\(son_i\)代表i的儿子节点.(如果一些\(son\)不给出下标,即代表当前节点\(u\)的儿子

每个\(u\)统计它的子树大小,顺着路径标起来.(即\(cnt_u+=cnt_{son}\))

我们会发现第一次从s回溯到它们的LCA时候,\(cnt_{LCA}+=cnt[son_{LCA}]\)

\(cnt_{LCA}=0\)! "不是LCA会被经过一次嘛,为什么是0!"

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

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

依旧统计每个u的子树大小\(cnt_u+=cnt_{son}\)

再度回到\(LCA\) 依旧 是\(cnt_{LCA}+=cnt[son_{LCA}]\)

这个时候 \(cnt_{LCA}=1\) 这就达到了我们要的效果 (是不是特别优秀 ( ? ?ω?? )?

担忧: 万一我们再从\(LCA\)向上回溯的时候使得其父亲节点的子树和为1怎么办?

这样我们不就使得其父亲节点被经过了一次? 因此我们需要在\(cnt_{faher(lca)}--\)

这样就达到了标记我们路径上的点的要求! 厉不厉害 (o?▽?)o tql!!

这样点的差分应该没什么问题了吧 ,有问题可以问我的哦 qwq (如果我会的话.)

2.边的差分

既然我们已经get到了点的差分,那么我们边的差分也是很简单啦!

机房某dalao:"这不和点差分标记方式一样吗?不就是把边塞给点吗? 看我切了它!"

为这位大佬默哀一下 qwq.

的确,我们对边进行差分需要把边塞给点,但是,这里的标记并不是同点差分一样.

PS: 把边塞给点的话,是塞给这条边所连的深度较深的节点. (即塞给儿子节点

先请大家思考\(5s\)

\(\vdots\)

\(\vdots\)

\(\vdots\)

好,时间到,有没有想到如何标记?(只要画图模拟一下就可以啦! 上图!

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

正常的话,我们的图是这样的.↓

但是由于我们把边塞给了点,因此我们的图应该是这样的↓

但是根据我们点差分的标记方式来看的话显然是行不通的,

这样的话我们会经过\(father_{LCA}--> LCA\)这一路径.

因此考虑如何标记我们的点,来达到经过红色边的情况

聪明的你一定想到了,这样来标记

\(cnt_s++\) , \(cnt_t ++\) ,\(cnt_{LCA}-=2\)

这样回溯的话,我们即可只经过图中红色边啦!(这里就不详细解释啦,原理其实相同 qwq

把边塞入点中的代码这样写.qwq(顺便在搜索的时候处理即可

void dfs(int u,int fa,int dis)
{
    //u为当前节点,fa为当前节点的父亲节点,dis为从fa通向u的边的边权.
    depth[u]=depth[fa]+1;
    f[u][0]=fa;//相信写过倍增LCA的人都能看懂.
    init[u]=dis;//这里是将边权赋给点.
    for(int i=1;(1<<i)<=depth[u];i++)f[u][i]=f[f[u][i-1]][i-1];//预处理倍增数组.
    for(int i=head[u];i;i=edge[i].u)
    {
        if(edge[i].v==fa)continue;
        dfs(edge[i].v,u,edge[i].w);
    }
    //这个每个人的写法不一样吧.
    //所以根据每个人的代码风格不一样,码出来的也不一样
}

例题选讲

代码会在下面统一发 qwq

  1. 先来一个简单题练练手 --->p3128 最大流
    这个题应该算是树上差分的入门题
    就是入门题qwq

    题意简单概括一下
    求被经过次数最多的点,(其实概括出来的话,这题就裸了.)

    显然,裸的点差分.

    所以请在课下切掉它qwq.

  2. 再来一个简单题练手 --->p3258 松鼠的新家

    也是一个简单的树上差分的题.不过有一些小坑点.

    读完题大家先思考\(5s\)

    \(\vdots\)

    时间到。

    简单分析
    很明显,这是一道点差分.但是不同的是,我们需要在每个位置”中转“一下.

    即从 \(a_1-->a_2\) ,\(a_2-->a_3\) ,\(a_3-->a_4\) \(\dots\)我们会重复经过\(a_2\),\(a_3\),\(\dots\)这一些点.

    因此我们经过这些节点次数会被重复计算,因此,我们需要将其\(--\)

    还要注意的是,当我们到达\(a_n\)这一位置的时候,小熊会吃饭 qwq ,即在这里不会有糖果吃. 所以这个位置的经过次数也需要\(--\).

    代码中需要注意的位置也只有这里 这样↓

     for(RI i=2;i<=n;i++)cnt[a[i]]--;

    3.上点难度了 ----->p2680 运输计划

    (可能是因为我太弱了 qwq

    先感谢dalao的讲解 @GMPotlc

    读完题,我们发现,这是一道边差分的题.

    简单分析
    于是建完边我们先dfs一遍预处理出根节点到每个节点的距离.并把边权塞给点。

    预处理距离的话只需要再在dfs中加入一句即可

    Dis[edge[i].v]=Dis[u]+edge[i].w;

    然后我们可以计算出每条航道间的距离,类似这样

    //\(query[i].dis\)代表第i个询问两航道之间的距离

    //则\(query[i].dis=Dis[x]+Dis[y]-2*Dis[lca_{x,y}]\)

    不能理解这个计算的话来看图 qwq.

    图中给出的边均为从根节点到达某节点的距离.

    颜色对应

    我们发现,实际只要记录的距离仅为LCA下面的红色和绿色路径.

    而我们重复经过了LCA上面的边两次."这没用啊"

    因此只要减去2*Dis_{LCA}即可.

    考虑:

    我们需要将被经过次数最多,且边权最大的边删去.

    这样能使我们所用总时间最大值尽可能小 (很明显 qwq

    要求最大值最小? 很明显,我们想到了二分答案.

    解决

    既然想到了二分答案,那我们就二分这些路径的长度.(即工作时间.

    如果一些路径长度大于当前二分的mid,我们就需要记录这些路径上的边其被经过次数.

    (比mid小的路径一定已经合法,我们可以在mid时间内完成任务.)

    假设路径长度大于mid的有num个

    (我们找到被这些路径共同经过的最大的边权,删去它,使得这些路径长度都小于mid,那么这个mid就是合法的.

    小细节:我们可以通过排序得到最大的路径长度,如果这条最长的路径减去被经过次数<=mid,那这个mid就是合法的,我们就可以去寻找更优解.

    这里引用题解里的一句话

    因为要求求最小时间, 然而可以根据单调性可以通过二分一个时间来判定这个时间能不能成立.
    也就是通过二分答案将一个求答案的问题转化为\(log_{2}t_{\max}\)个判定性问题.

    因此这个题就很简单了

    PS:记得每次将标记数组清零.(因为大于mid的路径长度会变化.

    (可能做法常数有些大,但是是可以过的.

    (也可能是评测机看脸.第一次交T了一个点,第二次交就A掉了 qwq.

上面三个题的代码都在这里

小结

树上差分的裸题还是比较少的(其实是我遇到的比较少吧 qwq.

因此树上差分与其他算法的结合考察还是比较多的

例如刚刚讲的运输计划就是一个 LCA+树上差分+二分.

(其实树上差分问题一定有LCA的 qwq)

BB in last

总的来说,差分数组重点在于思想的运用.

而树上差分一般不会直接考裸题.

所以,我们需要掌握更多的算法. qwq

不会的可以问我 ,直到今年Noip我一直会在.

如果拿到省一的话,我会待的更久,拿不到就滚回去学文化课了 qwq

原文地址:https://www.cnblogs.com/-guz/p/9678715.html

时间: 2024-10-29 08:01:50

差分 and 树上差分的相关文章

差分数组 and 树上差分

差分数组 定义 百度百科中的差分定义 //其实这完全和要讲的没关系 qwq 进去看了之后是不是觉得看不懂? 那我简单概括一下qwq 差分数组de定义:记录当前位置的数与上一位置的数的差值. 栗子 容易发现的是,\(\sum_{j=1}^{i} b_j\)即代表\(a_i\) 的值. \((\sum\) 即代表累加.) 思想 看到前面的\(\sum\) 你一定会发现这是前缀和! 那你认为这是前缀和? 的确是qwq. 实际上这并不是真正意义上的前缀和. 前缀和的思想是 根据元素与元素之间的并集关系(

[填坑]树上差分 例题:[JLOI2014]松鼠的新家(LCA)

今天算是把LCA这个坑填上了一点点,又复习(其实是预习)了一下树上差分.其实普通的差分我还是会的,树上的嘛,也是懂原理的就是没怎么打过. 我们先来把树上差分能做到的看一下: 1.找所有路径公共覆盖的边 例题:[NOIP2015]运输计划 (然而我还没过就先不讲了) 反正就是中间有一步要求一条边被所有计划公共覆盖. 那么怎么求它呢?暴力(滚粗).我们有一个非常好的方法就是树上差分(记录tmp为差分数组) 询问操作为从叶子节点的权值向上累加到root 在一条路径u→ v,如果tmp[u]++,那么我

【BZOJ-4326】运输计划 树链剖分 + 树上差分 + 二分

4326: NOIP2015 运输计划 Time Limit: 30 Sec  Memory Limit: 128 MBSubmit: 703  Solved: 461[Submit][Status][Discuss] Description 公元 2044 年,人类进入了宇宙纪元.L 国有 n 个星球,还有 n−1 条双向航道,每条航道建立在两个星球之间,这 n−1 条航道连通了 L 国的所有星球.小 P 掌管一家物流公司, 该公司有很多个运输计划,每个运输计划形如:有一艘物流飞船需要从 ui

【COGS 2434】 暗之链锁 树上差分+LCA

差分就是把一个值拆成许多差的和如 1 2 4 6 9 那么 把这个东西拆成 1 1 2 2 3 就是了,当然也可以理解为对一个问题分解为多个子问题并对其进行操作来得到原问题的答案. 树上差分就更玄妙了,它既可以把原问题拆成他到根节点的所有点,也可以拆成子树,拆成子树的话修改一个点影响的是他到根的路径上所有点,根据这个我们可以再加上LCA来解决许多问题. 这道题:I. 我们可以看出我们可以把它转化成一棵有根树,那么两部分一定是一个子树和其他 II. 那些虚边,都是砍断实边之后的藕断丝连,至于如何计

【CF739B】Alyona and a tree(树上差分,二分,树形DP)

题意:给出一棵有根树,树上每个点.每条边都有一个权值. 现在给出"控制"的定义:对一个点u,设点v在其子树上,且dis(u,v)≤av,则称u控制v. 要求求出每个点控制了多少个点 n (1?≤?n?≤?2·105).  (1?≤?ai?≤?109) 1?≤?pi?≤?n, 1?≤?wi?≤?109) 思路:在学校CF有时上不去不知道为什么 对于确定的点i,计算它对哪些点有贡献 dis[i]-dis[u]<=a[i] dis[u]<=a[i]-dis[i]满足二分性 倍增枚

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

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

BZOJ 4326 NOIP2015 运输计划 (二分+树上差分)

题意:中文题. 析:首先二分是很容易想出来的,然后主要是判断这个解合不合法,先二分答案 mid,因为有 m 个计划,所以只要添加虫洞的肯定是所有的时间长于 mid 的计划 中,也就是是那些的共同边,这个就可以用树上差分来做了,假设 s 到 t,那么让in[s]++,in[t]++,in[lca(s, t)] -= 2,其中in 表示的是 该结点与其父结点的边的计数,最后再跑一次dfs,把所有的权值都累加上去,这样就能知道哪些是共同的边了. 代码如下: #pragma comment(linker

BZOJ 3631 [JLOI2014]松鼠的新家 | 树上差分

链接 BZOJ 3631 题解 看起来是树剖?实际上树上差分就可以解决-- 当要给一条路径(u, v) +1的时候,给d[u] += 1, d[v] += 1, d[lca(u, v)] -= 1, d[fa[lca(u, v)]] -= 1. 注意这道题中路径的终点是不 +1的. #include <cstdio> #include <cmath> #include <cstring> #include <algorithm> #include <q

NOIp2015 运输计划 [LCA] [树上差分] [二分答案]

我太懒了 吃掉了题面 题解 & 吐槽 一道很好的树上差分练习题. 不加fread勉强a过bzoj和luogu的数据,加了fread才能在uoj里卡过去. 可以发现,答案则是运输计划里花费的最大值,最大值最小,便是二分答案的标志. 那么该怎么check呢... 我们得找出所有超过限制的计划,这个过程可以在LCA倍增的过程中预处理出来. 然后再找出一些被这些计划都覆盖的边,找到最大的那条边,如果最大的计划花费减去最大的那条边小于x,那么x就是可行的. 但是该怎么找到那些被计划都覆盖的边呢... 我们