2014藍橋杯 小朋友排隊(歸併排序 和 數狀數組 求逆序對)

標題:小朋友排隊

n 個小朋友站成一排。現在要把他們按身高從低到高的順序排列,
但是每次只能交換位置相鄰的兩個小朋友。
每個小朋友都有一個不高興的程度。開始的時候,所有小朋友的不高興程度都是0。
如果某個小朋友第一次被要求交換,則他的不高興程度增加1,如果第二次要求他交換,則他的不高興程度增加2(即不高興程度爲3),依次類推。當要求某個小朋友第k次交換時,他的不高興程度增加k。
請問,要讓所有小朋友按從低到高排隊,他們的不高興程度之和最小是多少。
如果有兩個小朋友身高一樣,則他們誰站在誰前面是沒有關係的。

【數據格式】
輸入的第一行包含一個整數n,表示小朋友的個數。
第二行包含 n 個整數 H1 H2 … Hn,分別表示每個小朋友的身高。
輸出一行,包含一個整數,表示小朋友的不高興程度和的最小值。
例如,輸入:
3
3 2 1
程序應該輸出:
9
【樣例說明】
首先交換身高爲3和2的小朋友,再交換身高爲3和1的小朋友,再交換身高爲2和1的小朋友,每個小朋友的不高興程度都是3,總和爲9。

【數據規模與約定】
對於10%的數據, 1<=n<=10;
對於30%的數據, 1<=n<=1000;
對於50%的數據, 1<=n<=10000;
對於100%的數據,1<=n<=100000,0<=Hi<=1000000。

資源約定:
峯值內存消耗 < 256M
CPU消耗 < 1000ms

歸併排序可以求總的逆序對,這個大家應該是知道的
他人 歸併排序參考
此題所求並非是總的逆序對, 而是相對的。

解題思想:

如果有如下序列:
3 6 4 7 5 2 0 1
那麼對於1來說,要想回到前面, 必須要經過 2 5 7 4 6 3 次序並不重要, 重要的是個數。
那麼對於2來說,要被0 1經過。

那麼總結一下就是:
就是相對於 2 來說有幾個逆序對 即: 小於2並且在2右邊的,大於2並且在其左邊的, 取之和即爲所求。

接下來一張草圖,來解釋歸併求逆序對的過程。
在這裏插入圖片描述
歸併排序求逆序對代碼如下:
去掉含有 下面兩個變量的代碼所在行, 就是歸併排序 ( ni_num_sum,ni_num_sum_temp )

#define MAX 1000005
// 原數組 
long num[MAX] = {0};
// 輔助排序 
long num_sort[MAX] = {0};

// 去掉含有 下面兩個變量的代碼所在行, 就是歸併排序
// 記錄左右兩邊的逆序 個數 
long ni_num_sum[MAX] = {0}; 
// 輔助記錄 
long ni_num_sum_temp[MAX] = {0};
void pai_sort(int l, int mid, int r){
	int i = l;
	int j = mid + 1;
	int index = l;
	while(i <= mid && j <= r){
		if(num[i] < num[j]){	// 後面大於前面 
			num_sort[index] = num[i]; 
			ni_num_sum[index] = ni_num_sum_temp[i] + (j - mid - 1); // 逆序數 
			++i; 
		}else if(num[i] > num[j]){ // 前面 大於 後面  出現逆序 
			num_sort[index] = num[j]; 
			ni_num_sum[index] = ni_num_sum_temp[j] + (mid - i + 1);	// 逆序數 
			++j; 
		} else{// 等於, 不需要加逆序數 
			num_sort[index] = num[i]; 
			++i; 
			num_sort[index] = num[j]; 
			++j; 
		}
		++index;
	}
	while(i <= mid){
		if(num[i] != num_sort[index])	// 如果出現相等的情況, 就不要計算
			ni_num_sum[index] = ni_num_sum_temp[i] + (j - mid - 1);
		num_sort[index] = num[i];
		++index;
		++i;
	}
	while(j <= r){ 
		if(num[i] != num_sort[index])	// 如果出現相等的情況, 就不要計算
			ni_num_sum[index] = ni_num_sum_temp[j] + (mid - i + 1);
		num_sort[index] = num[j];
		++index;
		++j;
	}
	// 排序完成 放回原數組, 並且將逆序結果緩存等待取用。
	for(i = l; i <= r; ++i){
		num[i] = num_sort[i];
		ni_num_sum_temp[i] = ni_num_sum[i]; // 緩存上一次的逆序對數 
	}
}
void gui_pai(int l, int r){
	if(l < r){
		int mid = (l + r) >> 1;
		gui_pai(l, mid);
		gui_pai(mid + 1, r);
		pai_sort(l, mid, r);	
	}
} 

那麼求出逆序對之後列 ?

之後便是轉換成不滿意程度了。

也好解決:
比如 2 有 7 個逆序對,那麼就是 1 + 2 + 3 + 4 + 5 + 6 + 7 也就是n - 1求和
對於 n 到 1的求和 求和公式爲 **( ( (n + 1) * n) / 2 ) ** 。
代碼如下:

// 1 - n  連續數  求和 
long sum_0_to_n(long n){
	return ((n + 1) * n )>> 1;
} 

完整代碼:

#include<stdio.h>
#define MAX 1000005
// 時間複雜度 O( N*log2(N) ) 
long N = 0;

// 原數組 
long num[MAX] = {0};
// 輔助排序 
long num_sort[MAX] = {0};

// 記錄左右兩邊的逆序 個數 
long ni_num_sum[MAX] = {0}; 
// 輔助記錄 
long ni_num_sum_temp[MAX] = {0};

unsigned long long ans = 0;
// 1 - n  連續數  求和 
long sum_0_to_n(long n){
	return ((n + 1) * n )>> 1;
} 
void pai_sort(int l, int mid, int r){
	int i = l;
	int j = mid + 1;
	int index = l;
	while(i <= mid && j <= r){
		if(num[i] < num[j]){	// 後面大於前面 
			num_sort[index] = num[i]; 
			
			ni_num_sum[index] = ni_num_sum_temp[i] + (j - mid - 1); // 逆序數 
			++i; 
		}else if(num[i] > num[j]){ // 前面 大於 後面  出現逆序 
			num_sort[index] = num[j]; 
			ni_num_sum[index] = ni_num_sum_temp[j] + (mid - i + 1);	// 逆序數 
			++j; 
		} else{// 等於, 不需要加逆序數 
			num_sort[index] = num[i]; 
			++i; 
			num_sort[index] = num[j]; 
			++j; 
		}
		index++;
	}
	while(i <= mid){
		if(num[i] != num_sort[index]) 
			ni_num_sum[index] = ni_num_sum_temp[i] + (j - mid - 1);
		num_sort[index] = num[i];
		++index;
		++i;
	}
	while(j <= r){ 
		if(num[i] != num_sort[index])
			ni_num_sum[index] = ni_num_sum_temp[j] + (mid - i + 1);
		num_sort[index] = num[j];
		++index;
		++j;
	}
	// 放回 
	for(i = l; i <= r; i++){
		num[i] = num_sort[i];
		ni_num_sum_temp[i] = ni_num_sum[i]; // 緩存上一次的逆序對數 
	}
}
void gui_pai(int l, int r){
	if(l < r){
		int mid = (l + r) >> 1;
		gui_pai(l, mid);
		gui_pai(mid + 1, r);
		pai_sort(l, mid, r);	
	}
} 
int main(){
	scanf("%d", &N);
	for(int i = 0; i < N; i++){
		scanf("%d", num + i);
	} 
	gui_pai(0, N - 1);
	// 使用逆序對 計算結果
	for(int i = 0; i < N; ++i){
		ans += sum_0_to_n(ni_num_sum[i]);
	} 
	printf("%lld\n", ans);
	for(int i = 0; i < N; i++){
		printf("%d ", ni_num_sum[i]);
	}
	return 0;	
}

解題思想二 數狀數組:

同樣是求逆序對, 但是求解方式不同。
同樣一張草圖, 表示解題過程。

在這裏插入圖片描述

#include<stdio.h>
#define N_MAX 100005
// 時間複雜度 O( N*log2(N) ) 

long N = 0;
// 原數組 
long num[N_MAX] = {0};
// 這裏需要使用數字的最大值存放數據, 因爲這相等於揹包 
#define MAX 1000005 
// 樹狀數組解法
// 需要兩個數組 
long tree_num_left[MAX];
long tree_num_right[MAX];
// 存放求和結果 
long ans_num_left[MAX];
long ans_num_right[MAX];
// 存放最大方便確定掃描區間 減少複雜度 
long max_num = -999999;
inline int low_bit(int t){
	return t&(-t);
} 
// 求和 
inline long tree_sum(long tree_num[], long index){
	long sum = 0;
	for(long i= index; i > 0; i -=low_bit(i)){
		sum +=  tree_num[i];
	}
	return sum;
} 
// 更新單節點
inline void update(long tree_num[], int index, long num){
	for(int i = index; i <= max_num; i += low_bit(i)){
		tree_num[i] += num;
	}
}
int main(){
	
	scanf("%d", &N);
	for(int i = 0; i < N; i++){
		scanf("%d", num + i);
		if(num[i] > max_num){
			max_num = num[i];
		} 
	} 
	max_num ++; // 因爲會出現0的情況, 將最大值加1就可以, 相當於向右偏移1, 當然 多加也無所謂 
	
	// 樹狀數組解法 
	
	// 更新 由左向右掃描 
	for(int i = 0; i < N; ++i){
		update(tree_num_left, num[i] + 1, 1); 
		// 若是 替換成 ans_num_right[num[i]] 數據將變得有序 , 但是後續合併和計算複雜度 最差 將會變高10倍 
		// 而且並不需要有序, 只需要結果就好 
		ans_num_left[i] = tree_sum(tree_num_left, max_num) - tree_sum(tree_num_left, num[i] + 1); 
	}
	// 更新 由右向左掃描 
	for(int i = N - 1; i >= 0; --i){
		update(tree_num_right, num[i] + 1, 1); 
		ans_num_right[i] = tree_sum(tree_num_right, num[i]); 
	}
	// 合併 
	unsigned long long ans_2 = 0;
	for(int i = 0; i < N; ++i){
		ans_num_right[i] += ans_num_left[i]; 
		// 求值
		ans_2 += sum_0_to_n(ans_num_right[i]);
		printf("%ld ", ans_num_right[i]);
	} 
	puts("");
	printf("樹狀數組求得:%lld\n", ans_2);
	
	return 0;	
}

運行截圖:

歸併排序 和 數狀數組全部代碼

#include<stdio.h>
#define N_MAX 100005
// 時間複雜度 O( N*log2(N) ) 

long N = 0;
// 原數組 
long num[N_MAX] = {0};
// 輔助排序 
long num_sort[N_MAX] = {0};

// 記錄左右兩邊的逆序 個數 
long ni_num_sum[N_MAX] = {0}; 
// 輔助記錄 
long ni_num_sum_temp[N_MAX] = {0};



unsigned long long ans = 0;

// 1 - n  連續數  求和 
long sum_0_to_n(long n){
	return ((n + 1) * n )>> 1;
} 

void pai_sort(int l, int mid, int r){
	int i = l;
	int j = mid + 1;
	int index = l;

	while(i <= mid && j <= r){
		if(num[i] < num[j]){	// 後面大於前面 
			num_sort[index] = num[i]; 
			
			ni_num_sum[index] = ni_num_sum_temp[i] + (j - mid - 1); // 逆序數 
			++i; 
		}else if(num[i] > num[j]){ // 前面 大於 後面  出現逆序 
			num_sort[index] = num[j]; 
			
			ni_num_sum[index] = ni_num_sum_temp[j] + (mid - i + 1);	// 逆序數 
			++j; 
		} else{// 等於, 不需要加逆序數 
			num_sort[index] = num[i]; 
			++i; 
			num_sort[index] = num[j]; 
			++j; 
		}
		index++;
	}
	while(i <= mid){
		
		if(num[i] != num_sort[index]) 
			ni_num_sum[index] = ni_num_sum_temp[i] + (j - mid - 1);
		num_sort[index] = num[i];
		++index;
		++i;
	}
	while(j <= r){ 
		if(num[i] != num_sort[index])
			ni_num_sum[index] = ni_num_sum_temp[j] + (mid - i + 1);
		num_sort[index] = num[j];
		++index;
		++j;
	}
	// 放回 
	for(i = l; i <= r; i++){
		num[i] = num_sort[i];
		ni_num_sum_temp[i] = ni_num_sum[i]; // 緩存上一次的逆序對數 
	}
 
}
void gui_pai(int l, int r){
	
	if(l < r){
		int mid = (l + r) >> 1;
		gui_pai(l, mid);
		gui_pai(mid + 1, r);
		
		pai_sort(l, mid, r);	
	}
	
} 


// 這裏需要使用數字的最大值存放數據, 因爲這相等於揹包 
#define MAX 1000005 
// 樹狀數組解法
// 需要兩個數組 
long tree_num_left[MAX];
long tree_num_right[MAX];
// 存放求和結果 
long ans_num_left[MAX];
long ans_num_right[MAX];
// 存放最大方便確定掃描區間 減少複雜度 
long max_num = -999999;
inline int low_bit(int t){
	return t&(-t);
} 
// 求和 
inline long tree_sum(long tree_num[], long index){
	long sum = 0;
	for(long i= index; i > 0; i -=low_bit(i)){
		sum +=  tree_num[i];
	}
	return sum;
} 
// 更新單節點
inline void update(long tree_num[], int index, long num){
	for(int i = index; i <= max_num; i += low_bit(i)){
		tree_num[i] += num;
	}
}

int main(){
	
	scanf("%d", &N);
	for(int i = 0; i < N; i++){
		scanf("%d", num + i);
		if(num[i] > max_num){
			max_num = num[i];
		} 
	} 
	max_num ++; // 因爲會出現0的情況, 將最大值加1就可以, 相當於向右偏移1, 當然 多加也無所謂 
	
	// 樹狀數組解法 
	
	// 更新 由左向右掃描 
	for(int i = 0; i < N; ++i){
		update(tree_num_left, num[i] + 1, 1); 
		// 若是 替換成 ans_num_right[num[i]] 數據將變得有序 , 但是後續合併和計算複雜度 最差 將會變高10倍 
		// 而且並不需要有序, 只需要結果就好 
		ans_num_left[i] = tree_sum(tree_num_left, max_num) - tree_sum(tree_num_left, num[i] + 1); 
	}
	// 更新 由右向左掃描 
	for(int i = N - 1; i >= 0; --i){
		update(tree_num_right, num[i] + 1, 1); 
		ans_num_right[i] = tree_sum(tree_num_right, num[i]); 
	}
	// 合併 
	unsigned long long ans_2 = 0;
	for(int i = 0; i < N; ++i){
		ans_num_right[i] += ans_num_left[i]; 
		// 求值
		ans_2 += sum_0_to_n(ans_num_right[i]);
		printf("%ld ", ans_num_right[i]);
	} 
	puts("");
	printf("樹狀數組求得:%lld\n", ans_2);
	 
	
	gui_pai(0, N - 1);
	// 使用逆序對 計算結果
	for(int i = 0; i < N; ++i){
		ans += sum_0_to_n(ni_num_sum[i]);
	} 
	for(int i = 0; i < N; ++i){
		printf("%ld ", ni_num_sum[i]);
	} 
	puts("");
	printf("歸併排序求得: %lld\n", ans);
	
	
	
		
	
	return 0;	
}



運行截圖:
在這裏插入圖片描述

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