1. 程式人生 > >二分歸併 排序 求逆序對

二分歸併 排序 求逆序對

題目:The Number of Inversions

題面:

For a given sequence , the number of pairs  where  and , is called the number of inversions. The number of inversions is equal to the number of swaps of Bubble Sort defined in the following program:

bubbleSort(A)
  cnt = 0 // the number of inversions
  for i = 0 to A.length-1
    for j = A.length-1 downto i+1
      if A[j] < A[j-1]
	swap(A[j], A[j-1])
	cnt++

  return cnt

For the given sequence , print the number of inversions of . Note that you should not use the above program, which brings Time Limit Exceeded.

Input

In the first line, an integer , the number of elements in , is given. In the second line, the elements  () are given separated by space characters.

output

Print the number of inversions in a line.

Constraints

  •  
  •  
  •  are all different

Sample Input 1

5
3 5 2 1 4

Sample Output 1

6

Sample Input 2

3
3 1 2

Sample Output 2

2

題解:

遞歸回溯是個奇妙的東西,幾乎很多難題都要用到遞迴和回溯,能解出更多的解法,也能將所有情況都考慮到

逆序對 ,如果存在正整數 i, j 使得 1 ≤ i < j ≤ n 而且 A[i] > A[j],則 <A[i], A[j]> 這個

有序對稱為 A 的一個逆序對,也稱作逆序數。

例如,陣列(3,1,4,5,2)的逆序對有(3,1),(3,2),(4,2),(5,2),共4個,資料在陣列中的位置是左邊,但是數值比右邊的某些資料大,這兩個就組成 了一個 逆序對。

求解 逆序對方法:

1.最原始的 冒泡法

#include<stdio.h>
#include<iostream>
#include<string.h>
using namespace std;
typedef long long ll;
#include<stdio.h>
int b[10];
int main()
{
	int num,i,j,k=0;
	scanf("%d",&num);
	for(i=0;i<num;i++)
		scanf("%d",&b[i]);
	for(i=0;i<num-1;i++)
		for(j=0;j<num-1-i;j++)
			{
				if(b[j]>b[j+1])
				{
				swap(b[j],b[j+1]);
					k++;
				}
			}
	printf("%d",k);
	
	return 0;
}

時間複雜度 是O(n^2) 資料大的時候,就會超時。這個方法是很容易理解的,我就不做解釋了,c語言的書上有相關的解釋,這個還是很簡單的,但不是最優的解法。

2.二分並歸排序 求解逆序對

我一開始也是眾多迷茫讀者中的一個,因為網上的題解都是些圖解(不是圖解不好),但是對於ACM新手來說,由圖解就上手敲程式碼,還是很困難的(要經過很長的時間和大量的題的磨練才能夠到達的)。

首先它是遞歸回溯與二分的一個結合,任何其中的一個都是比較難實現的,這裡我們先將 遞歸回溯 和 二分 分開來考慮,因為這裡面有很多的狀態跳轉,很不好理解。

第一先知道二分是怎麼分的,看圖解

https://visualgo.net/zh 這是一個 模擬排序的動畫,大家可以看看,應該是很有幫助的。分割槽間(是由遞迴)和 合併區間(是由一個輔助陣列來實現的)。

程式碼實現有些註釋已經附上:

#include<stdio.h>
#include<iostream>
#include<string.h>
using namespace std;
typedef long long ll;
const ll maxn=200005;
ll a[maxn],b[maxn];
ll count;

void merge_sort1(ll x,ll mid,ll y)
{
	//以下 是 將左右部分比較,誰小 就將誰裝進b陣列(輔助陣列),這樣就保證b中 是有序的。  
	ll i=x,j=mid+1,k=x;
		while(i<=mid&&j<=y)
		{
			if(a[i]<=a[j])
				b[k++]=a[i++];       //如果左邊區間的某個元素小於右半邊的元素,就不用考慮逆序對,因為這不是逆序對,就將左邊區間元素下標+1,再比。 
			else
				{
					b[k++]=a[j++];
					count+=mid-i+1;   //在比較兩邊的區間 元素時,mid 兩邊的 區間 全是有序的,mid - i + 1 就是 中間 mid點(也包括mid 處點的元素) 到比較的左點 之間有幾個元素,因為左邊(右邊)全是有序的,之間有幾個元素,那就有幾個比右半邊元素大的逆序對。然後右半邊的元素下標+1,再比. 
				}	
		}
		
		
		
	// 以下的部分 就是 兩邊的東西誰比較大,就將大的裝進b陣列 (輔助陣列)。 
	while(i<=mid)						
		b[k++]=a[i++];
	while(j<=y)
		b[k++]=a[j++];	
	// 再把b陣列 中的東西  從x 到 y b陣列中已經有序的東西,複製進 a 陣列。	
	for(i=x;i<=y;i++)
		a[i]=b[i];
			
			
			
			
	//經過以上兩個 部分 ,就將 從 x 到 y 的部分 排好序了。 
}


void merge_sort(ll x,ll y)
{
	if(x==y)              //遞迴分割槽間,結束標誌
		return ;
	ll mid=(x+y)/2;       //每次取半 
	merge_sort(x,mid);    // 先排左邊(不是一開始到這裡就排,而是要回溯時,用 merge_sort1(x,mid,y)去排) ,這裡是可以分割槽間,分出左邊以兩個為一組的區間。 
	merge_sort(mid+1,y);  // 再排右邊(不是一開始到這裡就排,而是要回溯時,用 merge_sort1(x,mid,y)去排) ,這裡也是分割槽間的,分出右邊以兩個為一組的區間。 
	merge_sort1(x,mid,y); // 這部好了的話,就是(左或者右的一半) 已經排好了,進行下一半的排序。 
	
	//遞迴: 先遞迴找到最小的區間(有兩個組成的)。
	//回溯: 由小的區間 到大的區間,去排左右兩邊的區間 裡的元素。 
	
}


int main()
{
	ll num;
	ll i;
	scanf("%lld",&num);
	for(i=0;i<num;i++)
		scanf("%lld",&a[i]);
	merge_sort(0,num-1); 
	printf("%lld\n",count);
	return 0;
}

如何分割槽間,是個重要的部分,這才能保證,怎麼說呢,你才不會有BUG,不會出錯。

看了上面的 程式碼,我們先來 弄懂 它是怎麼用程式碼實現 分割槽間的。

mid=(x+y)/2 , 這個x(一個區間的起始) 和y(一個區間的結束)和 mid  都是陣列中的下標,不是陣列的長度,就如 5 4 1 3 起始x位置 0 ,y位置 3 , mid = (x+y)/2=1 分 區間, mid=1處 , x=0 , y=3; 這時想想,什麼時候還能再分?,當x 和 mid 相等的時候,就相當於x位置處的資料和mid 位置處的資料相同,這時,就不能再分出來,這其實上就用了遞迴了。 我們已經分好左半邊的了,如何分右邊的?方法一樣 用 x=mid+1 , 和 y=區間的結束位置。不斷mid=(x+y)/2,當 mid 與 x 相等的時候,就不能再分割槽間了。

分完區間後 ,就要考慮到如何排序,通過比較來找逆序對,這個過程 是包含在分割槽間 中的,它是一邊分割槽間,一邊比較排序的,不是一個個獨立的過程,遞迴分割槽間,回溯時,就可以去排序,找逆序對。

下面看看如何來 求逆序對和排序。

先附上圖解:

這個圖解是每次回溯是都會這樣排。下面 看看程式碼實現

void merge_sort1(ll x,ll mid,ll y)
{
	//以下 是 將左右部分比較,誰小 就將誰裝進b陣列(輔助陣列),這樣就保證b中 是有序的。  
	ll i=x,j=mid+1,k=x;
		while(i<=mid&&j<=y)
		{
			if(a[i]<=a[j])
				b[k++]=a[i++];       //如果左邊區間的某個元素小於右半邊的元素,就不用考慮逆序對,因為這不是逆序對,就將左邊區間元素下標+1,再比。 
			else
				{
					b[k++]=a[j++];
					count+=mid-i+1;   //在比較兩邊的區間 元素時,mid 兩邊的 區間 全是有序的,mid - i + 1 就是 中間 mid點(也包括mid 處點的元素) 到比較的左點 之間有幾個元素,因為左邊(右邊)全是有序的,之間有幾個元素,那就有幾個比右半邊元素大的逆序對。然後右半邊的元素下標+1,再比. 
				}	
		}
		
		
		
	// 以下的部分 就是 兩邊的東西誰比較大,就將大的裝進b陣列 (輔助陣列)。 
	while(i<=mid)						
		b[k++]=a[i++];
	while(j<=y)
		b[k++]=a[j++];	
	// 再把b陣列 中的東西  從x 到 y b陣列中已經有序的東西,複製進 a 陣列。	
	for(i=x;i<=y;i++)
		a[i]=b[i];
			
			
			
			
	//經過以上兩個 部分 ,就將 從 x 到 mid+1 的部分 排好序了。 
}

在這裡,我們需要一個輔助陣列來輔助,每次排好,都需要往原來的數組裡複製一下,在這裡,我們需要很多的下標變數來往輔助數組裡面裝東西,由上圖可知,我們需要分別比較兩邊區間(都是已經排好序的)各個位資料的大小,則就要三個 i , j , k ,來分別匹配左半邊區間的開始位置,右半邊區間的開始位置,左區間的開始位置,如果左半邊的元素小於右半邊的對應元素,則就把左半邊這個元素裝進輔助陣列中,否則就把右半邊的這個元素裝進輔助元素中,當裝的時候,相應的下表變數也會變化,當i和j都小於相應的(mid)和(y)時,就繼續比較來排序,當其中一個不滿足時,就不在比了(因為都是有序的)。這時輔助陣列中的值都是有序的,並且都是兩邊區間比較小的,兩邊區間有一個的i或者j已經大於mid或者y。這時該往輔助數組裡面排剩下區間的元素,如果i<=mid 就把左半邊區間(剩下的)的元素裝到輔助數組裡面(裝的時候輔助陣列肯定是有序的),如果j<=y(又區間的結束)時,就裝它剩下的元素,為什麼就能精確的裝那個資料呢?因為有下標變數i,j,k(是輔助陣列的下標)來控制。

這樣就能使輔助陣列中是有序的,然後再將輔助陣列的有序元素,一個一個都裝到原來陣列中,這樣就保證有序了。

以上的過程都是每次回溯時要做的,每次排的時候都會上演一次,不只是最後一次排的。

先用遞迴分割槽間在這之後用一個合併的函式,而這就是用遞迴一步一步分和分和實現的。

接下來,怎麼來求逆序對,上面已經有了定義了,逆序對就是下標與資料大小不同步,看一下,什麼時候有逆序對

while(i<=mid&&j<=y)
		{
			if(a[i]<=a[j])
				b[k++]=a[i++];      
			else
				{
					b[k++]=a[j++];
					count+=mid-i+1;   
				}	
		}

當正常比後面區間各個資料小的時候,是沒有逆序對的,當比後面區間大的時候,這時就會有了逆序對,而計數器count=mid-i+1就是這時候的逆序對的數量,由上面說的,這個 mid 就是 在陣列中的下標,mid - i+1 就是 mid 到 i的距離 ,這個距離 就是 此時的逆序對的個數,因為 這個 距離 就是 mid 與 i 之間 的 左區間元素的個數 ,因為 左區間是遞增 的 ,所以 x 到 mid 之間所有的點 的個數 就是 逆序對的個數,當然 下一個 狀態 還是可能會有逆序對的 ,每一次和回溯 ,都會 經過這個過程。

如果還有什麼 不懂的話,可以去VS 上除錯幾遍,弄懂。

這個時間複雜度是O(nlog^(n));