神奇的差分法(内附树状数组的一点扩展)

差分法是我们所用的一个强力的武器!

有这把武器你就可以统治世界。。。

一个大佬曾经讲过,一但碰到区间修改的题,就要优先考虑差分。

普通差分法

我们有时做题,会发现这么一种题。

给你长度为n的序列,m次操作,有两种:1. 让[l,r]区间加上k。2. 查询一个点的值。

典型的区间修改,单点查询。
单点查询简单。
但是区间修改怎么做?
暴力卡常。。。
线段树。。。
比较正经的做法是差分,差分是什么?

在这里插入图片描述

对于一个序列a,定义另一个序列b,\(b[i]=a[i]-a[i-1]\),则叫b数组为a数组的差分数组,这样有什么优势呢?

首先,如果要求a[i],我们会发现就是\(b[i]+a[i-1]\),不断拆开,\(a[i]=b[i]+b[i-1]+b[i-2]+b[i-3]+....+b[1]\),是不是很棒棒,就是前缀和!

但是单点查询O(n)呀

先讲这样怎么修改,假设是\([l,r]\)区间加上k,那么我们就让\(b[l]\)加上\(k\),让\(b[r+1]\)减去\(k\)就行了。这样,在\([l,r]\)区间内,每个数都会加上\(b[l]\)多出的\(k\),但是在\(r\)之后,我们也会因为\(b[r+1]\)减去了\(k\)从而和\(b[l]\)\(k\)抵消。

这样,区间修改就完成了!

但是单点查询又复杂了,它可以表达成前缀和,前缀和......树状数组维护就可以了!
是不是很不错!

这里给大家一点扩展!

至于树状数组区间查询,区间修改,我粘上一个大佬的话,我认为很不错!


我们还是需要引入delta数组,这里的delta[i]表示区间a[i...j]都需要加上的值的和。那么当我们需要将区间[l,r]上的每个数都加上x时,我们还是可以直接在树状数组上将delta[l]加上x,delta[r+1]减去x。

那么问题来了,如何查询区间[l,r]的和?

我们设a[1...i]的和为sum[i],根据delta数组的定义,则:

\[sum[i]=\sum_{j=1}^ia[j]+\sum_{j=1}^idelta[j]*(i-j+1) \]

\[sum[i]=\sum_{j=1}^ia[j]+(i+1)*\sum_{j=1}^idelta[j]-\sum_{j=1}^idelta[j]*j \]

这样我们就不难看sum[i]是由哪三个部分组成的了。我们需要用一个asum数组维护a数组的前缀和,delta1与delta2两个树状数组,delta1维护delta数组的和,delta2维护delta[i]*i的和,代码如下:

void add(int *arr int pos,int x){
    while(pos<=n) arr[pos]+=x,pos+=lowbit(pos);
}
void modify(int l,int r,int x){
    add(d1,l,x),add(d1,r+1,-x),add(d2,l,x*l),add(d2,r+1,-x*(r+1));
}
int getsum(int *arr,int pos){
    int sum=0;
    while(pos) sum+=arr[pos],pos-=lowbit(pos);
    return sum;
}
int query(int l,int r){
    return asum[r]+r*getsum(d1,r)-getsum(d2,r)-(asum[l-1]+l*getsum(d1,l-1)-getsum(d2,l-1));
}

摘自


咳咳,回归正题,总结一下
普通差分就是这个数减去前一个数所得到的一个数组,他不是个算法,只是种技巧,比如在树状数组中的妙用,让树状数组具有区间查询,单点修改的功能。

差分套差分(二阶差分)

没错你没有听错,差分都可以套了!

好像又叫二阶差分。

怎么套?将差分数组再差分一遍,求到了差分套差分的数组,定位c数组。

那么,推一推,发现\(a[i]=c[i]*1+c[i-1]*2+...+c[1]*(i-1)\)

嗯,这个有什么用呢?

如果有一个毒瘤出题人,出了一道题(就是我被坑了,就写出来了):

给你一个长度为n的序列a,有m次操作,每次操作让区间[l,r]分别加上t,t*2,t*3,...,t*(r-l+1)
最后输出a序列的每个数的值

把1操作中加上的数差分,就为\(t,t,t,t,t,...,t\)(注意:以后求高阶差分的修改公式,将加上的数组也进行差分来推是最好的!),那么,就等于给a的差分数组b区间加上t,那么就将b再差分出另一个差分数组c来更改,最后O(n)输出一下答案就好了。

当然,相比差分,差分套差分会有更多应用,欢迎大家探究!

高阶差分

\(n(n>1)\)阶等差数列就是两项之差的序列是\(n-1\)阶等差数列的序列。

当然,要处理这个的话一般是用FFT来搞。

想学习这个请食用FFT或者其他类似算法。

树上等差数列

基本概念:

  1. 树上两点之间只有一条最短路径
  2. 树上两点只有一个最近公共祖先

1. 点差分

点差分求什么?

给你一棵树,并给你一些在树上的路径,让你求每个点在树上被经过的次数。

整篇博客没一张图。。。
在这里插入图片描述
图中红色绿色蓝色代表三条路径,点旁边的标记代表他被经过的次数。

如何求?
暴力!
在这里插入图片描述

。。。
DFS暴力的话,肯定过不了呀!如果你送毒瘤出题人足够刀片说不定可以。。。

这时候,就有人跳出来发明了个算法,叫树上差分:
设路径的开头与结尾为\(st\)\(ed\),设\(k=lca(st,ed)\)\(father_{k}\)\(k\)\(father\)
那么我们把\(f[st]++,f[ed]++,f[k]--,f[father_{k}]--\),有什么用?
在这里插入图片描述

我们跑用DFS遍历一遍一棵树,设\(tot_{i}\)\(i\)的子树的所有节点的\(f\)和,如图:在\(st->ed\)这条路径中,除了\(ed\)外,\(tot\)值都是\(1\),又因为\(k\)点的\(f\)值为\(-1\),所以将一个1消掉了,所以\(k\)的tot值也为1.

某银:那\(k\)的父亲呢?

因为我们让\(father_{k}\)\(f值也减了1\),所以他和\(k\)的1抵消了,所以并没有影响。

所以我们只需要\(O(1)\)将所有路径处理完,\(O(n)\)遍历处理答案就好了!

2. 边差分

跟点差分差不多。

给你一棵树,并给你一些在树上的路径,让你求每条边在树上被经过的次数。

在这里插入图片描述

首先,我们应当考虑把边压到点里面,那么我们就让每个点到父亲的那条边压到这个点身上,然后求每个点被经过的次数就行了,点差分一下,有什么难?

恭喜你WA了。
在这里插入图片描述

难道你就没有发现只算一条路径的话,\(k\)到父亲这条边没有被经过,但是在点差分过程中\(tot_{k}=1\)吗?所以,我们应当改一下修改\(f\)值的过程。

\(f[st]++,f[ed]++,f[k]-=2\)

那么在\(k\)点的时候,就把\(st、ed\)的影响消掉了,是不是很舒服?

最后DFS一遍,别忘了每个点代表的是他到父亲的边!

那么不就解决了?

至此,基础的差分结束了。

终于写完了,ヾ(≧▽≦*)o,<( ̄ˇ ̄)/,~( ̄▽ ̄~)(~ ̄▽ ̄)~,(:逃

欢迎大家D我,让我能更好的完善博客!

posted @ 2018-11-06 11:01  敌敌畏58  阅读(456)  评论(0编辑  收藏  举报