排序算法的经典运用

1.普通排序

AcWing 103.电影
这个题目没什么说的,主要是复习一下离散化:本题可以考虑直接使用桶排序(利用map这一数据结构),或用vector或数组将数据离散化后用桶排序。map在此处不多做介绍,直接展示vector的写法:

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N=2e5+5;
vector<int> lsh;
int a[N],b[N],c[N],tub[N];
int get(int x){
    return lower_bound(lsh.begin(),lsh.end(),x)-lsh.begin();
}
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        lsh.push_back(a[i]);
    }
    int m;
    cin>>m;
    for(int i=1;i<=m;i++){
        cin>>b[i];
    }
    for(int i=1;i<=m;i++){
        cin>>c[i];
    }
    sort(lsh.begin(),lsh.end());
    lsh.erase(unique(lsh.begin(),lsh.end()),lsh.end());
    for(int i=1;i<=n;i++){
        int x=get(a[i]);
        tub[x]++;
    }
    int maxn=0,ans=1;
    for(int i=1;i<=m;i++){
        int x=get(b[i]);
        if(lsh[x]==b[i] && tub[x]>maxn){
        //特别注意判断b[i]是否在vector中出现过
            maxn=tub[x];
            ans=i;
        }
    }
    int maxx=0;
    for(int i=1;i<=m;i++){
        int x=get(b[i]);
        if((lsh[x]==b[i] && tub[x]==maxn) || maxn==0){
            int y=get(c[i]);
            if(lsh[y]==c[i] && tub[y]>maxx){
                ans=i;
                maxx=tub[y];
            }
        }
    }
    cout<<ans<<endl;
    return 0;
}

2.归并排序

AcWing 108.奇数码问题
此问题可以转化为排序中的逆序对问题:将整个矩阵去掉空格后从上到下、从左到右得到一个长度为n*n-1的序列,那么考虑左右移动空格不会改变序列,而上下移动则相当于将某个元素x前移n-1位或后移n-1位,假设这n-1个跳过的元素中有a个元素大于x,有b个元素小于x(a+b=n-1)。则逆序对个数ans+=x-y,显然x-y是偶数,没有改变逆序对个数奇偶性。由此可以初步得到一个结论:若初状态与末状态的逆序对个数相同,则两个状态可以互相达成。这里证明了必要性,充分性过于复杂不做证明。此结论可以进一步推广到偶数码以及n * m矩阵的情况,具体见蓝书P39。总而言之,这类问题的解的存在性判断可以通过归并排序求逆序对实现。如果要求具体移动方法,则需要使用A * 算法求解。

#include<iostream>
#include<cstring>
using namespace std;
const int N=505;
int a[N*N],b[N*N],gb[N*N];
long long nxd;
void msort(int a[],int l,int r){
    if(l>=r) return;
    int mid=(l+r)>>1,i=l,j=mid+1,k=l;
    msort(a,l,mid);
    msort(a,mid+1,r);
    while(i<=mid && j<=r){
        if(a[i]<=a[j]) gb[k++]=a[i++];
        else{
            nxd+=mid-i+1;
            gb[k++]=a[j++];
        }
    }
    while(i<=mid) gb[k++]=a[i++];
    while(j<=r) gb[k++]=a[j++];
    for(int p=l;p<=r;p++) a[p]=gb[p];
}
int main(){
    int n;
    while(cin>>n){
        memset(a,0,sizeof a);
        memset(b,0,sizeof b);
        int cnt=0;
        for(int i=1;i<=n*n;i++){
            int k;
            cin>>k;
            if(k!=0) a[++cnt]=k;
        }
        cnt=0;
        for(int i=1;i<=n*n;i++){
            int k;
            cin>>k;
            if(k!=0) b[++cnt]=k;
        }
        nxd=0;
        msort(a,1,n*n-1);
        long long x=nxd%2;
        nxd=0;
        msort(b,1,n*n-1); 
        long long y=nxd%2;
        if(x!=y) cout<<"NIE"<<endl;
        else cout<<"TAK"<<endl;
    }
}

3.绝对值不等式(贪心)

基本模型:AcWing104.货仓选址
在一条数轴上有 n家商店,它们的坐标分别为 A1∼An。现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。
假设货仓的位置为x,那么问题就转化为了求|a1-x|+|a2-x|+……+|an-x|的最小值。那么这个问题就很简单,在初一学过,只要将x取在a1~an的中位数上即可。
你以为事情就这么简单?不!这个问题还要在很多难题中得到应用。
类型1:二维货仓选址(AcWing 123.士兵)
本题很明显要先在y轴上使用货仓选址的公式,那么,x轴上怎么办呢?当然也要将其转化为货仓选址问题。但是并不是直接使用公式。所以我们要思考一下:假设第一个士兵移到了一个位置上,其横坐标为x。那么移动步数为|x[1]-x|。同理第二个士兵要移动|x[2]-x-1|步,第三个移动|x[3]-x-2|步,以此类推,第n个要移动|x[n]-x-(n-1)|=|x[n]-n+1-x|。那么很明显,我们只要将所有的a[i]=x[i]-i+1排序求中位数即可。

#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=10005;
int a[N],b[N];
int main(){
    int n;
    long long ans=0;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i]>>b[i];
    }
    sort(a+1,a+n+1);
    sort(b+1,b+n+1);
    for(int i=1;i<=n;i++){
        a[i]-=i;
    }
    sort(a+1,a+n+1);
    for(int i=1;i<=n;i++){
        ans+=abs(a[i]-a[n+1>>1]);
        ans+=abs(b[i]-b[n+1>>1]);
    }
    cout<<ans<<endl;
}

类型2:推公式转化

例题1:AcWing 122.糖果传递
凡是碰到环形类问题,都要想到拆环为链的思想,那么我们想一下链状的糖果传递怎么做呢?很明显是从左向右不断地传递就可以了。所以对于该问题,设给定的糖果数为a1……an,设a为整个数组中的数字的平均数,那么ans=|a1-a|+|a1+a2-2a|+|a1+a2+a3-3a|+……+|a1+a2+a3+……+an-na|。但这个式子并不是我们想要的形式,所以要对它变形:根据拆环为链的思想,我们应当去枚举断开的位置,但是这样的复杂度达到了O(n^2),不能接受。所以我们可以假设拆开后的链的第一个数是原数组的第k个数ak。那么式子就变成了:|ak-a|+|ak+1 + ak+2 -2a|+……+|ak+ak+1 + ak+2 +……+an- (n-k)a|+|ak+ak+1 + ak+2 +……+an+a1- (n-k+1) a|+……+|ak+ak+1 + ak+2 +……+an+a1+……+ak-1 -na|,那么到这里,我们注意到每一项中ai的个数与a的系数相同,可以将它们配对。那么配对后可以发现这其中的每一项都是原数组中的一段区间和,考虑采用前缀和:令b[i]=a[i]-a,f[i]为b[i]的前缀和数组。那么式子可以化为:|f[k]-f[k-1]|+|f[k+1]-f[k-1]|+……+|f[n]-f[k-1]|+|f[n]-f[k-1]+f[1]|+|f[n]-f[k-1]+f[2]|+……+|f[n]-f[k-1]+f[k-1]|。我们惊奇地发现:所有的项中都有-f[k-1],好像可以用公式了,但是后面还有f[n]怎么办?显然f[n]=0。至此,这个问题被完美地解决了。

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1e6+5;
typedef long long ll;
ll a[N],f[N];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        scanf("%lld",&a[i]);
        a[0]+=a[i];
    }
    a[0]/=n;

    for(int i=1;i<=n;i++){
        a[i]-=a[0];
        f[i]+=f[i-1]+a[i];
    }
    sort(f+1,f+n+1);
    ll ans=0;
    for(int i=1;i<=n;i++){
        ans+=abs(f[i]-f[n+1>>1]);
    }
    cout<<ans<<endl;
}

总结:求解这一类问题,一定要对一些基本的转化思想与模板题了然于胸,并且对数学有一定的敏感程度。
例题2:AcWing 105.七夕祭
这题就是二维的糖果传递,没什么好说的。直接上代码:

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
using namespace std;
typedef long long ll;
const int N=1e5+5;
int row[N],col[N];
ll get_min(int a[N],int n,int sum){
    ll b[N];
    int ave=sum/n;
    for(int i=1;i<=n;i++) b[i]=b[i-1]+a[i]-ave;
    sort(b+1,b+n+1);
    ll mid=b[(n+1)/2],ans=0;
    for(int i=1;i<=n;i++) ans+=abs(b[i]-mid);
    return ans;
}
int main(){
    int n,m,t;
    cin>>n>>m>>t;
    while(t--){
        int x,y;
        cin>>x>>y;
        row[x]++;
        col[y]++;
    }
    int R=0;
    for(int i=1;i<=n;i++) R+=row[i];
    int C=0;
    for(int i=1;i<=m;i++) C+=col[i];
    ll ans=0;
    bool flagr=0,flagc=0;
    if(R%n==0){
        ans+=get_min(row,n,R);
        flagr=1;
    }
    if(C%m==0){
        ans+=get_min(col,m,C);
        flagc=1;
    }
    if(flagr && flagc){
        cout<<"both "<<ans;
    }
    else 
        if(flagr){
            cout<<"row "<<ans;
        }
        else 
            if(flagc){
                cout<<"column "<<ans;
            }
            else puts("impossible");
    return 0;
}

4.中位数

AcWing 106.动态中位数
本题作为模板题,核心就是“对顶堆”:即用大根堆存前半部分的数,小根堆存后半部分的数,每次读入一个数据,便将其与当前的中位数比较。若小于,压入大根堆;若大于,压入小根堆。并且每次都要调节两堆中数字的个数,使它们的个数之差始终为1或0。

代码

#include<iostream>
#include<queue>
#include<algorithm>
using namespace std;
priority_queue<int> q1,nul1;
priority_queue<int,vector<int>,greater<int> > q2,nul2;
int main(){
    int m;
    cin>>m;
    while(m--){
        q1=nul1;
        q2=nul2;
        int n,bh;
        cin>>bh>>n;
        cout<<bh<<' '<<(n+1)/2<<endl;
        int a,cnt=0;
        for(int i=1;i<=n;i++){
            cin>>a;
            if(!q1.size() || a<q1.top()) q1.push(a);//注意这些小细节,要保证q1中的个数始终大于等于q2中的个数
            else q2.push(a);
            while(q1.size()<q2.size()){//调剂个数
                int t=q2.top();
                q1.push(t);
                q2.pop();
            }
            while(q1.size()>q2.size()+1){
                int t=q1.top();
                q2.push(t);
                q1.pop();
            }
            if(i&1){
                cnt++;
                cout<<q1.top()<<' ';
                if(cnt%10==0) cout<<endl;
            }
        }
        if(cnt%10!=0) cout<<endl;
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值