• 【总结】逆序对


    逆序对总结

    概念

    度娘是这么说的:

    设 A 为一个有 n 个数字的有序集 (n>1),其中所有数字各不相同。

    如果存在正整数 i, j 使得 1 ≤ i < j ≤ n 而且 A[i] > A[j],则 <a[i], a[j]="">这个有序对称为 A 的一个逆序对,也称作逆序数


    求法

    • 通常情况下有这几种求逆序对的方法
    1. $O(n^{2}$)暴力,几乎没有用,不再介绍
    2. $O(n*logn)$的归并排序和树状数组.


    归并排序

    思想

    仔细想想归并排序的核心思想;
    实际上归并排序的交换次数就是这个数组的逆序对个数;

     

    归并排序是将数列a[l,h]分成两半a[l,mid]和a[mid+1,h]分别进行归并排序,然后再将这两半合并起来。  

    在合并的过程中(设l<=i<=mid,mid+1<=j<=h),当a[i]<=a[j]时,并不产生逆序数;当a[i]>a[j]时,在前半部分中比a[i]大的数都比a[j]大,将a[j] 放在a[i]前面的话,逆序数要加上mid+1-i。  

    因此,可以在归并排序中的合并过程中计算逆序数.  

     

    代码

    #include <iostream>   
    #include <string.h>   
    #include <stdio.h>   
    
    using namespace std;   
    const int N = 1005;   
    
    int a[N],tmp[N];   
    int ans;   
    
    void Merge(int l,int m,int r)   
    {   
        int i = l;   
        int j = m + 1;   
        int k = l;   
        while(i <= m && j <= r)   
        {   
            if(a[i] > a[j])   
            {   
                tmp[k++] = a[j++];   
                ans += m - i + 1;   
            }   
            else   
            {   
                tmp[k++] = a[i++];   
            }   
        }   
        while(i <= m) tmp[k++] = a[i++];   
        while(j <= r) tmp[k++] = a[j++];   
        for(int i=l;i<=r;i++)   
            a[i] = tmp[i];   
    }   
    
    void Merge_sort(int l,int r)   
    {   
        if(l < r)   
        {   
            int m = (l + r) >> 1;   
            Merge_sort(l,m);   
            Merge_sort(m+1,r);   
            Merge(l,m,r);   
        }   
    }   
    
    int main()   
    {   
        int n,T,tt=1;   
        scanf("%d",&T);   
        while(T--)   
        {   
            scanf("%d",&n);   
            for(int i=0;i<n;i++)   
                scanf("%d",&a[i]);   
            ans = 0;   
            Merge_sort(0,n-1);   
            printf("Scenario #%d:
    %d
    
    ",tt++,ans);   
        }   
        return 0;   
    }
    


    树状数组

    先理解树状数组在该背景下的意义

    我们假设一个数组A[n],当A[n]=0时表示数字n在序列中没有出现过,A[n]=1表示数字n在序列中出现过。A对应的树状数组为c[n],则c[n]对应维护的是数组A[n]的内容,即树状数组c可用于求A中某个区间的值的和。

    树状数组的插入函数(假设为 void insert(int i,int x) )的含义:在求逆序数这个问题中,我们的插入函数通常使用为insert( i , 1 ),即将数组A[i]的值加1 (A数组开始应该初始化为0,所以也可以理解为设置A[ i ]的值为1,即将数字i 加入到序列的意思 )。,同时维护c数组的值。

    树状数组中区间求和函数(假设函数定义为: int getsun(int i ) )的含义:该函数的作用是用于求序列中小于等于数字 i 的元素的个数。这个是显而易见的,因为树状数组c 维护的是数组A的值,则该求和函数即是用于求下标小于等于 i 的数组A的和,而数组A中元素的值要么是0要么是1,所以最后求出来的就是小于等于i的元素的个数。

    所以要求序列中比元素a大的数的个数,可以用i - getsum(a)即可( i 表示此时序列中元素的个数)。


    如何使用树状数组求逆序数总数:

     

    首先来看如何减小问题的规模:  

    要想求一个序列 a b c d,的逆序数的个数,可以理解为先求出a b c的逆序数的个数k1,再在这个序列后面增加一个数d,求d之前的那个序列中值小于d的元素的个数k2,则k1+k2即为序列a b c d的逆序数的个数。

    举个例子加以说明:

    假设给定的序列为 4 3 2 1,我们从左往右依次将给定的序列输入,每次输入一个数temp时,就将当前序列中大于temp的元素的个数计算出来,并累加到ans中,最后ans就是这个序列的逆序数个数。

     

    序列的变化序列中大于新增加的数字的个数操作
    {} 0 初始化时序列中一个数都没有
    {4} 0 往序列中增加4,统计此时序列中大于4的元素个数
    {4 3} 1 往序列中增加3,统计此时序列中大于3的元素个数
    {4 3 2} 2 往序列中增加2,统计此时序列中大于2的元素个数
    {4 3 2 1} 3 往序列中增加1,统计此时序列中大于1的元素个数

     

    当所有的元素都插入到序列后,即可得到序列{4 3 2 1}的逆序数的个数为1+2+3=6.  

     

    代码

    #include <iostream> 
    #include <string> 
    using namespace std; 
    #define N 1010 
    int c[N];  
    int n; 
    int lowbit(int i) 
    { 
        return i&(-i); 
    } 
    int insert(int i,int x) 
    { 
        while(i<=n){ 
            c[i]+=x; 
            i+=lowbit(i); 
        } 
        return 0; 
    } 
    
    int getsum(int i) 
    { 
        int sum=0; 
        while(i>0){ 
            sum+=c[i]; 
            i-=lowbit(i); 
        }  
        return sum; 
    } 
    void output() 
    { 
        for(int i=1;i<=n;i++) cout<<c[i]<<" "; 
        cout<<endl; 
    } 
    int main() 
    { 
        while(cin>>n){ 
            int ans=0; 
            memset(c,0,sizeof(c)); 
            for(int i=1;i<=n;i++){ 
                int a; 
                cin>>a; 
                insert(a,1); 
                ans+=i-insert(a);//统计当前序列中大于a的元素的个数 
            } 
            cout<<ans<<endl; 
        } 
        return 0; 
    }
    


    应用实例

    寒假作业

    有n项寒假作业。a给每项寒假作业都定义了一个疲劳值Ai,表示抄这个作业所要花的精力。b现在想要知道,有多少组连续的寒假作业的疲劳值的平均值不小于k?

    题目大意

    简单地说,给定n个正整数A1,A2,A3,…,An,求出有多少个连续的子序列的平均值不小于k。

    思路

    • 我们可以求前缀和sum,若要满足题意则(sum[j]-sum[i-1])/(j-i+1)>=k
      • 等价于(a[i]-k)+(a[i+1]-k)+…+(a[j]-k)/(j-i+1)>=0
      • (a[i]-k)+(a[i+1]-k)+…+(a[j]-k)>=0
    • 我们可以另设一个数组 b[],使 b[i]=a[i]-k
      • 等价于b[i]+b[i+1]+…+b[j]>=0
      • 所以可以再对b数组用一次前缀和,则有pre[j]-pre[i-1]>=0

    所以说,这是“顺”序对


    代码

    #pragma GCC optimize("Ofast") 
    #include<cstdio> 
    #include<iostream> 
    #include<algorithm> 
    #define redir(name) freopen(name".in","r",stdin),freopen(name".out","w",stdout) 
    using namespace std; 
    inline char gc(){ 
        static char buf[1<<17],*p1=buf,*p2=buf; 
        return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<17,stdin),p1==p2)?EOF:*p1++; 
    } 
    template<class T> 
    inline void read(T&n){ 
        register char ch=gc(); 
        for(n=0;ch<'0'||ch>'9';ch=gc()); 
        for(;ch>='0'&&ch<='9';ch=gc()) n=(n<<1)+(n<<3)+ch-'0'; 
    } 
    const int N=1000010; 
    int a[N],b[N],n,k,s; 
    long long sum[N]; 
    long long t[N]; 
    void mergesort(int l,int r) { 
        if(l==r) 
            return; 
        int mid=(l+r)/2; 
        int p=l; 
        int i=l; 
        int j=mid+1; 
        mergesort(l,mid); 
        mergesort(mid+1,r); 
        while(i<=mid&&j<=r) { 
            if(sum[j]>=sum[i]) { 
                s+=mid-i+1; 
                t[p]=sum[j]; 
                p++; 
                j++; 
            } else { 
                t[p]=sum[i]; 
                p++; 
                i++; 
            } 
        } 
        while(i<=mid) { 
            t[p]=sum[i]; 
            p++; 
            i++; 
        } 
        while(j<=r) { 
            t[p]=sum[j]; 
            p++; 
            j++; 
        } 
        for(i=l; i<=r; i++) 
            sum[i]=t[i]; 
    } 
    int main(){ 
        redir("choose"); 
        read(n),read(k); 
        for(unsigned int i=1;i<=n;++i) read(a[i]); 
        for(unsigned int i=1;i<=n;++i) sum[i]=sum[i-1]+a[i]-k; 
        mergesort(0,n); 
        printf("%d
    ",s); 
        return 0; 
    }
    



    参考http://www.cnblogs.com/xiongmao-cpp/p/5043340.html

  • 相关阅读:
    文字转语音功能
    windows定时计划任务
    写电子合同,爬过的坑,趟过的雷,犯过的错,都是泪
    前端应该如何去认识http
    I/O理解
    观察者模式
    js --代理模式
    js --策略模式
    js --单例模式
    js 单线程 异步
  • 原文地址:https://www.cnblogs.com/bbqub/p/8420794.html
Copyright © 2020-2023  润新知