1. 程式人生 > >[分治與資料結構]逆序對

[分治與資料結構]逆序對

目錄

題目描述

解題思路

方法1.分治

方法2.樹狀陣列


題目描述

設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

解題思路

方法1.分治

歸併排序求逆序對,典型的分治,詮釋了分而治之。首先將原數列進行遞迴分解,分解到1個的時候,我們便可以返回進行合併操作了,合併所構成的數列我們可以保證是一定有序的(從大到小或從小到大)。

所以在合併時我們如果發現有與i與j(j>i&&a[i]>a[j])一對逆序對那麼i所在序列的後面的那一些數肯定也會與j成為逆序對

於是這題的方法就出來了,表示這就是歸併的板題。操作大致如圖

程式碼如下

#include <cstdio>
#include <cstring>
#include <iostream>
#include <cmath>
#include <vector>
#include <algorithm>
#include <queue>
#include <map>
#define max(a,b) a > b ? a : b
using namespace std;
int n,a[100005],b[100005];
long long ans;
void msort(int l,int r){
    if (l >= r)
        return ;
    int mid = (l + r ) / 2;
    msort (l,mid);
    msort(mid + 1 ,r);
    int x = l,y = mid + 1,k = l;
    while (x <= mid && y <= r){
        if (a[x] > a[y]){
            b[k ++]  = a[y ++];
            ans += mid - x+1;
        }
        else
            b[k ++ ] = a[x ++];
    }
    while (x <= mid )
        b[k ++ ] = a[x ++];
    while (y <= r)
        b[k ++ ] = a[y ++];
    for (int i = l; i <= r ;i ++ )
        a[i] = b[i];
}
int main(){
    scanf ("%d",&n);
    for (int i = 1;i <= n; i ++ ){
        scanf ("%d",&a[i]);
    }
    msort(1,n);
    printf ("%lld",ans);
}

 

方法2.樹狀陣列

先說說樹狀陣列的一系列定義吧

樹狀陣列(Binary Indexed Tree(B.I.T)也稱作Fenwick Tree)是一區間查詢單點修改複雜度都為log(n)的資料結構。主要用於查詢任意兩點之間的所有元素之和樹狀陣列(Binary Indexed Tree(B.I.T)

首先先給一個簡易樹狀陣列的圖,順便看看它是怎麼生成的

 

 

由圖,我們可以看出樹狀陣列使用二進位制數所形成的。c[i]表示i所擁有的子節點所對應的數之和,但如何確定i有幾個子節點呢,應該累加那些呢?所以有了構造樹狀陣列近乎模板的東西。

這裡就介紹一下lowbit

lowbit(i)的意思是i 轉化成二進位制數之後,保留最低位的1及其後面的0,截斷前面的內容,然後再轉十進位制數,這個數也是樹狀陣列中i號位的子葉個數。

例如:lowbit(22)22的二進位制數為10110,進行lowbit操作後,就成了這一個二進位制編碼便成了10再轉換成十進位制數,就是2,也就是說22的子節點總數只有2,c[22]=a[21]+a[22];

同時update(原陣列的值進行單點修改)與sum(字首和)也是會經常用到的。

int lowbit(int x)//求子節點的個數
{
	return x & -x;
}
void update(int k,int x)
{
	for(int i = k; i <= n; i += lowbit(i))//單點進行更新,這一個點的父節點也要加上
    //因此i加上lowbit(i),到達它父節點的下標
		C[i] += x;
}
void Sum(int k)
{
	for(int i = k; i > 0; i -= lowbit(i))//求和函式,由於求1-k的和,c[i]是累加了的
    //因此所有i的子節點都不需要再加。
		B[k] += C[i];	
}

 

構造樹狀陣列我就不再贅述。這裡介紹一下本題需要使用的離散化

離散化的意義

離散化可以有效地降低時間複雜度,對一些無法作為下標進行操作但它們本身屬性並不重要的數可以進行操作。

離散化的兩種實現方法

(1).陣列離散化

操作前後如圖

這裡其實就是運用一種下標的思想,逆序對知道他們的大小是沒有多大用處的,所以我們用離散化,降低資料。noip提高的火柴排隊也可以說使用了離散化與歸併求逆序對。

for(int i = 1; i <= n; i ++){    
    cin >> 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]的值的地址 - 陣列首地址 ,從而實現離散化

解釋一下unique吧。平常沒有怎麼用(對於我來說)

unique函式屬於STL中的函式,它的功能是元素去重。即”刪除”序列中所有相鄰的重複元素(只保留一個)。此處的刪除,並不是真的刪除,而是指重複元素的位置被不重複的元素給佔領了(詳細情況,下面會講)。由於它”刪除”的是相鄰的重複元素,所以在使用unique函式之前,一般都會將目標序列進行排序。因此如果相同的數相隔較遠,那麼便不能夠完全去重,所以先用sort排序。這裡要注意一下,並這道題並不能說原陣列有重複就要刪去,因此我們離散化還是要覆蓋原陣列的每一個數

弄完離散之後,我們便可以考慮構造樹狀陣列了。構造樹狀陣列有點難以描述,通過構造,我們可以得到sum(k)就是得到了順序對的個數,那麼用這一個i也就是表示原數是第幾個數來減去這一個順序對個數即可。來個圖例

根據圖片,樹狀陣列C其實就是B陣列從1-6進行遍歷進行更值的。到i時,c[b[i]]就要加1,同時,它的父節點也要弄。就相當於update(k,1)。

具體操作見程式碼

/*
    樹狀陣列模板題。
*/
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
#include <iostream>
#include <cstdlib>
#include <algorithm>
#include <queue>
#include <map>
using namespace std;
struct node {
    int num,val;
    bool operator < (const node &x)const {
        if (x.val < val)
            return 0;
        else
            return 1;
    }
};
int n,b[100005],c[100005],a[100005];
long long ans;
int lowbit (int x){//lowbit(x)表示第x個數的子節點個數。
    return x & -x;
}
void update(int k){//單個點值進行改變。
    for (int i = k ;i <= n ;i += lowbit(i))
        c[i] += 1;
}
long long  sum (int k){//求字首和
    long long tot = 0;
    for (int i = k;i >=1 ;i -=lowbit(i))
        tot += c[i];
    return tot;
}
int main(){
    scanf ("%d",&n);
    for (int i = 1;i <= n; i ++ ){
        scanf ("%d",&a[i]);
        b[i] = a[i];
    }
    //以下全是進行離散化處理,從小到大,求出逆序對個數,同時還要進行去重。
    //兩種處理方法,一是陣列,一是STL
    sort(b + 1,b + 1 + n);
    int cnt = unique(b + 1,b + 1 + n) - b - 1;
    for (int i = 1;i <= n;i ++)
        a[i] = lower_bound(b + 1,b + 1 + cnt,a[i]) - b;
    /*int cnt = 0;//陣列進行離散化
    for (int i = 1;i <= n; i ++){
        if (a[i].val != a[i - 1].val)
            cnt ++;
        b[a[i].num] = cnt;
    }*/
    for (int i = 1;i <= n;i ++ ){
        update(a[i]);//相當於構造樹狀陣列
        ans += i - sum(a[i]); //求出字首和,sum其實就表示第i個數前面小於這一個數的。用i減去這一個字首和,就求出了在i前面比第i個數小的數。
    }
    printf("%lld",ans);
}