树状数组(下)

树状数组(下)

目录

  • 树状数组(下)

    • 应用

      • 逆序对
      • 康托展开
      • 逆康托展开
      • RMQ问题树状数组解法
      • 查询第k小
    • 习题
      • Preprefix sum

树状数组(上)中我提到了树状数组的基本操作与变式,现在来看看它的实际应用和一些题目。

应用

逆序对

设\(a\)为一个有\(n\)个数字的有序集(\(n>1\)),其中所有数字各不相同。

如果存在正整数\(i\),\(j\)使得\(1\leqslant i<j\leqslant n\)且\(a[i]>a[j]\),

则有序对\((a[i],a[j])\)称为\(a\)的一个逆序对。

用树状数组的方法可以\(O(n\log_2 n)\)求一段正整数序列中逆序对的数目。

思路很简单,先离散化(否则\(\text{MLE}\)),然后按照序列顺序从左到右地用树状数组记录每个\(a_i\)出现了几次,遍历到\(a_i\)时就\(\operatorname{add}(a_i)\),同时\(ans\!\gets\!ans\!+\!\operatorname{ask}(a_i)\)。由于下标小于\(i\)且能与\(a_i\)构成逆序对的数一定会在\(a_i\)之前加入,因此\(\operatorname{ask}(a_i)\)返回值是\(a_i\)与其之前的数构成的逆序对数量。

当然,需要考虑重复元素的情况,处理这个问题只要在离散化排序的时候以下表作为第二关键字排序就可以了(或者用\(\text{algorithm}\)库的\(\operatorname{stable\_sort}()\))。

LG1908 逆序对

模板题:

#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
using namespace std;
int tree[500005];
struct Tmp{
	int d,r;
}a[500005];
int n;
long long ans;
inline bool cmp(Tmp a,Tmp b)
{
	if(a.d==b.d)
		return a.r>b.r;
	return a.d>b.d;
}
inline void add(int x,int k)
{
	for(;x<=n;x+=x&-x)
		tree[x]+=k;
}
inline int ask(int x)
{
	int res=0;
	for(;x;x-=x&-x)
		res+=tree[x];
	return res;
}
int main()
{
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;i++)	a[i].r=i,cin>>a[i].d;
	sort(a+1,a+n+1,cmp);
	for(int i=1;i<=n;i++)
		ans+=ask(a[i].r),add(a[i].r,1);
	printf("%lld\n",ans);
	return 0;
}

康托展开

康托展开是一个全排列到一个自然数的双射,常用于建立哈希表时的空间压缩。

具体来说,设\(X\)表示一个排列前有几个字典序小于它的排列(元素相同),则有下式:

\[X=\sum_{i=1}^{n}a_i\times{(n-i)!}
\]

其中的\(a_i\)代表\(\sum_{j=i}^n(a_j<a_i)\),以排列\(A\{2,3,1,4,5\}\)为例,\(a_1=1\),\(a_2=1\),\(a_3\),\(a_4\),\(a_5\)都为\(0\)。

(当然,对于一个长度为\(n\)的排列,\(a_n\)始终为\(0\))

我们来解释一下这个式子。

首先考虑第一位,\(A_1=2\),\(a_1=1\),因此前一位与\(A_1\)相同的排列有\(1\times{(5-1)!}=24\)种。

接着考虑第二位,\(A_1=3\),\(a_2=1\),因此前两位与\(A_2\)相同的排列有\(1\times{(5-2)!}=6\)种。

依此类推,直到第五位为止(不都是零吗)。

所以排列\(A\{2,3,1,4,5\}\)前有\(30\)个排列,它在全排列中的序号为\(31\)。

现在需要解决如何计算\(a_i\)的问题。

显而易见,可以用一个树状数组维护\(a_i\),使得\(\operatorname{ask}(i)\)表示“\(A_i\)在区间\([1,i]\)中出现了几次”。可以先从\(1\)到\(n\)\(\operatorname{add}(i,1)\),每次读入\(A_i\)时\(\operatorname{add}(A_i,-1)\),再\(\operatorname{ask}(a_i)\),加到答案中。

LG5367【模板】康托展开

模板题:

#include<iostream>
#include<cstdio>
#define MOD 998244353
using namespace std;
int tr[1000005],a[1000001],n;
long long fac[1000005]={1},ans;//fac是预处理出的阶乘,fac[0]=1
inline void add(int x,int k){for(;x<=n;x+=x&-x)	tr[x]+=k;}
inline int ask(int x)
{	int res=0;
	for(;x;x-=x&-x)
		res=(res+tr[x])%MOD;
	return res;
}
int main()
{
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;i++)
		fac[i]=fac[i-1]*i%MOD,add(i,1);
	for(int i=1;i<=n;i++)
		cin>>a[i],add(a[i],-1),ans=(ans+(ask(a[i]))*fac[n-i])%MOD;
	printf("%lld",ans+1);
	return 0;
}
//没开long long见祖宗

另外,康托展开主要用于排列的哈希。

逆康托展开

康托展开是一个双射,因此在已知元素及其先后顺序的情况下可以通过一个排列的康托展开值还原出这个排列,也就是“逆康托展开”。

看个例子:

\((1,2,3,4,5)\),\(ans=31\)。

  • 首先\(ans-1\)得到康托展开值\(30\)
  • \(30/4!=1\),\(30\bmod(4!)=6\),因此\(a_1=1\),第一个元素为\(2\)
  • \(6/3!=1\),\(6\bmod(3!)=0\),因此\(a_2=1\),第二个元素为\(3\)
  • \(0/2!=0\),\(0\bmod(2!)=0\),因此\(a_3=0\),第三个元素为\(1\)
  • \(0/1!=0\),\(0\bmod(1!)=0\),因此\(a_4=0\),第四个元素为\(4\)
  • \(0/0!=0\),\(0\bmod(0!)=0\),因此\(a_5=0\),第五个元素为\(5\)

    原排列为\(A\{2,3,1,4,5\}\)。

    具体做法代更(这就是你咕咕咕的借口)

RMQ问题树状数组解法

RMQ问题:在一段连续区间查找最大最小值的问题

众所周知,利用ST表可以实现\(O(n)\)预处理,\(O(1)\)查找。

不过,在这里我们主要谈该问题的树状数组解法(\(O(\log_2 n)\)的查找)。

建立很简单,模板改一下就可以了

#define lowbit(x) x&-x
int n,a[N],trmin[N],trmax[N];
inline void add(int x,int k)
{
	for(;x<=n;x+=lowbit(x))
	{
		trmax[x]=max(trmax[x],k);
		trmin[x]=min(trmin[x],k);
	}
}

查询则难一些,因为RMQ问题不满足区间减法性质,你不能用两区间相减得出某一区间的最值。这时需要另一种思路。

根据树状数组的性质(1):\(C[i]=\sum_{j=i-\operatorname{lowbit}(i)+1}^{n}\!{A[j]}\),可知\(trmax[i]\)的值为闭区间\([i-\operatorname{lowbit}(i)+1,i]\)中的最大值。

因此对于闭区间\([l,r]\):

  • 如果\(l\leqslant r-\operatorname{lowbit}(r)+1\),说明\(trmax[r]\)的值对\([l,r]\)有效,所以答案为\(\max({trmax[r],\operatorname{askmax}(l,r-\operatorname{lowbit}(r)))}\)。
  • 如果\(l<r-\operatorname{lowbit}(r)\),说明\(trmax[r]\)的值对\([l,r]\)无效(因为我们并不能确定这个值是否在\([l,r]\)中出现过),所以答案为\(\max({a[r],\operatorname{askmax}(l,r-1)})\)(可以证明这里的\(\operatorname{askmax}()\)一定会变成第一种情况)。
  • 如果\(l\geqslant r\),返回\(a[l]\)即可(思考一下上面两种情况的\(\operatorname{askmax}\)函数的参数)

    看到这里就可以尝试打代码了。

//这是查询最大值的做法,查询最小值同理
int askmax(int l,int r)
{
    if(l<r)
        if(r-lowbit(r)>l) return max(trmax[r],askmax(l,r-lowbit(r)));
        else return max(a[r],askmax(l,r-1));
    return a[l];
}

当然也有用循环的做法,不过个人感觉递归好写点。

给出模板题:

[USACO07JAN]Balanced Lineup G

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int n,m;
int a[50005],trmax[50005],trmin[50005];
//由于奇怪的原因,在这道题中不用lowbit函数会TLE(至少我调试的时候TLE了(雾))
inline int lowbit(int x)
{
    return x&-x;
}
inline void add(int x,int k)
{
    for(;x<=n;x+=lowbit(x))
    {
        trmax[x]=max(trmax[x],k);
        trmin[x]=min(trmin[x],k);
    }
}
inline int askmax(int l,int r)
{
    if(l<r)
        if(r-lowbit(r)>l)    return max(trmax[r],askmax(l,r-lowbit(r)));
        else    return max(a[r],askmax(l,r-1));
    return a[l];
}

inline int askmin(int l,int r)
{
    if(l<r)
        if(r-lowbit(r)>l)    return min(trmin[r],askmin(l,r-lowbit(r)));
        else    return min(a[r],askmin(l,r-1));
    return a[l];
}
int main()
{
    ios::sync_with_stdio(false);
    memset(trmin, 0x3f3f3f3f, sizeof(trmin));
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        cin>>a[i],add(i,a[i]);
    for(int i=1,l,r;i<=m;i++)
        cin>>l>>r,cout<<askmax(l,r)-askmin(l,r)<<endl;
    return 0;
}

查询第k小

这个问题的树状数组解法时间复杂度\(O((\log_2\!n)^2)\),好像还比较优秀?

具体解法待更

习题

Preprefix sum

这题是一个叫前前缀和的东西(???)

首先,不能用树状数组维护树状数组来求“前前缀和”,因此需要化简式子:

前前缀和的式子是

\[SS_i=\sum_{j=1}^i\sum_{k=1}^j{a_k}
\]

\[=\sum_{j=1}^i(i-j+1){a_j}
\]

\[=(i+1)\times\sum_{j=1}^{i}a_j-\sum_{j=1}^{i}(j\times a_j)
\]

是不是很熟悉?

所以你应该知道怎么办了[滑稽.jpg]

#include<iostream>
#include<cstdio>
#define N 100005
using namespace std;
long long tree[N],tree2[N],a[N],n,m;
inline void add(long long x,long long k)
{
    for(long long x0=x;x<=n;x+=x&-x)
        tree[x]+=k,tree2[x]+=k*x0;
}
inline long long ask(long long x)
{
    long long ans=0;
    for(long long x1=x+1;x;x-=x&-x)
        ans+=x1*tree[x]-tree2[x];
    return ans;
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>n>>m;
    string c;
    for(long long i=1;i<=n;i++)
        cin>>a[i],add(i,a[i]);
    for(long long i=1,I,X;i<=m;i++)
    {
        cin>>c>>I;
        if(c[0]==‘M‘)  cin>>X,add(I,X-a[I]),a[I]=X;
        if(c[0]==‘Q‘)  printf("%lld\n",ask(I));
    }
    return 0;
}

原文地址:https://www.cnblogs.com/LZShuing-xuan/p/12547127.html

时间: 2024-10-07 14:40:35

树状数组(下)的相关文章

hdu 5249区间第k大(学习了下树状数组的搞法)

KPI Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 205    Accepted Submission(s): 70 Problem Description 你工作以后, KPI 就是你的全部了. 我开发了一个服务,取得了很大的知名度.数十亿的请求被推到一个大管道后同时服务从管头拉取请求.让我们来定义每个请求都有一个重要值.我的K

Vijos P1066 弱弱的战壕【多解,线段树,暴力,树状数组】

弱弱的战壕 描述 永恒和mx正在玩一个即时战略游戏,名字嘛~~~~~~恕本人记性不好,忘了-_-b. mx在他的基地附近建立了n个战壕,每个战壕都是一个独立的作战单位,射程可以达到无限(“mx不赢定了?!?”永恒[email protected][email protected]). 但是,战壕有一个弱点,就是只能攻击它的左下方,说白了就是横纵坐标都不大于它的点(mx:“我的战壕为什么这么菜”ToT).这样,永恒就可以从别的地方进攻摧毁战壕,从而消灭mx的部队. 战壕都有一个保护范围,同它的攻击

【初识——树状数组】 区间求最值

说树状数组其实是一个索引表,但是是一个特殊的,树状的索引表,它利用了二进制的一些特性. 就区间求和的要求来说: 首先我们用a[]数组来存储原始数据.然后在a[]之上构造c[]数组来作为树状数组. 如图 这个图表示,当i为奇数时,c[i]中保存的都是a[i]本身.然后,c[2]中保存了a[1], a[2],共2个,c[4]中保存的是a[1], a[2], a[3], a[4],c[6]又是保存两个,c[5]和c[6].c[8]保存8个,c[1], c[2], c[3], c[4], c[5], c

实用数据结构---树状数组(二叉索引树)

树状数组适用于动态连续和查询问题,就是给定一个区间, 查询某一段的和或者修改某一位置的值. 关于树状数组的结构请去百度百科,否则将看不懂下面内容 我们看这个题 士兵杀敌(二) 时间限制:1000 ms  |  内存限制:65535 KB 难度:5 描述 南将军手下有N个士兵,分别编号1到N,这些士兵的杀敌数都是已知的. 小工是南将军手下的军师,南将军经常想知道第m号到第n号士兵的总杀敌数,请你帮助小工来回答南将军吧. 南将军的某次询问之后士兵i可能又杀敌q人,之后南将军再询问的时候,需要考虑到新

UVA 10869 - Brownie Points II(树状数组)

UVA 10869 - Brownie Points II 题目链接 题意:平面上n个点,两个人,第一个人先选一条经过点的垂直x轴的线,然后另一个人在这条线上穿过的点选一点作垂直该直线的线,然后划分出4个象限,第一个人得到分数为1,3象限,第二个人为二四象限,问第一个个人按最优取法,能得到最小分数的最大值,和这个值下另一个人的得分可能情况 思路:树状数组,可以枚举一点,如果能求出右上和左下点的个数就好办了,其实用一个树状数组,把y坐标离散化掉,然后记录进来,然后把点按x从左往右,每次删掉点后查询

浅谈二维中的树状数组与线段树

一般来说,树状数组可以实现的东西线段树均可胜任,实际应用中也是如此.但是在二维中,线段树的操作变得太过复杂,更新子矩阵时第一维的lazy标记更是麻烦到不行. 但是树状数组在某些询问中又无法胜任,如最值等不符合区间减法的询问.此时就需要根据线段树与树状数组的优缺点来选择了. 做一下基本操作的对比,如下图. 因为线段树为自上向下更新,从而可以使用lazy标记使得矩阵的更新变的高校起来,几个不足就是代码长,代码长和代码长. 对于将将矩阵内元素变为某个值,因为树状数组自下向上更新,且要满足区间加法等限制

浅析树状数组(二叉索引树)及一些模板

树状数组 动态连续和查询问题.给定一个n个元素的数组a1.a2.……,an,设计一个数据结构,支持以下两种操作:1.add(x,d):让ax增加d;2.query(l,r):计算al+al+1+…+ar 如何让query和add都能快速完成呢?方法有很多,这里介绍的便是树状数组.为此我们先介绍lowbit. 对于正整数x,我们定义lowbit(x)为x的二进制表达式中最右边的1所对应的值(而不是这个比特的序号).比如,38288的二进制1001010110010000,所以lowbit(3828

求逆序数数目(树状数组+离散化)

404在玩忍者印记(Mark of the Ninja)操纵忍者时遇到这样一个场景,两栋大楼之间有许多绳索,从侧面看,就像这个样子: 我们的忍者非常有好奇心,他可以观察到每个绳索的端点在两栋楼的高度,想知道这些绳索有多少个交点(图中黑色的点).他观察到不会建筑上不会有一点上有两个绳索,并且没有三条绳索共点. 输入描述 第一行:整数T,代表有T组数据. (1 <= T <= 100) 下一行:整数N,代表有N条绳索. (1 <= N <= 100000) 接下来Na行给出两个整数A_

ZOJ-2386 Ultra-QuickSort 【树状数组求逆序数+离散化】

Description In this problem, you have to analyze a particular sorting algorithm. The algorithm processes a sequence of n distinct integers by swapping two adjacent sequence elements until the sequence is sorted in ascending order. For the input seque