码迷,mamicode.com
首页 > 编程语言 > 详细

树状数组(下)

时间:2020-04-05 22:19:08      阅读:102      评论:0      收藏:0      [点我收藏+]

标签:通过   序号   整数   sum   head   logs   情况   ble   连续   

树状数组(下)


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

应用

逆序对

\(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;
}

树状数组(下)

标签:通过   序号   整数   sum   head   logs   情况   ble   连续   

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

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!