求逆序对数的NLogN解法:归并排序、树状数组和线段树

定义

对于一个包含N个非负整数的数组A[1..n],如果有i < j,且A[ i ]>A[ j ],则称(A[ i] ,A[ j] )为数组A中的一个逆序对。

例如,数组(3,1,4,5,2)的逆序对有(3,1),(3,2),(4,2),(5,2),共4个。

朴素的枚举

双重循环搞定,O(N^2)的复杂度。程序就不写了。

用归并排序思想解决

网上思路很多,理解归并排序就不难解决了。具体的归并排序就不介绍了,这里给出RQNOJ 173的代码

var
    a,b:array[1..1000000] of longint;
    i,j,k,n:longint;
        ans:int64;


procedure mergesort(l,r:longint);
var
    i,j,k,m:longint;
begin
  if l>=r then
         exit;
    m:=(l+r) shr 1;
    mergesort(l,m);
    mergesort(m+1,r);
        i:=l;
    j:=m+1;
        k:=l;
        repeat
            if a[i]>a[j] then
                begin
                    b[k]:=a[j];
                    inc(ans,m-i+1);//关键在于这步
                    inc(j);
                    inc(k);
                end
            else begin
                b[k]:=a[i];
                inc(k);
                inc(i);
            end
    until (i>m) or (j>r);
    while i<=m do
    begin
        b[k]:=a[i];
        inc(k);
        inc(i);
    end;
    while j<=r do
    begin
      b[k]:=a[j];
        inc(k);
        inc(j);
    end;
  for i:=l to r do
        a[i]:=b[i];
end;
begin
  readln(n);
  for i:=1 to n do
        read(a[i]);
     mergesort(1,n);
    writeln(ans);
end.

用支持区间修改的数据结构解决

用线段树和树状数组可以在NLogN的时间内求逆序对数。

一对数是逆序对当且仅当i < j && a[i] >a[j]。

过程

我们将序列的元素逆序插入,插入a[i]就是update(a[i],1)。每次插入前ans += getsum(1,a[i]-1)。最后ans就是逆序对数了。

正确性

因为是逆序插入的,所以在getsum中的元素的下标都比i大,而且查询a[i]-1,满足值比a[i]小的条件。

这两个操作都是线段树和树状数组的典型应用,写起来应该比不太常用的归并排序熟练地多。而且树状数组代码能写得很短。

 

 

下面是HDU 1394的代码:

/*
题意:一个由0..n-1组成的序列,每次可以把队首的元素移到队尾,求形成的n个序列最小逆序对数目

算法:
由树状数组求逆序对。加入元素i即把以元素i为下标的a[i]值+1,从队尾到队首入队,
每次入队时逆序对数 += getsum(i - 1),即下标比它大的但是值比它小的元素个数。

因为树状数组不能处理下标为0的元素,每个元素进入时+1,相应的其他程序也要相应调整。

求出原始的序列的逆序对个数后每次把最前面的元素移到队尾,逆序对数即为
原逆序对数+比i大的元素个数-比i小的元素个数,因为是0..n,容易直接算出,每次更新min即可。

体会:不要照搬别人的程序,细节处理不一样会Debug很久的
*/


#include <stdio.h>
#define MAXN 10000

int c[MAXN],a[MAXN],n;

int min(int a,int b)
{
    if (a < b) return a;
    else return b;
}

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


void update(int i,int x)
{
    while (i <= n)
    {
        c[i] += x;
        i += lowbit(i);
    }
}

int getsum(int x)
{
    int sum = 0;
    while (x > 0)
    {
        sum += c[x];
        x -= lowbit(x);
    }
    return sum;
}

int main()
{
    while (scanf("%d", &n) == 1)
    {
        for (int i = 1; i <= n; i++)
            c[i] = 0;
        int sum = 0;
        for (int i = 1; i <= n; i ++)
        {
            scanf ("%d", &a[i]);
        }
        for (int i = n; i >= 1; i --)
        {
            update (a[i] + 1, 1);
            sum += getsum (a[i]);
        }
        int ans = sum;
        for (int i = 1; i <= n; i++)
        {
            sum = sum - a[i] + (n - a[i] - 1);
            ans = min(ans, sum);
        }
        printf("%d\n", ans);
    }
    return 0;
}
 
线段树的代码:
/*
    题意:一个由0..n-1组成的序列,每次可以把队首的元素移到队尾,
          求形成的n个序列最小逆序对数目
    算法:将元素依次插入线段树,每次增加的逆序对数为比它大的已经插入的
          数的个数,可以用线段树维护,由于元素值为0..n,每次移动可求出增减
          逆序对的数量更新。
    体会:线段树敲了20分钟,如果是PASCAL并且写得难看些10分钟应该能搞定。
          Debug 10分钟,主要是更新时没考虑清楚+记错题目求成最大逆序对数了

*/


#include <stdio.h>
#define MAXN 100000
#define ROOT 1

struct node
{
    int left,right,sum;
}t[MAXN];

int val[MAXN];
int n;

void build(int p, int left, int right)
{
    int m;
    t[p].left = left;
    t[p].right = right;
    t[p].sum = 0;
    if (left == right)
        return;
    m = (left + right) / 2;
    build(p*2, left, m);
    build(p*2+1, m+1, right);
}

void update(int p, int goal, int add)
{
    t[p].sum += add;
    if (t[p].left == t[p].right)
        return;
    int m = (t[p].left + t[p].right) / 2;
    if (goal <= m)
        update(p*2, goal, add);
    if (goal > m)
        update(p*2+1, goal, add);
}

int getsum(int p, int left, int right)
{
    if (left > right)
        return 0;
    if (t[p].left == left && t[p].right == right)
        return t[p].sum;
    int m = (t[p].left + t[p].right) / 2;
    if (right <= m)
        return getsum(p*2, left, right);
    else if (left > m)
        return getsum(p*2+1, left, right);
    else return getsum(p*2, left, m) + getsum(p*2+1, m + 1, right);
}


int main()
{
    while (scanf("%d", &n) == 1)
    {
        build(ROOT, 0, n - 1);
        int i,sum = 0,ans;
        for (i = 1; i <= n; i++)
        {
            scanf("%d", &val[i]);
            sum += getsum(ROOT, val[i], n - 1);
            update(ROOT, val[i], 1);
        }
        ans = sum;
        for (i = 1; i <= n; i++)
        {
            sum = sum + (n - val[i] - 1) - val[i];
            ans = sum < ans ? sum : ans;
        }
        printf("%d\n", ans);
    }
    return 0;
}

POJ 3067

/*
    题意:左边一排城市,右边一排城市,中间若干条道路,求交点。

    算法:转化为求逆序对的问题。两条路有交点当且仅当A.x<B.x && A.y>B.y。将路径以x为第一关键字
    y为第二关键字升序排序后就是求y的逆序对了。树状数组可以解决。

    此外还有一个复杂度为mn的方法,详见http://www.cnblogs.com/penseur/archive/2011/04/14/2015998.html

    注意点:
    1.题目的k很大。数组要开得大。
    2.由于树状数组不能处理下标为0的元素,所以输入的时候y下标都加上1.

    收获与感想:
    1.复习了用区间和求逆序对的方法
    2.学会了调用qsort函数
    3.进一步理解了指针

    一点废话:
    这题做了一天啊!!!整整一天啊!!!网上的资料不可信啊,给一段编译出错的代码啊!!!关键时刻还是要看K&R
    啊!!!MAXM和MAXN搞错了又是调试很久啊~~~

    2011-07-18 13:07
*/



#include <stdio.h>
#include <stdlib.h>

#define MAXN 1100
#define MAXM 1000100

int n, m, k;
struct point
{
     int x, y;
}a[MAXM];

long long c[MAXN];

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

long long getsum(int i)
{
    long long sum = 0;
    while (i > 0)
    {
        sum += c[i];
        i -= lowbit(i);
    }
    return sum;
}

void add(int i)
{
    while (i <= MAXN)
    {
        c[i]++;
        i += lowbit(i);
    }
}


int cmp(const void *a,const void *b)
{
    struct point *c = (struct point *)a;
    struct point *d = (struct point *)b;
    if (c->x == d->x)
        return d->y - c->y;
   return d->x - c->x;
}

int main()
{
    int t, j, i;
    long long ans;
    scanf("%d", &t);
    for (j = 1; j <= t; j++)
    {
        ans = 0;
        scanf("%d%d%d", &n, &m, &k);
        memset(c, 0, sizeof(c));
        for (i = 1; i <= k; i++)
            scanf("%d%d", &a[i].x, &a[i].y);
        for (i = 1; i <= k; i++)
            a[i].x++;
        qsort(a+1, k, sizeof(a[0]), cmp);
    //    for (i = 1; i <= k; i++)
    //        printf("%d %d\n", a[i].x , a[i].y);
    //    printf("\n");
        for (i = 1; i <= k; i++)
        {
            ans += getsum(a[i].y-1);
            add(a[i].y);
        }
         printf("Test case %d: %lld\n",j, ans);
    }
    return 0;
}
 
 

posted on 2011-07-20 12:47  oa414  阅读(3131)  评论(0编辑  收藏  举报

导航