樹狀陣列基礎引入: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;
}
二.舉一反三
上題是直接告訴你要求逆序對,如果是下題呢?
題目:氣泡排序(從小到大)
描述:將一列數用氣泡排序,問最少交換多少次。
很明顯,這道題不能直接模擬過程。因為從小到大排序,那麼原本有序的兩個數就根本不用動,只有兩個數之間是逆序關係 才會交換兩數。所以他要求的就是逆序對的個數
三.總結
樹狀陣列是十分有用的,再查訊某數的位置和某區間數的總和十分有用,所以要好好學樹狀陣列。