1. 程式人生 > >逆序數介紹以及算法實現淺析

逆序數介紹以及算法實現淺析

模擬 情況 一個 class 代數 ring 不同的 例如 觀察

前言

線性代數中對於一段數字序列的排列情況有這樣一個定義:在一個排列中,如果一對數的前後位置與大小順序相反,即前面的數大於後面的數,那麽它們就稱為一個逆序一個排列中逆序的總數就稱為這個排列的逆序數。一個排列中所有逆序總數叫做這個排列的逆序數。也就是說,對於n個不同的元素,先規定各元素之間有一個標準次序(例如n個 不同的自然數,可規定從小到大為標準次序),於是在這n個元素的任一排列中,當某兩個元素的先後次序與標準次序不同時,就說有1個逆序。一個排列中所有逆序總數叫做這個排列的逆序數。(摘自 百度百科)

對於逆序數通俗的理解:對於序列中每個位置的的數,其之前比他的值大的個數之和,或者在其之後比他的值小的個數之和

,如此稱為逆序數


實現

實現手段:線段樹、樹狀數組、離散化、歸並排序、枚舉

  • 我們容易根據逆序數的理解寫出O(n^2)的模擬算法,是一個普通冒泡排序類似物:
int ans = 0;  //逆序數個數
int num[maxn];
for (int i = 1; i < n; i++) {
    for (int j = n - i; j > 0; j--) {
        if (num[j + 1] < num[j]) {
            swap(num[j + 1], num[j]);
            ans++;  
        }
    }
}
  • 如果當一段數字集中在某個範圍中,還可以利用hash的特性,復雜度O(n*num(max)),但這個仍然是個暴力算法:
#include <iostream>
using namespace std;
int  a[100005];//存儲有多少比它大的數字在它之前
int main()
{
    int n, m, i, j, k;
    cin >> n;
    long long int sum = 0;
    for (i = 1; i <= n; i++)
    {
        scanf("%d", &m);
        sum = sum + a[m];// 有多少比它大的數字在他之前,就要加上多少組
        for (j = 0; j<m; j++)//在它之前的數字都+1
        {
            a[j]++;
        }
    }
    cout << sum << endl;
}
  • 上面那步的思想其實就是每次將數字壓入數據集時,查詢比當前數字排名來得大得數字數量,這個數字就是當前下標數字得逆序數。但是這個寫法不足地方有兩點:1,數據分散時空間復雜度高;2,每次都要執行一個O(num)大小的前綴操作。我們這是就可以利用線段樹,樹狀數組的樹形結構將前綴操作以及查詢操作都均攤到O(logn)級別,從而提高效率。

    細節

    關於sum的含義是求得1~idx下標得前綴和,在這裏根據方法2得思路就是rank <= idx的前綴數量,這裏就要利用到一點容斥的思想:我們的目標是考慮當前有多少個比當前數排名大的數,當前全集為i,rank <= idx的數量為sum(idx),則當前 rank > idx 的數量為i-sum(idx)。其實c維護的就是rank的數量。舉例:{7,4,3,8,6}
    技術分享圖片

觀察C數組的變化

技術分享圖片

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;
int n,c[100010];
int lowbit(int x)
{
    return x&(-x);
}
void add(int k,int num)
{
    while(k<=n)
    {
        c[k]+=num;
        k+=lowbit(k);
    }
}

int query(int k)
{
    int sum=0;
    while(k)
    {
        sum+=c[k];
        k-=lowbit(k);
    }
    return sum;
}
typedef struct nodee
{
    int x,i;
}node;
node maze[100010];
bool cmp(node u,node v)
{
    if(u.x==v.x)
        return u.i>v.i;
    return u.x<v.x;
}
int b[100010];

int main(void)
{
    int i,j,x,y;

    while(~scanf("%d",&n))
    {
        ll sum = 0;
        memset(c,0,sizeof(c));
        for(i=1;i<=n;i++)
        {
            scanf("%d",&maze[i].x);
            maze[i].i = i;
        }
        sort(maze+1,maze+1+n,cmp);

        int cnt = 1;
        for(i=1;i<=n;i++){
            if(i!=1&&maze[i].x!=maze[i-1].x)
                cnt++;
            b[maze[i].i] = cnt;
        }

        for(i=1;i<=n;i++)
        {
            add(b[i],1);
            ll tmp = (i-query(b[i]));
            sum += tmp;
            cout << "\n逆序數 = " << tmp << "  排名 = " << b[i] << ‘\n‘;
            cout << "C數組:\n";
            for (int j = 1; j <= n; j++) {
                            cout << c[j] << " ";
                        }
        }
        printf("\n\n排列的逆序數為 = %lld\n",sum);
    }
    return 0;
}
  • 對於逆序數還有歸並排序的求法,註意歸並排序是穩定的排序算法,寫法有細節要註意。
#include <iostream>
#include <algorithm>

using namespace std;
const int maxn = (int)1e5+5;
typedef long long ll;
typedef double db;
typedef long double ldb;

int val[maxn],tmp[maxn];
ll cnt;

void Merge (int l, int m, int r) {
    int i = l;
    int j = m + 1;
    int k = l;
    //歸並排序為穩定排序,穩定的關鍵是mid後面那部分只有在小於前面的時候才往前提,相等不提!!!
    while (i <= m && j <= r) {
        if (val[i] > val[j]) {
            cnt += j-k; // 每當後段的數組元素提前時,記錄提前的距離
            tmp[k++] = val[j++];
        }else {
            tmp[k++] = val[i++];
        }
    }
    
    while (i <= m) {
        tmp[k++] = val[i++];
    }
    while (j <= r) {
        tmp[k++] = val[j++];
    }
    for (int i = l; i <= r; i++) {
        val[i] = tmp[i];
    }
}

void MergeSort(int l, int r) {
    if (l < r) {
        int mid = l + ((r - l) >> 1);
        MergeSort(l, mid);
        MergeSort(mid + 1, r);
        Merge(l, mid, r);
    }
    return ;
}


int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> val[i];
    }
    cnt = 0;
    MergeSort(1, n);
    cout << cnt << ‘\n‘;
    return 0;
}

逆序數介紹以及算法實現淺析