树状数组例题

树状数组例题

1.【模板】树状数组1

题意:序列单点修改,区间查询

思路:模板

#include<bits/stdc++.h>
#define lowbit(x) -x&x
using namespace std;
const int N = 5e5+5;

int n,m;
int opt,x,k;
int sum[N];

void add(int x,int k){
	for(;x<=n;x+=lowbit(x))
		sum[x]+=k;
}
int query(int x){
	int res=0;
	for(;x;x-=lowbit(x))
		res+=sum[x];
	return res;
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,t;i<=n;i++)
		scanf("%d",&t),add(i,t);
	while(m--){
		scanf("%d%d%d",&opt,&x,&k);
		if(opt==1)add(x,k);
		else printf("%d\n",query(k)-query(x-1));
	}
	return 0;
}

树状数组的核心其实是那个 #define lowbit(x) -x&x

链接

2.[POJ3468]A Simple Problem with Intergers

题意:序列区间修改,区间查询

思路:线段树秒杀(!!注意常数),以下摘自《算法竞赛进阶指南》

首先简化题目:区间修改,单点查询怎么做?

我们知道树状数组的本质是前缀和,设原序列 \(a[]\) 不妨建立一个差分数组 \(b[i]=a[i]-a[i-1]\)

这时候如果我们要将区间 \([l,r]\) 增加 \(d\) ,转化成了这样: \(b[l]+=d;b[r+1]-=d\)

考虑这时候的 \(b\) 的前缀和:

  • 对于 \(1\leq x<l\) ,前缀和不变
  • 对于 \(l\leq x\leq r\) ,前缀和增加 \(d\)
  • 对于 \(r<x\leq n\) ,前缀和不变

我们发现,b 数组的前缀和 \(b[1...x]\) 就是区间 \([l,r]\) 增加 \(d\)\(a\) 的影响,于是可以用树状数组维护 \(b\)

容易发现,修改后 \(a[x]\) 的实际值 \(=query(x)+a[x]\)

好的,现在回到原问题。

我们现在知道 \(\sum_{i=1}^xb[i]\) 就是区间修改后 \(a[x]\) 增加的值。

那么序列 \(a\) 的前缀和 \(a[1..x]\) 整体增加的值就是:

\[\sum_{i=1}^x\sum_{j=1}^ib[j]=\sum_{i=1}^x(x-i+1)*b[i]=(x+1)\sum_{i+1}^xb[i]-\sum_{i=1}^xi*b[i] \]

所以我们可以再用一个树状数组维护 \(i*b[i]\) 的值。

于是,回答询问 \([l,r]=(sum[r]+(r+1)*query(c_0,r)-query(c_1,r))-\\(sum[l-1],(r-1)*query(c_0,l-1)-query(c_1,l-1))\)

其中树状数组 \(c_0\) 维护 \(i*b[i]\) ,树状数组 \(c_1\) 维护 \(b[i]\)

//[POJ3468]A_Simple_Problem_with_Integers
#include<bits/stdc++.h>
#define lowbit(x) -x&x
using namespace std;
typedef long long ll;
const int N = 1e5+5;

int a[N],n,m;
ll sum[N],c[2][N];

void add(int which,int x,int val){
    for(;x<=n;x+=lowbit(x))
        c[which][x]+=val;
}

ll query(int which,int x){
    ll res=0;
    for(;x;x-=lowbit(x))
        res+=c[which][x];
    return res;
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        sum[i]=sum[i-1]+a[i];
    }
    while(m--){
        char op[2];
        int l,r,d;
        scanf("%s%d%d",op,&l,&r);
        if(op[0]=='C'){
            scanf("%d",&d);
            add(0,l,d);
            add(1,l,d*l);
            add(0,r+1,-d);
            add(1,r+1,-d*(r+1));
        }else{
            ll ans=sum[r]+(r+1)*query(0,r)-query(1,r);
            ans-=sum[l-1]+l*query(0,l-1)-query(1,l-1);
            printf("%lld\n",ans);
        }
    }
}

链接

3.[POJ2352]Stars

题意:链接

分析:这里 https://blog.csdn.net/qq_42367703/article/details/98093999 说得很清楚

4.「POJ2299」Ultra-QuickSort

题意:请分析,对于一串数列,至少要交换多少次(相邻两个数交换)才能使该数列有序(从小到大)?

分析:逆序对模板

for(int i=n;i;i--){
	ans+=query(a[i]-1);
	add(a[i],1);
}

5.「NOIP2013」火柴排队

题意:现有两列每列个数为 \(n\) 的火柴,且每列中火柴棒的高度均不相同,求得到 \(\sum[(a_i-b_i)^2]\) 的最小值的时候,最少需要交换火柴的次数,其中 \(i\) 表示 \(a、b\) 两列火柴棒中第i根火柴

分析:由数据范围,我们首先知道要离散化,然后我们可以对式子进行变型:

\[\sum(a_i-b_i)^2=\sum a_i^2+b_i^2-2a_ib_i=\sum(a_i^2+b_i^2)-\sum(2a_ib_i) \]

所以也就是求 \(\sum(a_ib_i)\) 最大值,

容易证明, 对于数列 \(k_1\dots k_n,p_1\dots p_n,\sum(k_i*p_i)\) 的最小值要求两个数列有序的分别从小到大(或从大到小)排列

也即是:顺序之乘>=乱序之乘

所以也就是逆序对了。

链接

6.楼兰图腾

题意:给定一串序列,分别找到满足 \(1<=i<j<k<=n\)\(yi>yj,yj<yk\) (称为‘V’图腾); 满足 \(1<=i<j<k<=n\)\(y_i<y_j,y_j>y_k\) (称为' ∧ ' 图腾)的数量

分析: 树状数组求逆序对,让我们知道了如何在一个序列中计算每个数后面有多少个数比它小,因此我们可以通过这个性质来做一些事情

‘v’图腾求法

正序扫描序列 \(a \) ,利用树状数组求出每个 \(a[i]\) 前面有几个数比它大,记录为 \(left[i]\)

for(int i=1;i<=n;add(a[i++],1))l[i]=query(mx)-query(a[i]);

倒序扫描序列 \(a\),利用树状数组求出每个 \(a[i]\) 后面有几个数比它大,记录为 \(right[i]\)

for(int i=n;i>=1;add(a[i--],1))r[i]=query(mx)-query(a[i]);

然后我们就可以枚举每一个点为中间点,那么这个点为中心的’v’图腾的个数就是\(\sum_{i=1}^n=left[i]\times right[i]\)
’^’图腾求法

正序扫描序列 \(a\),利用树状数组求出每个 \(a[i]\) 前面有几个数比它小,记录为 \(left[i]\)

for(int i=1;i<=n;add(a[i++],1))l[i]=query(a[i]-1);

倒序扫描序列 \(a\),利用树状数组求出每个 \(a[i]\) 后面有几个数比它小,记录为 \(right[i]\)

for(int i=n;i>=1;add(a[i--],1))r[i]=query(a[i]-1);

(这波是压行大法好)

然后我们就可以枚举每一个点为中间点,那么这个点为中心的’^’图腾的个数就是\(\sum_{i=1}^1=left[i]×right[i]\)

同时注意数组清空

这段分析摘自(并良心地加上了 \(Latex\) ):

作者:秦淮岸灯火阑珊
链接:https://www.acwing.com/solution/content/1008/
来源:AcWing

7.「IOI2001」Mobile Phones

题意:矩阵内单点修改,区域询问

分析:二维树状数组

链接

8.[CQOI2006]简单题

题意:有一个 \(n\) 个元素的数组,每个元素初始均为 \(0\)。有 \(m\) 条指令,有两种操作:

  1. 形如\(1 l r\),将区间 \([l,r]\) 之间的数反转,反转规则是:0 变为 1,1 变为 0。
  2. 形如 \(2 x\),输出数组下标为 \(x\) 的值

分析: 由1变0,0变1这一波操作你们想到了什么? 异或

区间修改,考虑异或差分序列,每个元素的真实值是异或前缀和,修改的时候改 \(l\)\(r+1\) 两个点,树状数组维护。

链接

posted @ 2021-02-21 22:47  _Famiglistimo  阅读(194)  评论(0编辑  收藏  举报