計蒜客 - 排序

計蒜客 排序

題目描述

你需要分析排序算法,將 nn 個互不相同的整數,通過交換兩個相鄰的元素使得數列有序的 最少交換次數。
比如,原數列爲: 9,1,0,5,49,1,0,5,4 排序後的數列爲: 0,1,4,5,90,1,4,5,9

樣例

輸入格式
第一行一個整數 n(n500000)n(n\leq500000)
接下來 nn 行,每行一個整數 ai(ai109)a_i(a_i\leq10^9)

5
9
1
0
5
4

輸出格式
輸出一個整數,表示操作次數。

6

算法與數據結構

樹狀數組
離散化

題解

這是一道用樹狀數組做的題,還要用到離散化的技巧。

一次有效的交換意味着什麼呢?

爲了使序列有序,一次有效的交換應該是後一個較小的數與他前一個較大的數交換,那麼單獨一個數字的交換次數,應該是這個數字前面比它大的數字的個數。

換句話說,當一個數字出現的時候,出現在它左邊且比它大的數字的個數,就是當出現到這個數字爲止(這個數字右邊的數字還沒出現)時,這個數字要到正確位置上交換的次數。

對每一個位置上的數字累加這樣的次數,就是答案。

更一般的,令初始 sum = 0,對於第 i 個數字,出現在它左邊且比它大的數字的個數如果是 x,就令 sum += x,遍歷所有的數字,最後得到的累加和,就是答案。

現在剩一個問題:怎麼知道出現在左邊的、比自己大的數字有多少?
對於遍歷到的第 i 個數字(記爲 x),如果在遇到它的時候,單點更新樹狀數組 change(x),改變的動作是加 1,即計數,即令 x 出現的次數加 1,那麼,對於 getSum(x) 操作,就是求出從 1 到 x 的數字一共出現了多少次,即,不小於 x 的數字有多少個。

當我們得知了這個結果以後,由於出現在 x 右邊的數字是還沒有遇到的,所以,不小於 x 的數字都出現在 x 的左邊,那就可以簡單地使用 i + 1 - getSum(x) 得到出現在 x 左邊、比自己大的數字的個數。

這個公式是顯然成立的:對於第 i 個數字,目前已經出現的所有數字一共是 i + 1 個,在這些數字裏,不小於 x 的有 getSum(x) 個,所以,大於 x 的有 i + 1 - getSum(x) 個。並且,這些數字要麼是 x 本身,要麼出現在 x 的左邊。

累加所有的 i + 1 - getSum(x),得到答案。

scanf("%d", &n);
for (int i = 0; i < n; i++) {
    scanf("%d", &x);
    change(x); // 更新樹狀數組,記下新的數 x
    ans += i + 1 - getSum(x);
    // getSum(x) 是已出現的(在 x 左邊的)小於等於 x 的數
    // x 在第 i + 1 位,它和它的左邊有 i + 1 個數
    // 所以它左邊比它大的數有 i + 1 - getSum(x) 個,有那麼多個逆序對,就需要交換那麼多次
}
printf("%d", ans);

但是,由於數字範圍實在是太大了,我們需要用到離散化的技巧,即將大範圍的數字映射到小範圍,因爲我們只關心數字之間值的大小關係,而不關心具體的數值。

for (int i = 0; i < n; i++) {
    scanf("%d", &x[i]);
    dis[i] = x[i]; // 讀入數據,複製一份以便離散化處理
}

對於 C++,可以使用 unique() 函數來去重,首先對 dis數組排序,然後去重,得到去重後的長度 length。在去重後的數組中,用二分查找 x[i] 所在的位置,並用這個位置作爲 x[i] 離散化後的值。

sort(dis, dis + n); // 對數據排序
int length = unique(dis, dis + n) - dis; // 利用 unique 去重,使大小與下標對應,並得到去重後的長度
for (int i = 0; i < n; i++) {
    int index = lower_bound(dis, dis + length, x[i]) - dis; // 在去重後的數組中,使用二分查找找到 x[i] 所在位置
    x[i] = index + 1; // 用這個位置作爲離散後的值,由於樹狀數組下標從 1 開始,因此加 1
}

之後的操作與之前類似,遍歷 x 數組,change(x[i]),並計算 ans += i + 1 - getSum(x[i])

最後要注意一下,anslong long 範圍的。

以及提一句,uniquelower_bound 返回值都是 long long int,直接降級到 int 是一種不好的做法。

完整代碼

#include <bits/stdc++.h>

using namespace std;

int n = 0;
const int MAX_N = 500007;
int C[MAX_N] = {0}; // 樹狀數組
int x[MAX_N] = {0}; // 記錄原始數據
int dis[MAX_N] = {0}; // 離散化數據
long long ans = 0;

int lowBit(int x) {
    return x & -x; // return x & (x ^ (x - 1))
}

int getSum(int x) {
    int res = 0;
    while (x != 0) {
        res += C[x];
        x -= lowBit(x);
    }
    return res;
}

void change(int x) {
    while (x <= MAX_N) {
        C[x]++;
        x += lowBit(x);
    }
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &x[i]);
        dis[i] = x[i]; // 讀入數據,複製一份以便離散化處理
    }
    sort(dis, dis + n); // 對數據排序
    int length = unique(dis, dis + n) - dis; // 利用 unique 去重,使大小與下標對應,並得到去重後的長度
    for (int i = 0; i < n; i++) {
        int index = lower_bound(dis, dis + length, x[i]) - dis; // 在去重後的數組中,使用二分查找找到 x[i] 所在位置
        x[i] = index + 1; // 用這個位置作爲離散後的值,由於樹狀數組下標從 1 開始,因此加 1
    }
    for (int i = 0; i < n; i++) {
        change(x[i]); // 更新樹狀數組,記下新的數 x
        ans += i + 1 - getSum(x[i]);
        // getSum(x) 是已出現的(在 x 左邊的)小於等於 x 的數
        // x 在第 i + 1 位,它和它的左邊有 i + 1 個數
        // 所以它左邊比它大的數有 i + 1 - getSum(x) 個,有那麼多個逆序對,就需要交換那麼多次
    }
    printf("%lld", ans);

    return 0;
}

歡迎關注我的個人博客以閱讀更多優秀文章:凝神長老和他的朋友們(https://www.jxtxzzw.com)

也歡迎關注我的其他平臺:知乎( https://s.zzw.ink/zhihu )、知乎專欄( https://s.zzw.ink/zhuanlan )、嗶哩嗶哩( https://s.zzw.ink/blbl )、微信公衆號( 凝神長老和他的朋友們 )
凝神長老的二維碼們

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章