Ultra-QuickSort (树状数组):使序列有序的最小交换次数

原题POJ 2299

知识树状数组

题意:一个n个元素的数组,对它进行冒泡排序,输出排序时交换两个数字的次数。

解析:大家都知道,冒泡排序交换的次数就是数组中逆序数(不知道问度娘)之和,如果通过枚举,N^N直接做这题,肯定会T。你可以参考百度求逆序数的方法——并归,不过这里讲一下用树状数组的做法。

逆序数之和就是求ΣC[i],(C[i]代表第i个数后面比它小的数),稍微思考一下,它也就等于求ΣD[i],(D[i]代表第i个数前面比它大的数),这个就自己思考,不多讲了。

那怎么求ΣD[i]呢?

举个例子,2 5 3 7,我们从小往大算每个数的D[i]

第一个是2,前面有0个比2大的,D[1]==0;
第二个是3,前面有1个比3大的,D[2]==1;
第三个是5,前面有0个比2大的,D[3]==0;
第四个是7,前面有0个比7大的,D[4]==0;

这是一个怎么样的过程呢?

  1. 我们给原数组2 5 3 7的位置点一盏灯,1 1 1 1
  2. 我们按数字从小到大灭灯
  3. 首先灭2,0 1 1 1,如果统计2的位置前面还有几盏灯没有灭,为0
  4. 然后灭3,0 1 0 1,发现还有5的位置上的灯没灭,为1
  5. 然后灭5,0 0 0 1,5位置前面没有灯了,为0
  6. 最后灭7,0 0 0 0,同样,为0
  7. 所以0+1+0+0,只需要交换一次

解释一下

因为从小往大灭灯,所以当灭数字k位置上的灯时,所有还亮着的灯代表的数字都比k要大,要求k的位置前面的比k大的数,同解于,统计k位置前面的还没有灭的灯。

代码实现思路

代码需要有以下两个功能:

  1. 可以快速求解k位置前面还有几盏灯,即快速求区间和
  2. 可以实现快速灭灯,即快速单点更新

可以发现,和树状数组的优点如出一辙。

代码

#include<iostream>
#include<cstdio>
#include<cmath>
#include<string>
#include<cstring>
#include<algorithm>
#include<set>
#include<map>
#include<list>
#include<vector>
#include<stack>
#include<queue>
#include<ctime>
#include<cstdlib>
//#include<windows.h> 
#include<functional>
#define D long long
#define F double
#define MAX 0x7fffffff
#define MIN -0x7fffffff
#define mmm(a,b) memset(a,b,sizeof(a))
#define pb push_back  
#define mk make_pair  
#define fi first  
#define se second  
#define pill pair<int, int>  
#define for1(i,a,b) for(int i=a;i<=b;i++)
#define for2(i,a,b) for(int i=a;i>=b;i--)
#define ini(n) scanf("%d",&n)
#define inll(n) scanf("%lld",&n)
#define outisp(n) printf("%d ",n)
#define outllsp(n) printf("%lld ",n)
#define outiel(n) printf("%d\n",n)
#define outllel(n) printf("%lld\n",n)
using namespace std; 
#define N 500100 
#define MOD ((int)1e9+7)
#define random(a,b) (rand()%(b-a+1)+a)
#define stop Sleep(2000)
#define CLS system("cls")
const string el="\n";
const string elel="\n\n";
const string sp=" ";
const string spsp="  ";
const string tab="\t";

int n;
pill x[N];
int tr[N];
D ans;//ans可能非常大,需要用long long  

int lowbit(int i){
    return i&(-i);
}

void up(int a,int m){//A[a]+=m
    while(a<=n){
        tr[a]+=m;
        a+=lowbit(a);
    }
}

int query(int pos){
    int sum=0;
    while(pos>0){
        sum+=tr[pos];
        pos-=lowbit(pos);
    }
    return sum;
} 

void process(void){ans=0;
    //A数组就是此题树状数组所代表的数组 
    //mmm(A,1);//所谓的A数组赋初值一 
    ini(n);if(!n)return ;
    for1(i,1,n){
        int mid;ini(mid);
        x[i]=mk(mid,i);//保存在原数组中的位置
    }
    sort(x+1,x+1+n);//按照数的大小排序 
    //排序只是为了得到这些数的大小关系,重要的还是他们在原数组中是位置 

    for1(i,1,n){//因为A数组值为1,所以tr数组的值就等于tr[i]所代表的数的个数,lowbit(i) 
        tr[i]=lowbit(i);
    } 

    for1(i,1,n){//对于排序后的数组从小到大过一遍 
        //把A[i]变成0表示已经访问过了,A[i]前面的(A[1]~A[i-1])值还为1的个数,就是前面比它大的个数
        //因为for循环是从小到大, 变0的顺序也是,所以A[i]变0后还没有变0的就是比它大的数
        up(x[i].se,-1); //更新的是在原数组的位置 
        //1 to 0 , add -1
        ans+=query(x[i].se-1);//从1到i-1中1的个数 ,到i也可以,反正A[i]是0 
    } 
    outllel(ans);
    process();
}

int main(){
    process();
    return 0;
} 

第一次WA在了ans的范围,因为数据最多50万个,假设给你的是50个严格递减的,那么ans应该是50万*(50万+1)/2,int是肯定不行了。

说到这里,你们可能会有个疑问:万一很多个相同的,那灭灯的时候,前面有几个相同的数的位置上还有灯怎么办?

2 2 2,灯为 1 1 1,比如说先灭第二盏,那变成 1 0 1,前面有一个一,为1了,但是我们要求的是前面有几个大于的,不就冲突了吗?

这个我也是AC了才发现的,不过看了一下代码,会心一笑~

pair的sort在first相同时是按照second升序排列的,也就是说,一位置的2和二位置的二,会把一位置的2排在前面先灭,自然不会出现上面的问题了。

上面的代码可以看上去比较累,下面有删除注释版(写到main函数里面了,在其他都一样的情况下,运行结果从370ms变成了400ms,我还没有找原因,如果真的会变快下次全写main外面了)

//同上
int n;
pill x[N];
int tr[N];
D ans;

int lowbit(int i){
    return i&(-i);
}

void up(int a,int m){
    while(a<=n){
        tr[a]+=m;
        a+=lowbit(a);
    }
}

int query(int pos){
    int sum=0;
    while(pos>0){
        sum+=tr[pos];
        pos-=lowbit(pos);
    }
    return sum;
} 

int main(){
    while(ini(n),n!=0){ 
        ans=0;
        for1(i,1,n){
            int mid;ini(mid);
            x[i]=mk(mid,i);
        }
        sort(x+1,x+1+n);

        for1(i,1,n){
            tr[i]=lowbit(i);
        } 

        for1(i,1,n){
            up(x[i].se,-1); 
            ans+=query(x[i].se-1);
        } 
        outllel(ans);
    }
    return 0;
} 
  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值