return 0;
}
三、树状数组求逆序数
(1) 树状数组的原理及应用
树状数组顾名思义就是一个数组,只是我们把它看做是一棵树罢了。
???
“一个数组怎么可能是一棵树?”你可能会有这样的疑问。
首先我先放一个代码:
int lowbit(int x)
{
return x&-x;
}
可以发现这段代码虽然看起啦简单,但是实际理解起来却些许困难。
首先要知道一个常识:“计算机中所有的整数都是以补码的形式来存储的。”
所以我们可以知道,该代码实际上就是一个正数的补码与改正数的相反数的补码相“与”,得到的结果为2^k(从0开始,低位到高位,k为最后一个1所在的位置)。
然后,对于每一个节点来说,我们引入一个前缀和的概念(本博文仅仅解释最基本的操作,其实只要满足可加性即可。因而采用最简单的前缀和来解释)。只不过,这个不是简单的将前面所有的数加起来,而是将该点前2^k(从0开始,低位到高位,k为最后一个1所在的位置)累加的结果,即tree[x]存储的是a[x-lowbit(x)+1]~a[x]区间的所有数之和。
例如:
分析:这样做有什么用?
这样能够方便的进行单点查询和区间修改()。
例如:我们想要知道前7项的前缀和,我们仅仅只需要将树状数组中[7,6,4]累加起来即可,其时间复杂度为O(logn)。
同理,若我们想更改第3项的数,我们仅仅只需要更改第[3,4,8...]一直到区间的上限为止,同理其时间复杂度为O(logn)。
所以,单点修改,区间查询的代码如下:
void modify(int pos,int v,int n)
{
while(pos<=n)
{
tree[pos]+=v;
pos+=lowbit(pos);
}
}
int query(int l,int r)
{
int ansl=0,ansr=0;
l--;//前缀和,减前一个。
while(l>0)
{
ansl+=tree[l];
l-=lowbit(l);
}
while(r>0)
{
ansr+=tree[r];
r-=lowbit(r);
}
return ansr-ansl;
}
(2) 树状数组求逆序数的原理
因为树状数组的作用就是处理单点更新,区间查询的操作,我们可以转化一下思维。
因为逆序数顾名思义就是有多少前面已经出现的数比当前的数的大的个数总和。
所以,我们可以建立一个权值树状数组,在遍历的时候记录已经出现的数的个数。其中,通过遍历的顺序来保证i>j,在寻找时求得有多少个数比当前值大的,其时间复杂度为O(nlogn)。
注:建立权值树状数组时,因为数的值可能过于大,可能会造成空间不足,且空间浪费,所以我们采用离散化的思想。
下面是y总离散化的代码:
#include
#define FAST ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
#define int long long
using namespace std;
const int N=1e6+7;
vectornums;
int a[N];
int find(int x)
{
return lower_bound(nums.begin(),nums.end(),x)-nums.begin()+1; //返回的是从1开始的数
}
signed main()
{
FAST;
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
nums.push_back(a[i]);
}
sort(nums.begin(),nums.end());
nums.erase(unique(nums.begin(),nums.end()),nums.end());
for(int i=0;i
cout<
}
return 0;
}
(3) 树状数组求逆序数的代码
#include
#define FAST ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
#define int long long
using namespace std;
const int N=1e6+7;
vectornums;
int find(int x){
return lower_bound(nums.begin(),nums.end(),x)-nums.begin()+1;
}
int lowbit(int x){
return x&-x;
}
int a[N],tree[N];
void modify(int pos,int v,int n){
while(pos<=n){
tree[pos]+=v;
pos+=lowbit(pos);
}
}
int query(int l,int r){
int ansl=0,ansr=0;
l--;//前缀和,减前一个。
while(l>0){
ansl+=tree[l];
l-=lowbit(l);
}
while(r>0){
ansr+=tree[r];
r-=lowbit(r);
}
return ansr-ansl;
}
signed main(){
int n,ans=0;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
nums.push_back(a[i]);
}
sort(nums.begin(),nums.end());
nums.erase(unique(nums.begin(),nums.end()),nums.end());
for(int i=1;i<=n;i++)
{
int v=find(a[i]);
ans+=query(v+1,n+1);//此时nums可能的最大值为n,n+1避免越界
modify(v,1,n+1);
}
cout<
return 0;
}
四、线段树求逆序数
(1) 线段树的原理及应用
什么是线段树?
线段数顾名思义就是一棵树,他能够在log的时间复杂度上处理区间等问题。
详细请见:
线段树原理及应用
(2) 线段树求逆序数的原理
我么知道,线段树可以处理区间问题,所以,我们仍然可以类似于树状数组求逆序数。
建立一个权值线段树。同样的,为了防止空间过大和空间浪费,我们可以使用离散化进行处理。
(3) 线段树求逆序数的代码
#include
#define FAST ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
#define int long long
using namespace std;
const int N=1e6+7;
struct node
{
int l;
int r;
int v;
}tree[N<<2];
vectornums;
int a[N];
void pushup(int x)
{
tree[x].v=tree[x<<1].v+tree[x<<1|1].v;
}
void built(int x,int l,int r)
{
tree[x]={l,r,0};
if(l==r)
{
return;
}
int mid=(l+r)>>1;
built(x<<1,l,mid);
built(x<<1|1,mid+1,r);
pushup(x);
}
void modify(int x,int pos,int v)
{
if(tree[x].l==tree[x].r)
{
tree[x].v+=v;
return;
}
int mid=(tree[x].l+tree[x].r)>>1;
if(pos<=mid)modify(x<<1,pos,v);
if(pos>mid)modify(x<<1|1,pos,v);
pushup(x);
}
int query(int x,int l,int r)
{
if(tree[x].l>=l&&tree[x].r<=r)
{
return tree[x].v;
}
int mid=(tree[x].l+tree[x].r)>>1;
int v=0;
if(l<=mid)v+=query(x<<1,l,r);
if(r>mid)v+=query(x<<1|1,l,r);
return v;
}
int find(int x)
{
return lower_bound(nums.begin(),nums.end(),x)-nums.begin()+1;
}
signed main()
{
FAST;
int n,ans=0;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
nums.push_back(a[i]);
}
built(1,1,n+1);
sort(nums.begin(),nums.end());
nums.erase(unique(nums.begin(),nums.end()),nums.end());
for(int i=1;i<=n;i++)
{
int v=find(a[i]);
ans+=query(1,v+1,n+1);
modify(1,v,1);
}
cout<
return 0;
}
当然,如果题目给与的空间足够大的话,我们可以不需要离散化,可以采用动态开点线段树的方法求。其时间复杂度仍然是O(nlogn)。
五、Trie树(字典树)求逆序数
(1) Trie树(字典树)的原理及应用
所谓的字典数顾名思义就是将一些字符串通过一种方式结合的一种数据结构,以便用很好的处理一堆字符串的某些共同性质,且性质不以字符串的输入顺序有关。
例如,我们需要存储一些字符串
abaacabacabcac
那么,这些字符串构成的trie树为
其中,圆圈内红色的数字代表点的编号;圆圈外橙色数字表示从根节点到该节点有一个节点;箭头边黑色的字母表示状态转换的条件。
代码如下:
#include
#define FAST ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
#define int long long
using namespace std;
const int N=1e6+7;
char a[N];
int trie[N][26];//trie树
int sum[N];//记录有多少字符串的数量
int dfn=0;//时间戳
void insert(char *str)
{
int p=0;
for(int i=0;str[i];i++)
{
int u=str[i]-'a';
if(!trie[p][u])trie[p][u]=++dfn;
p=trie[p][u];
}
sum[p]++;
}
signed main()
{
FAST;
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a;
insert(a);
}
return 0;
}
(2) Trie树(字典树)求逆序数的原理
首先我们回到本质上来。
什么是逆序数,本质上来说就是满足iaj的这样的一个二元组。
对于处理iaj我们可以通过二进制数来判断。
判断两个数的大小,就是判断位数和位数上的数字。
例如对于[12 15 7 2]这个序列来说:
进制转换
十进制数121572二进制数1100111111110
我们可以依次的从高位到低位比大小。
为了使比较更加方便,我们将所有的数补齐高位0。
即 :
进制转换
十进制数121572二进制数1100111101110010
然后进行比较即可。
根据上面的原理,我们不难想出一个规律:
定义一个sum数组,表示在p节点上,经过"1"这个转换条件的数量.
将数依次插入trie树中。若该节点为0,则需要统计该兄弟节点的值ans+=sum[p](在该位比当前数大的数);若该节点为1,则需要加数量sum[p]++(在该位比当前数大的数);
如图:
该图树上的圈内红色的数字表示节点的编号;箭头上黑色的数字表示转换条件;圆圈边上的数字表示在p节点上,经过"1"数量;颜色表示不同的插入顺序所产生的贡献值。
(3) Trie树(字典树)求逆序数的代码
#include
#define FAST ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
using namespace std;
const int N=1e6+7;
int trie[N*10][2];//trie树开大一点
int sum[N*10];//开大一点
int dfn=0;//时间戳
long long ans=0;//记得long long
void insert(int v)
{
int p=0;
for(int i=31;i>=0;i--)//根据题意扩展到31位
{
int u=(v>>i)&1;//从高位到低位取值
if(!trie[p][u])trie[p][u]=++dfn;
if(u==0)
{
ans+=sum[p];
}
else
{
sum[p]++;
}
p=trie[p][u];
}
}
signed main()
{
FAST;
int n,x;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>x;
insert(x);
}
cout<
return 0;
}
毫无疑问,这个时间复杂度比上述的更低,只有O(nlog(ai))但是实际上,运行时间却比第一个归并排序要慢。原因是需要开辟很多的空间。