《算法竞赛进阶指南》0x05排序


在程序设计中通常会用到以下排序:
1.选择排序、插入排序、冒泡排序
2.堆排序、归并排序、快速排序
3.计数排序、基数排序、桶排序

前两类排序时基于比较的排序,第一类排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2),第二类排序的时间复杂度为 O ( n l o n g n ) 。 O(nlongn)。 O(nlongn)第三类排序不直接比较大小,而是对被排序的数值按照位划分,分类映射等处理方式,其时间复杂度不仅与n有关,还与数值的大小范围m有关。

离散化

假设问题涉及int范围内的n个整数a[1]-a[n],这n个整数可能重复,去重后共有m个。我们要把每个整数用1-m中的整数代替,并且保证大小顺序不变。

void discrete()
{
	sort(a+1,a+n+1);
	for(int i=1;i<=n;i++)
	{
		if(i==1||a[i]!=a[i-1])
		b[++m]=a[i];
	}
}
int query(int x)
{
	return lower_bound(b+1.b+m+1,x)-b;
}

例题
acwing103.电影

将2*m+n个可能涉及到的语言存到数组中,并进行离散化处理。对于每一种语言对应观众的数量进行计数。挨个看每一个电影对应的高兴和比较高兴的观众的数量,选择出最优的。

#include<iostream>
#include<algorithm>
using namespace std;
#define MAX_N 200000
int all[MAX_N*3+5];
int a[MAX_N+5];
int b1[MAX_N+5],b2[MAX_N+5];
int l[MAX_N*3+5];
int cnt[MAX_N+5];
int score1[MAX_N+5],score2[MAX_N+5];
int r=0,q=0;
int n,m;
int query(int x)
{
    return lower_bound(l+1,l+q+1,x)-l;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",a+i);
        all[++r]=a[i];
    }
    cin>>m;
    for(int i=1;i<=m;i++)
    {
        scanf("%d",b1+i);
        all[++r]=b1[i];
    }
    for(int i=1;i<=m;i++)
    {
        scanf("%d",b2+i);
        all[++r]=b2[i];
    }
    sort(all+1,all+r+1);
    for(int i=1;i<=n+2*m;i++)
    {
        if(i==1||all[i]!=all[i-1])
        l[++q]=all[i];
    }
    for(int i=1;i<=n;i++)
    {
    	int x=query(a[i]);
    	cnt[x]+=1;
	}

	int t1=-1,t2=-1,t3;
	for(int i=1;i<=m;i++)
	{
		int s1=cnt[query(b1[i])];
		int s2=cnt[query(b2[i])];
		if(s1>t1||(s1==t1&&s2>t2))
		{
			t3=i;
			t1=s1;
			t2=s2;
		}
	}
	cout<<t3;
    return 0;
}

中位数

在有序序列中,中位数有一些优美的性质,可以引出一系列与他相关的问题。动态维护中位数也非常值得探讨。

例题
acwing104.货舱选址

在N为奇数时,货仓建在A[(N+1)/2];
在N为偶数时,货仓建在A[N/2]~A[N/2+1]。
因此不论哪种情况建在A[(N+1)/2]为最优。

#include<iostream>
#include<algorithm>
using namespace std;
#define MAX_N 100000
int n;
int a[MAX_N+5];
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    scanf("%d",a+i);
    sort(a+1,a+n+1);
    int t=a[(n+1)/2];
    int ans=0;
    for(int i=1;i<=n;i++)
    ans+=abs(a[i]-t);
    cout<<ans;
    return 0;
}

acwing105.七夕祭

交换左右两个相邻摊点只会改变某两列cl感兴趣的摊点数,交换上下只会改变某两行感兴趣的摊点数,所以可以把本题分成两个部分。
1.通过最少次数的左右交换使每列中cl感兴趣的摊点数相同。
2.通过最少次数的上下交换使每行中cl感兴趣的摊点数相同。
经计算 ∣ x 1 ∣ + ∣ x 2 ∣ + . . . + ∣ x n ∣ = ∣ x 1 − c 1 ∣ + ∣ x 2 − c 2 ∣ + ∣ x n − c n ∣ 。其中 c k = ( k − 1 ) ∗ ∑ i = 1 k − 1 a i + ( k − 1 ) ∗ a ( a 为平均数 ) |x1|+|x2|+...+|xn|=|x1-c1|+|x2-c2|+|xn-cn|。其中ck=(k-1)*\sum\limits_{i=1}^{k-1}ai+(k-1)*a(a为平均数) x1∣+x2∣+...+xn=x1c1∣+x2c2∣+xncn。其中ck=(k1)i=1k1ai+(k1)a(a为平均数)因此可以把该问题转化为“货仓选址”。

#include<iostream>
#include<algorithm>
using namespace std;
#define MAX_N 100000
int n,m,t;
int row[MAX_N+5],col[MAX_N+5];
int sum[MAX_N+5];
int c[MAX_N+5];
long long solve(int l,int*a)
{
    if(t%l)return -1;
    int avg=t/l;
    for(int i=1;i<=l;i++)
    {
        sum[i]=sum[i-1]+a[i];
        c[i]=(i-1)*avg-sum[i-1];
    }
    for(int i=1;i<=l;i++)
    {
        c[i]=(i-1)*avg-sum[i-1];
    }
    sort(c+1,c+l+1);
    int p=c[(l+1)/2];
    long long ans=0;
    for(int i=1;i<=l;i++)
    ans+=1ll*abs(c[i]-p);
    return ans;
}
int main()
{
    cin>>n>>m>>t;
    for(int i=1,x,y;i<=t;i++)
    {
        scanf("%d%d",&x,&y);
        row[x]++;
        col[y]++;
    }
    long long q1=solve(n,row);
    long long q2=solve(m,col);
    if(q1==-1&&q2==-1)
    cout<<"impossible";
    else if(q1==-1)
    cout<<"column"<<" "<<q2;
    else if(q2==-1)
    cout<<"row"<<" "<<q1;
    else cout<<"both"<<" "<<q1+q2;
    return 0;
}

acwing106.动态中位数

使用一个大顶堆和一个小顶堆。序列从小到大排名1~M/2存到大顶堆,M/2+1~M的存在小顶堆。

#include<iostream>
#include<queue>
using namespace std;
#define MAX_N 100000
int t;
int arr[MAX_N+5];
int main()
{
    cin>>t;
    while(t--)
    {
        int id,m,cnt=0;
        scanf("%d %d",&id,&m);
        printf("%d %d\n",id,(m+1)/2);
        priority_queue<int>down;
        priority_queue<int,vector<int>,greater<int>>up;
        for(int i=1,x;i<=m;i++)
        {
            scanf("%d",&x);
            if(down.size()==0||x<down.top())down.push(x);
            else up.push(x);
            if(down.size()>up.size()+1)up.push(down.top()),down.pop();
            if(up.size()>down.size())down.push(up.top()),up.pop();
            if(i%2)
            {
                printf("%d ",down.top());
                if(++cnt%10==0)printf("\n");
            }
        }
        if(cnt%10)printf("\n");
    }
    return 0;
}

第K大数

给定n个数,如何求出第k大数?
从大到小进行快速排序的思想是,在每一层递归中,随机选取一个数为基准,把比它大的交换到左半段,其余的和基准值自身一起作为右半段,然后继续递归对两边分别排序,平均情况下快速排序的复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)
实际上在选出来基准值之后,我们可以统计处大于基准值的数的数量cnt,如 k ≤ c n t k\leq cnt kcnt,我们就在左半边寻找第k大数;如果k>cnt,我们就在右半边寻找第k-cnt大数。在平均情况下,时间复杂度是 n + n / 2 + n / 4 + . . . + 1 = O ( n ) n+n/2+n/4+...+1=O(n) n+n/2+n/4+...+1=O(n)

逆序对

使用归并排序可以在 O ( l o g n ) O(logn) O(logn)时间内求出来一个长度为n的序列中逆序对的个数。归并排序每次把序列二分,递归对左右两个半边排序,然后合并两个序列。

例题
acwing107.超快速排序

#include<iostream>
using namespace std;
#define MAX_N 500000
long long a[MAX_N+5],t[MAX_N+5];
long long merge_sort(int l,int r)
{
    if(l==r)return 0;
    int mid=l+r>>1;
    long long ret=merge_sort(l,mid)+merge_sort(mid+1,r);
    int i=l,j=mid+1,k=0;
    while(i<=mid&&j<=r)
    {
        if(a[i]<=a[j])t[++k]=a[i++];
        else if(a[i]>a[j])
        {
            ret+=(mid-i+1);
            t[++k]=a[j++];
        }
    }
    while(i<=mid)t[++k]=a[i++];
    while(j<=r)t[++k]=a[j++];
    for(int i=l,j=1;i<=r;i++,j++)a[i]=t[j];
    return ret;
}
int main()
{
    int n;
    while(scanf("%d",&n),n)
    {
        for(int i=1;i<=n;i++)
        scanf("%lld",a+i);
        printf("%lld\n",merge_sort(1,n));
    }
    return 0;
}

acwing108.奇数码游戏

对于n为奇数的情况,两个局面可相互到达等价于把各个方格的数依次写成一行,逆序对个数奇偶性相同。
对于n为偶数地情况,两个局面可相互到达等价于把各个方格的数依次写成一行,逆序对个数奇之差和两个局面下空格所在的行数之差奇偶性相同。
在n*m网格上,也服从上面两个结论之一(根据列数奇偶性分类讨论)。

#include<iostream>
using namespace std;
#define MAX_N 500
int arr[MAX_N*MAX_N+5];
int t[MAX_N*MAX_N+5];
long long merge(int l,int r)
{
    if(l==r)return  0;
    int mid=(l+r)>>1;
    int i=l,j=mid+1,k=0;
    long long res=merge(l,mid)+merge(mid+1,r);
    while(i<=mid&&j<=r)
    {
        if(arr[i]<=arr[j])t[++k]=arr[i++];
        else{
            res+=(mid-i+1);
            t[++k]=arr[j++];
        }
    }
    while(i<=mid)t[++k]=arr[i++];
    while(j<=r)t[++k]=arr[j++];
    for(int i=l,j=1;i<=r;i++,j++)
    arr[i]=t[j];
    return res;
}
int main()
{
    int n,a;
    while(scanf("%d",&n)!=EOF)
    {
        int cnt=0;
        if(n==1)
        {
            scanf("%d%d",&a,&a);
            printf("TAK\n");
            continue;
        }
        for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        {
            scanf("%d",&a);
            if(a==0)continue;
            arr[++cnt]=a;
        }
        long long q1=merge(1,cnt);
        cnt=0;
        for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        {
            scanf("%d",&a);
            if(a==0)continue;
            arr[++cnt]=a;
        }
        long long q2=merge(1,cnt);
        if((q1&1)==(q2&1))printf("TAK\n");
        else printf("NIE\n");
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值