歸併排序之神奇的逆序對
阿新 • • 發佈:2018-12-18
文章包含四部分
-
歸併排序的實現(略講)
-
歸併排序求逆序對
-
歸併排序例題詳講
-
樹狀陣列和歸併排序求逆序對的區別
-
關於歸併排序的實現
歸併排序主要分為兩大步:
1.分解
2.合併
關於歸併排序的具體原理請自行百度. 這裡只略講一下歸併排序的程式碼實現方式
-
分解
我們採用二分遞迴的方式處理
-
合併
我們採用三個指標來實現
板子題:快速排序
Code:
#include<iostream>
#include<cstdio>
using namespace std;
const int N=100000+5;
int n;
int a[ N];//需要排序的陣列
int r[N];//用來輔助排序的陣列
void msort(int s,int t)
{
if(s==t) return;//如果只有一個數就返回
int mid=(s+t)/2;
//遞迴分解
msort(s,mid);//分解左邊
msort(mid+1,t);//分解右邊
int i=s,j=mid+1,k=s;
//合併左右序列
while(i<=mid && j<=t) //正常排序合併
{
if(a[i]<=a[j]) {r[k]=a[i];k++ ;i++;}
else {r[k]=a[j];k++;j++;}
}
while(i<=mid){r[k]=a[i];k++;i++;}//複製排序後左邊子序列剩餘
while(j<=t){r[k]=a[j];k++;j++;}//複製排序後右邊子序列剩餘
for(int i=s;i<=t;i++)
a[i]=r[i];//更新a陣列
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf ("%d",&a[i]);
msort(1,n);
for(int i=1;i<=n;i++)
printf("%d ",a[i]);
return 0;
}
測試結果:
第一個是裡的 第二個是開了的 第三個是手寫的歸併排序
不能開
-
關於歸併排序求逆序對
模板題
關於逆序的定義:大的數排在小的數前面
當然你也可以用氣泡排序
實現方法:
每次合併的時候,記錄有多少次交換
假定我們要將一串無序數字排成升序(從小到大) 根據歸併排序的原理,每次我們進行合併操作的時候,左邊子序列中小的數都會被合併進輔助陣列。 這時如果左邊子序列有數剩餘,則說明這些數都比右邊子序列的數要大,那麼我們可以根據這一點來計算逆序對的個數
核心Code:
while(i<=mid && j<=t)
{
if(a[i]<=a[j]) {r[k]=a[i];k++;i++;}
else {r[k]=a[j];k++;j++;ans+=mid-i+1;}//核心操作
//mid-i+1即左邊子序列剩餘元素的個數
}
其餘部分沒有什麼差別
模板題Code:
#include<iostream>
#include<cstdio>
using namespace std;
const int N=5e5+5;
long long n,ans;
int a[N];
int r[N];
void msort(int s,int t)
{
if(s==t) return;
int mid=(s+t)/2;
msort(s,mid);
msort(mid+1,t);
int i=s,j=mid+1,k=s;
while(i<=mid && j<=t)
{
if(a[i]<=a[j]) {r[k]=a[i];k++;i++;}
else {r[k]=a[j];k++;j++;ans+=mid-i+1;}//核心操作
//mid-i+1即左邊子序列剩餘元素的個數
}
while(i<=mid){r[k]=a[i];k++;i++;}
while(j<=t){r[k]=a[j];k++;j++;}
for(int i=s;i<=t;i++)
a[i]=r[i];
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
msort(1,n);
printf("%lld",ans);
return 0;
}
-
歸併排序例題詳講
關鍵詞:
交換序列中相鄰的兩個元素
用最少的交換次數使原序列變成不下降序列
顯然在求逆序對的個數
套上板子即可
瑞士輪
這裡只用到了歸併排序中的合併操作
分析:
每組比賽的勝者:賽前,總分是按降序排的;獲勝後都得1分,仍是降序;
每組比賽的負者:賽前,總分是按降序排的;不得分,仍是降序。
先按初始分數排序,然後按分數高低兩人一組比賽;
勝者入隊,負者入隊。這樣、自身仍是有序的;
只需進行合併操作即可,合併操作的複雜度是,而如果用快排其複雜度為
Code:
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1e5+5;
int n,r,q;
struct fengzi
{
int id,sorce,power;
}m[2*N],x[2*N],y[2*N];
//m是選手,x是勝利者,y是失敗者
bool cmp(fengzi a,fengzi b)
{
if(a.sorce>b.sorce) return 1;//依題意從高到低排序
if(a.sorce==b.sorce && a.id<b.id) return 1;//處理特殊情況
return 0;
}
//合併操作
void merge()
{
int i=1,j=1,cnt=0;
while(cnt<=2*n)
{
if(i>n)
{
while(j<=n){m[++cnt]=y[j];j++;}
break;
}
if(j>n)
{
while(i<=n){m[++cnt]=y[i];i++;}
break;
}
if(x[i].sorce>y[j].sorce || (x[i].sorce==y[j].sorce && x[i].id<y[j].id))//依題意更新排名
m[++cnt]=x[i++];
else m[++cnt]=y[j++];
}
}
int main()
{
scanf("%d%d%d",&n,&r,&q);
for(int i=1;i<=2*n;i++)
{
scanf("%d",&m[i].sorce);//得分
m[i].id=i;//編號
}
for(int i=1;i<=2*n;i++)
scanf("%d",&m[i].power);//實力值
sort(m+1,m+1+2*n,cmp);//排序
for(int qwq=1;qwq<=r;qwq++)//列舉每場比賽
{
int i=0,j=0;
for(int ppp=1;ppp<=n;ppp++)//列舉每組選手
{
int pos=2*ppp;
if(m[pos-1].power>m[pos].power)//如果該組中的第一個選手比第二個選手強
{
m[pos-1].sorce++;//第一個選手的分數+1
x[++i]=m[pos-1];//第一個選手分到勝利者的隊伍
y[++j]=m[pos];//第二個選手分到失敗者的隊伍
}
else
{
m[pos].sorce++;
x[++i]=m[pos];
y[++j]=m[pos-1];
}
}
merge();//合併勝利者和失敗者,更新 新的排名
}
printf("%d",m[q].id);
return 0;
}
火柴排隊
分析:
由數學知識可知,要使$ \sum (a_i-b_i)^2a_i-b_i$最小
也就是說,中第小的數要和中第小的數排在一個位置
依據這個思想,我們可以新建一個數組 這個陣列的下標是陣列中第小的數在原序列中的編號 這個陣列中存的是陣列中第小的數在原序列中的編號
例如:
的原始序列為 的原始序列為 把兩個序列從小到大排序後, 序列為 序列為 我們假定此時的為3 序列中第小的數的原始編號就是 序列中第小的數的原始編號就是
那
顯然,這個陣列的特性就是,如果那麼此時,序列中第k小的數就和序列中的第小的數,一一對應了,即原始序列時,就有最小。
然後,我們的目標是,將陣列升序排列,求出交換的次數,這不就轉換成求逆序對了…
Code:
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int mod=99999997;
const int N=100000+5;
int n,ans,cnt;
struct team
{
int id,hight;//id是原始編號,hight是高度
}a[N],b[N];
int x[N],y[N];
//x待排序的陣列
//y是輔助陣列
bool cmp(team a,team b)
{
return a.hight<b.hight;//先按高度排序
}
void merge(int l,int r)
{
if(l