hdu 1394 Minimum Inversion Number

线段树 or 树状数组 or 归并排序

一个关于逆序数的问题。输入n,下面给出n个数字,分别是0到n-1,顺序不定。问你逆序数是多少?

逆序数就是逆序对的个数,所谓逆序对,就是一个序列中任取两个数字,若i<j,a[i]>a[j],则称a[i]和a[j]是一个逆序对,好像3,1,4,2,逆序对有(3,1),(3,2),(4,2),所以逆序数为3

但题目还没完,它还需要移动,每次将序列的第一个元素放在序列的最后,得到的新序列再求一次逆序数,可知一个长度为n的序列这样的移动可以做n-1次,可以得到n-1个新的序列,(第n次的话就会变回原始的序列)。然后问你,原序列和n-1个新序列中最小的逆序数是多少,然后输出。

 

1.这题的特殊性方便了我们去寻找最小值。现在假设我们知道了原序列的逆序数是first,那么每次移动后怎么算出新的逆序数呢?因为每次都只是移动头元素,假设头元素为x,那么可以知道由x产生的逆序对的个数为x,因为有x个数小于它(0,1,2……x-1),如果将它放到了末尾,那么这x个逆序对将会消失。同样地,整个序列中有(n-1-x)个元素大于x,那么x移到了末尾,将产生(n-1-x)个新的逆序对(这些逆序对分别为(x+1,x),(x+2,x),(x+3,x)……(n-1,x))。因此可以递推地解决这个问题。如果知道了当前序列逆序数为sum,那么移动头元素后的逆序数将会是sum-x+(n-1-x)

 

2.那么接下来的问题就是怎么求原序列的逆序数,得到它就可以知道所有其他序列的逆序数。用线段数解决

 

先以区间[0,9]为根节点建立val都为0的线段树,  
再看看怎样求下面序列的逆序数: 
1 3 6 9 0 8 5 7 4 2 
在线段树中插入1, 插入之前先询问区间[1,9]已插入的节点数(如果存在,必与1构成逆序)  v1=0 
在线段树中插入3, 插入之前先询问区间[3,9]已插入的节点数(如果存在,必与3构成逆序)  v2=0 
在线段树中插入6, 插入之前先询问区间[6,9]已插入的节点数(如果存在,必与6构成逆序)  v3=0 
在线段树中插入9, 插入之前先询问区间[9,9]已插入的节点数(如果存在,必与9构成逆序)  v4=0 
在线段树中插入0, 插入之前先询问区间[0,9]已插入的节点数(如果存在,必与0构成逆序)  v5=4 
在线段树中插入8, 插入之前先询问区间[8,9]已插入的节点数(如果存在,必与8构成逆序)  v6=1 
在线段树中插入5, 插入之前先询问区间[5,9]已插入的节点数(如果存在,必与5构成逆序)  v7=3 
在线段树中插入7, 插入之前先询问区间[7,9]已插入的节点数(如果存在,必与7构成逆序)  v8=2 
在线段树中插入4, 插入之前先询问区间[4,9]已插入的节点数(如果存在,必与4构成逆序)  v9=5 
在线段树中插入2, 插入之前先询问区间[2,9]已插入的节点数(如果存在,必与2构成逆序)  v10=7 
累加v1+……+v10  =22,这就是1 3 6 9 0 8 5 7 4 2的逆序数了. 

 

其实就是统计一个区间内已经插入了多少个元素,也可以说成是区间求和(一个区间能达到的最大和就是该区间长度,现在的和就是已经插入在内的元素个数)

 

下面是代码,只要看了上面的分析,代码是通熟易懂的

#include <cstdio>
#include <cstring>
//#define max(a,b) a>b?a:b
#define N 5000

struct node
{
    int l,r,c;
}t[4*N+10];
int x[N],n;

void build(int a ,int b ,int rt)
{
    t[rt].l=a; t[rt].r=b; t[rt].c=0;
    if(a==b) return ;
    int mid=(a+b)>>1;
    build(a,mid,rt<<1);
    build(mid+1,b,rt<<1|1);
}

int query(int a, int b ,int rt)
{
    if(t[rt].l==a && t[rt].r==b) return t[rt].c;
    int mid=(t[rt].l+t[rt].r)>>1;
    if(a>mid)       return query(a, b, rt<<1|1); //只访问右子树
    else if(b<=mid) return query(a, b, rt<<1); //只访问左子树
    else            return query(a, mid, rt<<1)+query(mid+1, b, rt<<1|1);; //左右均访问
}

void updata(int val , int rt)
{
    t[rt].c++;
    if(t[rt].l==t[rt].r) return ;
    int mid=(t[rt].l+t[rt].r)>>1;
    if(val>mid) updata(val, rt<<1|1);
    else        updata(val, rt<<1);
}

int min(int a ,int b)
{ return a<b?a:b; }

int main()
{
    while(scanf("%d",&n)!=EOF)
    {
        build(0,n-1,1);
        int first=0;
        for(int i=1; i<=n; i++)
        {
            scanf("%d",&x[i]);
            first+=query(x[i],n-1,1);
            updata(x[i],1);
        }
//        printf("first=%d\n",first);
        int ans,sum;
        ans=sum=first;
        for(int i=1; i<=n-1; i++)
        {
            sum=sum-x[i]+(n-1-x[i]);
            ans=min(ans,sum);
        }
        printf("%d\n",ans);
    }
    return 0;
}

 

 用树状数组解决,和线段树是完全一样的思想,就是统计,一开始c数组先清空为0,没读入一个数字,相当于相对应的a[i]增1,所以将增1沿路径修改c数组的值。

对于每个读入的元素x,就看[x+1,n-1]已经读入了多少个,就是这个数字产生的逆序对

#include <cstdio>
#include <cstring>
#define N 5010

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

void add(int *c ,int p ,int n,int k)
{
    while(p <= n)
    {
        c[p] += k;
        p += lowbit(p);
    }
}

int sum(int *c ,int p)
{
    int ans=0;
    while(p)
    {
        ans += c[p];
        p -= lowbit(p);
    }
    return ans;
}

int main()
{
    int n,m,first;
    int c[N],a[N];
    while(scanf("%d",&n)!=EOF)
    {
        memset(c,0,sizeof(c));
        first=0;
        for(int i=0; i<n; i++)
        {
            scanf("%d",&m);
            a[i]=m;
            //接着找出[m+1,n-1]里面已经有多少个元素
            //其实就是c[n-1]-c[m]
            //但c数组是从下标1开始的,所以c[n]-c[m+1]
            int s1,s2;
            add(c,m+1,n,1);  //m+1这个位置(下标从1开始)的元素加1
            s1=sum(c,m+1);
            s2=sum(c,n);
            first += (s2-s1);
        }
        //printf("%d\n",first);
        int ans=first , pre=first;
        for(int i=0; i<n-1; i++)
        {
            int x=a[i],s;
            s = pre-x+n-1-x;  //少了x对,多了n-1-x对
            if(s<ans) ans=s;
            pre=s;
        }
        printf("%d\n",ans);
    }
    return 0;
}

 

用归并排序来求解逆序对问题

在合并的过程中是将两个相邻并且有序的序列合并成一个有序序列,如以下两个有序序列

Seq13  4  5

Seq22  6  8  9

合并成一个有序序:

Seq2  3  4  5  6  8  9

对于序列seq1中的某个数a[i],序列seq2中的某个数a[j],如果a[i]<a[j],没有逆序数,如果a[i]>a[j],那么逆序数为seq1a[i]后边元素的个数(包括a[i]),即len1-i+1,

这样累加每次递归过程的逆序数,在完成整个递归过程之后,最后的累加和就是逆序的总数

#include <cstdio>
#include <cstring>
#define min(a,b) a<b?a:b
#define N 5050

int first,n;

void merge(int *c ,int *b , int left ,int right)
{
    int mid=(left+right)>>1 , i=left , j=mid+1 , k=left;
    while(i<=mid && j<=right)
    {
        if(c[i]>c[j]) //前部分大,要计数
        {
            b[k++]=c[j++];
            first+=(mid-i+1);
        }
        else b[k++]=c[i++];
    }

    if(i<=mid)
        while(i<=mid) { /*first+=(mid-i+1);*/ b[k++]=c[i++];  }
    else
        while(j<=right)  b[k++]=c[j++]; 
}

void mergesort(int *a , int *b , int left , int right)
{//把a数组的元素排好序保存在b数组中
    if(left==right) { b[left]=a[left]; return ;}
    int mid=(left+right)>>1 , c[N];
    mergesort(a,c,left,mid); //将a数组前部分排好临时保存在c数组中
    mergesort(a,c,mid+1,right); //将a数组后部分排好临时保存在c数组中
    merge(c,b,left,right); //将前后部分都有序的c数组合并在b数组中
}

int main()
{
    int a[N],x[N];
    while(scanf("%d",&n)!=EOF)
    {
        for(int i=1; i<=n; i++) scanf("%d",&x[i]);
        first=0;
        mergesort(x,a,1,n); //将x数组的元素排好序保存在a数组中,要保存x数组不变
//        printf("%d\n",first);
        int ans,sum;
        ans=sum=first;
        for(int i=1; i<=n-1; i++)
        {
            sum=sum-x[i]+(n-1-x[i]);
            ans=min(ans,sum);
        }
        printf("%d\n",ans);
    }
    return 0;
}

 

posted @ 2013-02-22 20:29  Titanium  阅读(481)  评论(0编辑  收藏  举报