P1908逆序对:从顺序表到树状数组

1 题目链接

洛谷P1908
本文参考的题解

2 顺序表解题思路

2.1 重要变量

  1. a数组:存储输入的元素
  2. s数组:用于记录元素加入逆序对统计过程的状态(可结合2.2的表格理解)。
  3. ans:记录当前逆序对的数量,初始化为0。
  4. ranks数组:存储输入的元素从小到大排序对应的序号。
    另外还需要定义结构体node,其成员变量有no和val。a数组是node类型的。

2.2 解题过程

  1. 在输入n个数时,no存储输入的顺序,val存储输入的值。
  2. 对a数组按照元素值从小到大排序,如果遇到元素值相同的情况,则输入顺序靠前的排在前面。
  3. 按照输入的顺序,获取每个元素的排序编号,以输入顺序为下标,存在ranks数组中。比如对于输入的6个数,5,1,2,6,3,1,则ranks[1]=5,ranks[2]=1,ranks[3]=3,ranks[4]=6,ranks[5]=4,ranks[6]=2。
  4. 遍历ranks数组的每个元素,根据表达式s[ranks[i]]=1,填数,过程如下:
    1)ranks[1]=5,所以s[5]=1,如下表所示。之后用变量tmp存储s[1]到s[5]填了1的格子数量,可知tmp=0。接着,我们用表达式 i - tmp表示填入第i个数位置有多少个数比第i个数大。然后将这个表达式结果加进ans中。可知当前ans = 0。
下标123456
数值000010

2)ranks[2]=1,所以s[1]=1,如下表所示。和1)一样,用i-tmp算出当前比第二个数大的数,表达式结果为1,更新ans的值,ans+=i-tmp=1。这里可以这样理解:如果我们把ranks里面的元素比作一群旅客住在宾馆的楼层(假设每层只住1人),每个旅客按照下标顺序入住,每个人想知道前面有多少人住的楼层比自己高,最后统计一个总和存储在ans中。对于第二个人住在1楼,他可以在上到自己的那层之前每层都去看一下,统计入住人数tmp。在这里ranks[2]=1,ranks[1]=5,所以2号旅客上到1楼入住后,他统计一下1楼之前及1楼只住了1个人,而他是第二个入住的,所以有一个人住的比他高。ans+=i-tmp=1.

下标123456
数值100010

3) ranks[3]=3,所以s[3]=1。同样,用2)中的例子来讲,第3个人住3楼,他也想知道有多少人住的比他高。所以在在上到自己的那层之前每层都去看一下,统计入住人数tmp=s[1]+s[2]+s[3]=2。而他是第3个入住的,所以ans+=i-tmp=2.

下标123456
数值101010

4) ranks[4]=6,所以s[6]=1。同样,第4个人住6楼,他也想知道有多少人住的比他高。所以在在上到自己的那层之前每层都去看一下,统计入住人数tmp=4。而他是第4个入住的,所以ans+=i-tmp=2.

下标123456
数值101011

5) ranks[5]=4,所以s[4]=1。同样,第5个人住4楼,他也想知道有多少人住的比他高。所以在在上到自己的那层之前每层都去看一下,统计入住人数tmp=3。而他是第5个入住的,所以ans+=i-tmp=4.

下标123456
数值101111

6) ranks[6]=2,所以s[2]=1。同样,第6个人住2楼,他也想知道有多少人住的比他高。所以在在上到2楼之前每层都去看一下,统计入住人数tmp=2。而他是第6个入住的,所以ans+=i-tmp=8.

下标123456
数值111111

按照这个过程去计算,最终样例的答案是8。

2.3 代码实现

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int s[500010],ranks[500010],n;
long long ans; 
struct point
{
    int num,val;
}a[500010];
inline bool cmp(point q,point w)
{
    if(q.val==w.val)
        return q.num<w.num;
    return q.val<w.val;
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i].val),a[i].num=i;
    sort(a+1,a+1+n,cmp);
    for(int i=1;i<=n;i++)
        ranks[a[i].num]=i;
    for(int i=1;i<=n;i++)
    {
    	s[ranks[i]]=1;
    	int tmp = 0;
        for(int j=1;j<=ranks[i];j++){
        	tmp += s[j];
		}
		ans+=i-tmp;
    }
    printf("%lld",ans);
    return 0;
}

3 树状数组解题思路

3.1 问题分析

用顺序表实现的代码提交测评会超时,时间主要浪费在这个双重循环里面↓

for(int i=1;i<=n;i++)
    {
    	s[ranks[i]]=1;
    	int tmp = 0;
        for(int j=1;j<=ranks[i];j++){
        	tmp += s[j];
		}
		ans+=i-tmp;
    }

3.2 树状数组解题思路

一个个去数s的元素效率太低,我们可以用树状数组去存储和统计。树状数组如下图所示:
在这里插入图片描述
在树状数组中,一开始每个节点的值初始化为0,每个节点会随其指向的所有节点和它自己的变化而变化。比如,tree[1]加了1,那么tree[2],tree[4],tree[8]也会加上1。变化时,我们可以发现只要对下标在原有基础上加上lowbit运算的结果,就可以变换到对应的下标,拿tree[5],tree[6],tree[8]举例:5+5&(-5)=6,6+6&(-6)=8。从树状数组的值变化可以看出,有变化更新时,不再是一个一个更新,而是基于lowbit运算更新,这样效率更高。

拿前面的ranks[1]=5,ranks[2]=1,ranks[3]=3,ranks[4]=6,ranks[5]=4,ranks[6]=2和旅客入住的例子来说一下如何用树状数组接这道题:
1) 首先第1个人入住5楼,他会在门前做一个记号:5楼又有1人入住,即tree[5]+=1,接着跑6楼门前又做1个记号:5楼和6楼又有1入住,即tree[6]+=1。他想知道有多少人住的比他高,他可以统计1-4楼的入住人数存在tmp中,再用i-tmp得到答案。因为tree[4]会随着tree[2]和tree[3]的值更新而更新,tree[2]随着tree[1]的值的更新而更新(后面会讲到),所有tmp=tree[4]+tree[5]=1。ans+=i-tmp,即ans=0。

下标123456
数值000011

2) 接着第2个人入住1楼,他也会在门前做一个记号:1楼又有1人入住,即tree[1]+=1,接着跑2楼门前又做1个记号:1-2楼又有1入住,即tree[2]+=1,接着跑4楼门前又做1个记号:1-4楼又有1入住,即tree[4]+=1。因为ranks元素范围是1-6,所有第2个人就不跑到8楼去了。tmp=tree[1]。ans+=2-tmp,即ans=1。

下标123456
数值110111

3) 再是第3个人入住3楼,他也会在门前做一个记号:3楼又有1人入住,即tree[3]+=1,接着跑4楼门前又做1个记号:1-4楼又有1入住,即tree[4]+=1。因为ranks元素范围是1-6,所有第3个人就不跑到8楼去了。tree[3]记录了3楼入住的人数,tree[2]记录了1-2楼的入住人数。tmp=tree[3]+tree[2]=2。ans+=3-tmp,即ans=2。

下标123456
数值111211

4)之后第4个人住6楼,他也会在门前做一个记号:6楼又有1人入住。因为ranks元素范围是1-6,所有第4个人就不跑到8楼去了。tree[4]记录了当前1-4楼的入住人数,tree[6]记录了5-6楼入住的人数,故tmp=tree[6]+tree[4]。ans+=4-tmp,即ans=2。

下标123456
数值111212

5)第5个人住4楼,他也会在门前做一个记号:4楼又有1人入住。因为ranks元素范围是1-6,所有第5个人就不跑到8楼去了。tree[4]记录了1-4楼的入住人数,故tmp=tree[4]。ans+=5-3,即ans=4。

下标123456
数值111312

6)最后第6个人住2楼,他也会在门前做一个记号:2楼又有1人入住。接着跑4楼门前又做1个记号:1-4楼又有1入住,即tree[4]+=1。因为ranks元素范围是1-6,所有第6个人就不跑到8楼去了。tree[2]记录了1-2楼的入住人数。故tmp=tree[2]。ans+=6-2,即ans=8。

下标123456
数值121412

这样每个人就不需要在入住之前一层一层的数人,只要在到达自己入住的楼层后,再到指向自己的楼层获取已入住的人数,用表达式计算即可,大大提高了效率。

3.3 代码实现

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int tree[500010],ranks[500010],n;
long long ans; 
struct point
{
    int num,val;
}a[500010];
bool cmp(point q,point w)
{
    if(q.val==w.val)
        return q.num<w.num;
    return q.val<w.val;
}
void insert(int p,int d)
{
    for(;p<=n;p+=p&-p)
        tree[p]+=d; 
}
int query(int p)
{
    int sum=0;
    for(;p;p-=p&-p)
        sum+=tree[p];
    return sum;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i].val),a[i].num=i;
    sort(a+1,a+1+n,cmp);
    for(int i=1;i<=n;i++)
        ranks[a[i].num]=i;
    for(int i=1;i<=n;i++)
    {
        insert(ranks[i],1);
        int tmp = query(ranks[i]);
        ans+=i-tmp;
    }
    printf("%lld",ans);
    return 0;
}
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值