树状数组基础引入:BZOJ 1266 计算逆序对问题

目录

一.题目

题目描述

输入

输出

样例输入

样例输出

题解

     二路归并

    树状数组

举一反三

总结


一.题目


题目描述

      设A[1..n]是一个包含N个数的数组。如果在i〈 j的情况下,有A[i] 〉a[j],则(i,j)就称为A中的一个逆序对。 例如,数组(3,1,4,5,2)的“逆序对”有 <3,1>,<3,2>,<4,2>,<<5,2> 共4个。 使用 归并排序 可以用O(nlogn)的时间解决统计逆序对个数的问题 。

输入

      第1行:1个整数N表示排序元素的个数。(1≤N≤100000) 第2行:N个用空格分开的整数,每个数在小于100000。

输出

      1行:仅一个数,即序列中包含的逆序对的个数。

样例输入

3

1 3 2

样例输出

1


题解

      这是一道十分简单的树状数组引入题目。相信大家一定都做过逆序对吧,首先来看二路归并的代码:

     


二路归并

            十分简单,只要会二分排序,那么就一定会这道题:

            代码如下:

#include <cstring>
#include <cstdio>
#define M 100005
int n, a[M], b[M], c[M];
long long ans;
inline void bing(int l, int mid, int r){
    int k = l, k1 = mid + 1, k2 = l;
    while(k <= mid && k1 <= r){
        if(a[k] <= a[k1])
            b[k2 ++] = a[k ++];
        else{
            ans = ans + mid - k + 1;
            b[k2 ++] = a[k1 ++];
        }
    }
    while(k <= mid) 
        b[k2 ++] = a[k ++];
    while(k1 <= r)
        b[k2 ++] = a[k1 ++];
    for(int i = l; i <= r; i ++)
        a[i] = b[i];
}
inline void fen(int l, int r){
    int mid = (l + r) / 2;
    if(l >= r)
        return ;
    fen(l, mid);
    fen(mid + 1, r);
    bing(l, mid, r);
} 
int main (){
    scanf("%d", &n);
    for(int i = 1; i <= n; i ++)
        scanf("%d", &a[i]);
    fen(1, n);
    printf("%lld", ans);
    return 0;
} 

     


但有时这种方法也许处理不了大数据,我们接下来看另一种新方法:

    树状数组

            我们的主要思想就是:每次往后找到比这个数更小的数,且使那一个数的BIT加1,最后再一个数一个数地算前缀和即可。

            但是,如果这样的话,有的数有可能会很大,那么我们的数组就会爆掉,于是,引入离散化:

           


离散化

             这是我的个人解释:

                    当一个数列中,数字十分的大但是我们只需要这些数字在数列中所在位置时,就可以用离散化。如图所示,就是一个离散化后的结果:

                   

             注:原数组的下表是那个数本身的位置,离散化后是从小到大的每个数的位置。

                   那么,怎么做到呢?主要思想就是:先存下每个数的原始位置,然后将它们排序,又用另一个数组存下每个数排序后的位置,离散化就完成了。(注意去重)

                   有两种方法:

                   1.数组

                   显而易见,就用以上方法:

                   代码如下:

for(int i = 1; i <= n; i ++){    
    scanf("%d", &a[i].val);    
	a[i].id = i;
}
    sort(a + 1, a + n + 1);           //定义结构体时按val从小到大重载
    for(int i = 1; i <= n; i ++)    
	b[a[i].id] = i;                    //将a[i]数组映射成更小的值,b[i]就是a[i]对应的rank(顺序)值

                   2.STL+二分

                   总体思想差不多,只是用了更高级的函数而已。

                   代码如下:

    #include<algorithm> // 需要的头文件
    //n原数组大小   num原数组中的元素    lsh离散化的数组    cnt离散化后的数组大小 
    int lsh[MAXN] , cnt , num[MAXN] , n;
    for(int i=1; i<=n; i++) 
    {	
        scanf("%d",&num[i]);	
        lsh[i] = num[i];	//复制一份原数组
    }
    sort(lsh+1 , lsh+n+1); //排序,unique虽有排序功能,但交叉数据排序不支持,所以先排序防止交叉数据
    //cnt就是排序去重之后的长度
    cnt = unique(lsh+1 , lsh+n+1) - lsh - 1; //unique返回去重之后最后一位后一位地址 - 数组首地址 - 1
    for(int i=1; i<=n; i++)	
        num[i] = lower_bound(lsh+1 , lsh+cnt+1 , num[i]) - lsh;
    //lower_bound返回二分查找在去重排序数组中第一个等于或大于num[i]的值的地址 - 数组首地址 ,从而实现离散化

                    要介绍几个函数:

                    (1)unique(起始下标,终止下标)

                     unique返回去重之后最后一位后一位地址 。在其后减lsh是因为unique返回的是一个指针,要减lsh才返回那一个地址;减1是因为unique返回的是最后一位后一位地址,所以要减1。

                    (2)lower _bound(起始下标,终止下标,查找的值)

                     lower_bound返回二分查找在去重排序数组中第一个等于或大于num[i]的值的地址,减lsh的原因同上。

                     有了离散化,我们就能很方便地用树状数组求值了。首先进入一个1到n的循环,每次就是往后更新一遍树状数组再找一次前缀和即可。前缀和找的就是比这个数小的数已经放了多少个,再用i去减这些数共有多少个,就求出了当前比这个数大的数有多少个,再全部累加起来就求出了共有多少个逆序对。

           


如下图:

           1.初始化:

           2.第一个:

         3.

         4.

         5.

         6.注意再次有一个去重操作,因为有两个数离散后下标都是1,所以直接在C数组下标1的位置再加1。

        7.最后一次操作:

       


所以代码如下:

#include <cstdio>
#include <algorithm>
#include <iostream>
using namespace std;
#define M 100005
struct node{
    int v, id;
}a[M];
int n, b[M], c[M];
long long ans;
bool cmp (node x, node y){
    return x.v < y.v; 
}
int lowbit(int i){
    return i & -i;
}
void update(int k, int x){
    for(int i = k; i <= n; i += lowbit(i))
        c[i] += x;
}
long long SUM (int x){
    int s = 0;
    for(int i = x; i >= 1; i -= lowbit(i))
        s += c[i];
    return s;
}
int main (){
    scanf("%d", &n);
    for(int i = 1; i <= n; i ++){
        scanf("%d", &a[i].v);
        a[i].id = i;
    }
    sort(a + 1, a + 1 + n, cmp);
    int cnt = 0;
    for(int i = 1; i <= n; i ++){
        if(a[i].v != a[i - 1].v)//去重操作
            cnt ++;
        b[a[i].id] = cnt;
    }
    for(int i = 1; i <= n; i ++){
        update (b[i], 1);
        ans += i - SUM (b[i]); 
    }
    printf("%lld", ans);
    return 0;
}

二.举一反三

      上题是直接告诉你要求逆序对,如果是下题呢?

      题目:冒泡排序(从小到大)

      描述:将一列数用冒泡排序,问最少交换多少次。

      很明显,这道题不能直接模拟过程。因为从小到大排序,那么原本有序的两个数就根本不用动,只有两个数之间是逆序关系  才会交换两数。所以他要求的就是逆序对的个数


三.总结

      树状数组是十分有用的,再查讯某数的位置某区间数的总和十分有用,所以要好好学树状数组。
 

猜你喜欢

转载自blog.csdn.net/weixin_43908980/article/details/84833109