1. 程式人生 > >樹狀陣列基礎引入:BZOJ 1266 計算逆序對問題

樹狀陣列基礎引入: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;
}

二.舉一反三

      上題是直接告訴你要求逆序對,如果是下題呢?

      題目:氣泡排序(從小到大)

      描述:將一列數用氣泡排序,問最少交換多少次。

      很明顯,這道題不能直接模擬過程。因為從小到大排序,那麼原本有序的兩個數就根本不用動,只有兩個數之間是逆序關係  才會交換兩數。所以他要求的就是逆序對的個數


三.總結

      樹狀陣列是十分有用的,再查訊某數的位置某區間數的總和十分有用,所以要好好學樹狀陣列。