简单的前置知识。
树状数组 1.基本功能 树状数组可以在 $O(\log n)$ 的时间里,完成1.求前缀和或者2.修改某一个数。
最坏情况下,也只是 $O(m \log n)$ 的复杂度。
2.基本原理 基于二进制拆分思想。
$\forall x\in N^+, x=2^ {i_k}+2^{i_{k-1}}+…+2^{i_2}+2{i_1}$,
其中 $i_k\geq i_{k-1} \geq…\geq i_2\geq i_1$。
于是,我们将整个区间分为 $\log n$ 个区间。
分别为 $[x-2^{i_1}+1,x]$,$[x-2^{i_2}-2^{i_1}+1,x-2^{i_1}]$ … $[1,2^{i_{k}}]$。
每次,都可以取出 $2^i$,它是 x 的二进制表示的最后一位 1。
定义 $\operatorname{lowbit}(r)$ 表示 r 的最后一位 1 所表示的数。
那么,$\forall x$,第一个区间都可以表示为 $[x-\operatorname{lowbit}(x)+1,x]$
又发现,每一个 x,只有一个区间,而这个区间又很容易得到。
那么,让树状数组 $c[x]$ 表示区间$[x-\operatorname{lowbit}(x)+1,x]$
又二进制拆分最多只有 $\log n$ 位,所以每一个 x 的前缀和都可以拆分为 $\log n$ 个区间。
举个例子,13 的区间计算如下:
$13=(1101)_2,\operatorname{lowbit}(13)=1:[13,13]$
$13-\operatorname{lowbit}(13)=12$
$12=(1100)_2,\operatorname{lowbit}(12)=4:[9,12]$
$12-\operatorname{lowbit}(12)=8$
$8=(1000)_2,\operatorname{lowbit}(8)=8:[1,8]$
$8-\operatorname{lowbit}(8)=0$
结束。
讨论如何单点修改。
比如,我们需要在 13 加一个数。
可以发现,14 16 都要加上这个数。
相当于, $c[x]$ 代表的区间包含该数,那么该点需要修改。
将树状数组改为树的形式,也就是它的祖先都要修改。
当 13 加上 $\operatorname{lowbit}(13)$,则从低到高所有连续的 1 都会向前进位,直到碰到一个0。
这样得到的数,所包含的区间一定包含该数。
最后,我们还没讨论 $\operatorname{lowbit}(x)$。
如何计算 $\operatorname{lowbit}(x)$。
结论:$\operatorname{lowbit}(x)=x$ & $(-x)$
$x=(…100…0)_2$(前面随意)。
又 $-x=\sim x+1$,所以所有的 0 都变成 1,接着加 1,所有最后的 1 都进位,直到原来的 1 不会进位,所以 & 后,不会舍去,而其他位都被去掉。
3.扩展 1)差分 支持区间修改和单点查询。
直接维护当前的差分数组即可(差分就是 $b[i]=a[i]-a[i-1]$ )
2)差分加区间和 同样维护一个差分数组,也就是维护前缀和。
公式如下:
进一步推导可以得到:
分离 x 与 i:
维护 b[x] 的前缀和和 $x * b[x]$ 的前缀和即可。
4.例题 T1:楼兰图腾 题目传送门 AcWing
维护当前左端的数,即可。
先从左向右记录比当前数大的个数和小的个数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #define lowbit(x) x&(-x) #define ll long long using namespace std;const int N=2e5 +10 ;ll c[N],g[N],l[N]; int n,a[N];void add (int x,int r) { for (int i=x;i<=n;i+=lowbit (i)) c[i]+=(ll)r; } ll sum (int x) { ll res=0 ; for (int i=x;i;i-=lowbit (i)) res+=c[i]; return res; } int main () { cin>>n; for (int i=1 ;i<=n;++i) scanf ("%d" ,a+i); for (int i=1 ;i<=n;++i) { g[i]=sum (n)-sum (a[i]); l[i]=sum (a[i]-1 ); add (a[i],1 ); } memset (c,0 ,sizeof c); ll res1=0 ,res2=0 ; for (int i=n;i;--i) { res1+=g[i]*(sum (n)-sum (a[i])); res2+=l[i]*(sum (a[i]-1 )); add (a[i],1 ); } printf ("%lld %lld" ,res1,res2); return 0 ; }
T2:一个简单的整数问题 题目传送门 AcWing
刚才扩展 1 的模板题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <iostream> #include <algorithm> #include <cstring> #include <cstdio> #define lowbit(x) x&-x #define ll long long using namespace std;const int N=1e5 +10 ;ll c[N]; int n;void add (int x,int r) { for (int i=x;i<=n;i+=lowbit (i)) c[i]+=(ll)r; return ; } ll sum (int x) { ll res=0 ; for (int i=x;i;i-=lowbit (i)) res+=c[i]; return res; } int main () { int m,x,y,t,l=0 ; char op[5 ]; cin>>n>>m; for (int i=1 ;i<=n;++i) { scanf ("%d" ,&x); add (i,x-l); l=x; } while (m--) { scanf ("%s%d" ,op,&x); if (op[0 ]=='Q' ) { cout<<sum (x)<<endl; } else { scanf ("%d%d" ,&y,&t); add (y+1 ,-t);add (x,t); } } return 0 ; }
T3:一个简单的整数问题2 题目传送门 AcWing
扩展 2 的模板题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #define lowbit(x) x&(-x) #define ll long long using namespace std;const int N=1e5 +10 ;ll c[2 ][N],a[N],n,sum[N]; void add (int k,int x,ll d) { for (;x<=n;x+=lowbit (x)) c[k][x]+=d; } ll query (int k,int x) { ll ans=0 ; for (;x;x-=lowbit (x)) ans+=(ll)c[k][x]; return ans; } int main () { int m,b;ll d; cin>>n>>m; for (int i=1 ;i<=n;++i) scanf ("%lld" ,a+i),sum[i]=sum[i-1 ]+a[i]; char op[3 ];int a; while (m--) { scanf ("%s%d%d" ,op,&a,&b); if (op[0 ]=='C' ) { scanf ("%lld" ,&d); add (0 ,a,d);add (0 ,b+1 ,-d); add (1 ,a,a*d);add (1 ,b+1 ,-(b+1 )*d); } else { ll ans=sum[b]-sum[a-1 ]; ans+=(b+1 )*query (0 ,b)-query (1 ,b); ans-=(a)*query (0 ,a-1 )-query (1 ,a-1 ); printf ("%lld\n" ,ans); } } return 0 ; }
T4:谜一样的牛 题目传送门 ACWing
在线维护从前往后第 $a[i]+1$ 个1,就是本题答案。
考虑如何维护。
首先维护成前缀和。
一种比较简单的方法就是二分答案。
时间复杂度为 $O(n \log^2 n)$。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <iostream> #include <cstring> #include <algorithm> #include <cstdio> #define lowbit(x) x&-x using namespace std;const int N=1e5 +10 ;int c[N],n,a[N],ans[N];void add (int x,int r) { for (int i=x;i<=n;i+=lowbit (i)) c[i]+=r; } int sum (int x) { int ans=0 ; for (int i=x;i;i-=lowbit (i)) ans+=c[i]; return ans; } int main () { cin>>n; for (int i=2 ;i<=n;++i) scanf ("%d" ,a+i); for (int i=1 ;i<=n;++i) add (i,1 ); for (int i=n;i;--i) { int l=1 ,r=n; while (l<r) { int mid=l+r>>1 ; if (sum (mid)>=a[i]+1 ) r=mid; else l=mid+1 ; } ans[i]=l; add (l,-1 ); } for (int i=1 ;i<=n;++i) printf ("%d\n" ,ans[i]); return 0 ; }
另一种是倍增算法。
我们发现,树状数组恰好为我们维护的 2 的整数幂。
考虑使用倍增,如果当前的和小于需要的结果,那么就加上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <iostream> #include <cstring> #include <algorithm> #include <cstdio> #define lowbit(x) x&-x using namespace std;const int N=1e5 +10 ,L=20 ;int c[N],n,a[N],ans[N];void add (int x,int r) { for (int i=x;i<=n;i+=lowbit (i)) c[i]+=r; } int sum (int x) { int ans=0 ; for (int i=x;i;i-=lowbit (i)) ans+=c[i]; return ans; } int main () { cin>>n; for (int i=2 ;i<=n;++i) scanf ("%d" ,a+i); for (int i=1 ;i<=n;++i) add (i,1 ); for (int i=n;i;--i) { int now=0 ,nowsum=0 ; for (int j=L;j>=0 ;--j) { if (now+(1 <<j)>n) continue ; if (nowsum+c[now+(1 <<j)]<a[i]+1 ) now+=(1 <<j),nowsum+=c[now]; } ans[i]=now+1 ; add (ans[i],-1 ); } for (int i=1 ;i<=n;++i) printf ("%d\n" ,ans[i]); return 0 ; }
算法
时间
空间
二分答案算法
267 ms
2514 KB
倍增算法
134 ms
2008 KB
实测 AcWing。