二分歸併 排序 求逆序對
題目: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]> 這個
例如,陣列(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));