計蒜客 排序
題目描述
你需要分析排序算法,將 個互不相同的整數,通過交換兩個相鄰的元素使得數列有序的 最少交換次數。
比如,原數列爲: 排序後的數列爲: 。
樣例
輸入格式
第一行一個整數 。
接下來 行,每行一個整數 。
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])
。
最後要注意一下,ans
是 long long
範圍的。
以及提一句,unique
和 lower_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 )、微信公衆號( 凝神長老和他的朋友們 )