树状数组( 二 )

之前已经讲道树状数组的几个好用的方法(链接:https://blog.csdn.net/REfusing/article/details/82350189),这里就不过多讲述了,这篇这要讲述树状数组用在逆序对的个数。

1.什么是逆序对?

  • 逆序对就是设 A 为一个有 n 个数字的有序集 (n>1),其中所有数字各不相同。如果存在正整数 i, j 使得 1 ≤ i < j ≤ n 而且 A[i] > A[j],则 <A[i], A[j]> 这个有序对称为 A 的一个逆序对,也称作逆序数。例:在5  4  6  7  2中逆序对有<5,4>,  <5,2>, <4,2>, <6,2>, <7,2>总共5个。

2.逆序对怎么求?

  • 5  4  6  7  2  这几个的逆序数可以这么求,假设这个数组中没有任何一个数,不断的向里面插入数,然后统计比最新插入的这个数大的个数m(i),然后把所有的m(i)加一起。所得的记过就是逆序对的对的个数。
  • 首先插入5, 5 前面没有比5 大的,所以sum = 0。
  • 然后插入4, 4前面有一个比他大的数, sum+1,sum此时等于1。
  • 然后在插入6, 6前面没有比6大的数,所以sum不用更新。
  • 再插入7, 7 前面同样没有比7大的数,sum也不用更新。
  • 再插入2, 2前面有4个比2大的数,sum + 4, 此时sum = 5.
  • 5就是逆序对数。

3.用树状数组的作用

  • 用树状数组求逆序对有点像桶排序,就比如5  4  6  7  2,初始化数组为0,然后插入一个5,a[5]++(为什么是a[5]++,因为可能5有重复的,所以是加加,而不是直接赋值为1,然后累加这个数前面的所有数,然后在用此时的位置减去累计的结果,就是此时插入位置的逆序数。数状数组就是用来查找的,树状数组有个功能单点修改区间查询的功能,所以可以每add(pos, 1),此处的位置加1,然后查询前面有多少个比大他的数。在这里树状数组的作用就是减少查询的时间复杂度,如果数据小的话直接就利用像桶排的方法,如果数据比特别大的话可以利用树状数组也不行,因为数组不能开这么大,所以就有了一个比较好用的方法

4.如果数据较大怎么办?

  • 如果数据较大那么就利用离散的方法处理数据,其他的和之前没有啥区别,如何用离散的方法处理数据呢?就是把这些数缩小,给的原始数据就是用来判断大小的,所以可以把这些数据按照从小到大的顺序排序,如果这个数这第m小,让m取代原来原来的数,让m放置在原来的位置。
  • 例5  4  6  7  2 ,5是第3小,所以5还是被3取代, 4是第2小所以4被2取代,6是第4小的,所以6被4取代,7是第5小的数,所以7倍5取代,2是第1小的数,所以2被1取代,最后变成   3  2  4  5  1.其实跟之前大小是一样的只是数据的规模变小了。

以CodeVs《1688 求逆序对》为例

链接:http://codevs.cn/problem/1688/

此题的数据比较小最多才1e5所以可以不用离散化处理直接就可以进行处理。

#include<stdio.h>
#include<iostream>
#include<stdio.h>
#include<algorithm>
using namespace std;
#define N 100001
int c[N] = {0}, a[N], maxn = 0;
int n;
int lowbit(int a){
    return a & (-a);
}
void add(int pos, long long val){
     for(int i = pos; i <= maxn; i += lowbit(i)){
        c[i] += val;
     }
}
long long Getsum(int pos){
    int sum = 0;
    for(int i = pos; i > 0; i -= lowbit(i)){
        sum += c[i];
    }
    return sum;
}
int main(){
    cin>>n;
    int i;
    for(i = 1; i <= n; i++){
        cin>>a[i];
        if(a[i] > maxn)maxn = a[i];//maxn用来更新最多数组能用到多大,不断更新
    }
    long long sum = 0;
    for(i = 1; i <= n; i++){
        add(a[i], 1);
        sum += i - Getsum(a[i]);//Getsum用来求插入时,前面有多少比他小的数m,然后总插入的数减去m就是此时你逆序数
    }
    cout<<sum<<endl;
return 0;
}

下面也是求逆序对,但是这道题的数据就比较大1e9,数组开不来这么大,所以需要进行离散化处理。

洛谷 P1908 逆序对   链接:https://www.luogu.org/problemnew/show/P1908

这道题怎么离散化处理,就是利用结构体加上排序

struct Node{
       long long val;
       int id;
}node[N];
bool cmp(Node A, Node B){
      return A.val < B.val;
}
sort(node + 1, node + n + 1, cmp);

为什么这么操作,因为这么做就是相当于val和id就是一个映射关系,id就代表与原始数据意义对一,然后遍历一遍,找到每个数是第几小的。下面是对这些数据进行缩小范围的过程。

    int t = 1;
    b[node[1].id] = t; 
    long long k = node[1].val;
    for(i = 2; i <= n; i++){
        if(node[i].val != k){
                k = node[i].val;
                t++;
        }
        b[node[i].id] = t;
    }

为什么这么做,因为这么做就把数据大小一样的设置为同样是第 i 小。

#include<stdio.h>
#include<iostream>
#include<stdio.h>
#include<algorithm>
using namespace std;
#define N 500001
long long c[N] = {0};
int b[N] = {0};
int n;
struct Node{
       long long val;
       int id;
}node[N];
bool cmp(Node A, Node B){
      return A.val < B.val;
}
int lowbit(int a){
    return a & (-a);
}
void add(int pos, long long val){
     for(int i = pos; i <= n; i += lowbit(i)){
        c[i] += val;
     }
}
long long Getsum(int pos){
    long long sum = 0;
    for(int i = pos; i > 0; i -= lowbit(i)){
        sum += c[i];
    }
    return sum;
}
int main(){
    scanf("%d", &n);
    int i;
    long long a;
    for(i = 1; i <= n; i++){
        scanf("%lld", &a);
        node[i].val = a;
        node[i].id = i;
    }
    long long  sum = 0;
   
    sort(node + 1, node + n + 1, cmp);
    int t = 1;
    b[node[1].id] = t; 
    long long k = node[1].val;
    for(i = 2; i <= n; i++){
        if(node[i].val != k){
                k = node[i].val;
                t++;
        }
        b[node[i].id] = t;
    }
    for(i = 1; i <= n; i++){
        add(b[i], 1);
        sum += i - Getsum(b[i]);
    }
    printf("%lld\n", sum);
return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值