随笔-80  评论-24  文章-0  trackbacks-0
逆序数问题是这样的一个问题:给定一个不含重复元素的序列<a1, a2, ..., an>,对于元素ai,若从a1~ai-1这i - 1个数中有k个数是比ai大的,那么ai的逆序数就是k,其中序列中的元素是可进行大小比较的。那么如何求一个序列的所有逆序的数量的总和呢?
我们可以考虑试试分治法,对于序列<a1, a2, ..., an>,我们可否先求<a1, a2, ..., an/2>和<an/2+1, an/2+2, ..., an>两个序列的各自的逆序数,然后再将两个逆序数相加是否就得到了总的逆序数呢?其实先求两个子序列的逆序数没有问题,直接相加也没问题,问题就出在,上述解法还漏掉了在子序列1中的元素比子序列2中的元素大的那种逆序,也就是跨越两个子序列的逆序。其实也就是下面的公式:T(n) = 2T(n/2) + T(merge)。关键问题就是如何求T(merge)。若序列seq1<a1, a2, ..., an/2>和序列seq2<an/2+1, an/2+2, ..., an>都是无序的,那么要找seq1中比an/2+i大的元素的数量则必须遍历seq1,这时候T(merge)的时间复杂度为O(n2)。那么T(n) = 2T(n/2) + O(n2)。总的时间复杂度太高。这样的merge不理想。那么如果seq1和seq2已经各自有序了呢?我们该如何merge呢?为了保持在递归过程中始终使子序列有序,我们每次merge后的序列都必须是有序的,所以这就个merge的过程就成了归并排序的merge过程了!!!关键就是在归并排序merge过程中该如何正确记录两个子序列之间的逆序数呢?归并排序其实就是对两个子序列,两个index i和j依次向后扫,对seq1[i]和seq2[j]做判断,若seq1[i]比seq2[j]大,则j++;若seq1[i]比seq2[j]小,则i++;我们仔细想想其实不难发现,若seq1[i]比seq2[j]小,那么seq2[1]~seq2[j  - 1]肯定都比seq1[i]小,这时候由seq1[i]决定的逆序数肯定为1 ~ j - 1;若seq1[i]比seq2[j]大,那么由于seq1[i]还有可能比seq2[j]后面的元素大,所以我们不计算。最后i和j有一个走到尾部之后,若seq1[]还有元素没有被扫描过,那么这些剩余的未被扫描的元素肯定比seq2[]中的所有元素都大,所以剩余这些的逆序数为seq1[]中剩余的元素数量 * seq2[]中总元素的数量。
根据poj2299(ultra quicksort)题目写的程序比较容易理解:

 1 #include <cstdio>
 2 #include <cstdlib>
 3 
 4 #define MAX 500005
 5 int a[MAX];
 6 
 7 long long merge(int *array, int low, int mid, int high) {
 8   if (!array) {
 9     return 0;
10   }
11   long long res = 0;
12   int *buf = (int *)malloc(sizeof(int) * (high - low + 1));
13   int i = low, j = mid + 1, k = 0;
14   while (i <= mid && j <= high) {
15     if (array[i] < array[j]) {
16       buf[k++] = array[i++];
17       res += j - mid - 1; //逆序数
18     } else {
19       buf[k++] = array[j++];
20     }   
21   }
22   while (i <= mid) {
23     buf[k++] = array[i++];
24     res += high - mid;
25   }
26   while (j <= high) {
27     buf[k++] = array[j++];
28   }
29   for (i = 0; i < k; ++i) {
30     array[low + i] = buf[i];
31   }
32   free(buf);
33   return res;
34 }
35 
36 long long merge_sort(int *array, int low, int high) {
37   if (!array || low >= high) {
38     return 0;
39   }
40   int mid = (low + high) / 2;
41   long long res1 = merge_sort(array, low, mid);
42   long long res2 = merge_sort(array, mid + 1, high);
43   long long res3 = merge(array, low, mid, high);
44   return res1 + res2 + res3;
45 }
46 
47 int main() {
48   int n;
49   while (scanf("%d", &n), n) {
50     int i;
51     for (i = 0; i < n; ++i) {
52       scanf("%d", &a[i]);
53     }
54     printf("%lld\n", merge_sort(a, 0, n - 1));
55   }
56   return 0;
57 }

另外这道题目其实还可以用线段树来解决,和上篇文章中秋stars的level的很像,stars那道题目是求序列中所有在自己前面的不大于自己值的数的数量。而逆序数是求所有在自己前面的大于自己值的数的数量。用同样的方法就可以解决,只不过由于该题的a[i]值比较大,最大值为999999999,这样的线段树太耗费内存。

到此时,其实上面的算法都比较常规,大部分人都知道求逆序数该用归并排序做,但是其实告诉大家一个事实,一个类似快排的树形结构,二叉排序树BST其实也可以做。只不过在二叉排序树的每个节点中都记录一个新的值,该值表示当前节点的右子树有多少个节点。具体算法不讲了,很简单,看代码:

 1 #include <cstdio>
 2 #include <cstdlib>
 3 
 4 #define MAX 500005
 5 
 6 struct BST {
 7   int value;
 8   int larger;
 9   BST *left;
10   BST *right;
11 };
12 
13 BST* insert(BST *t, int x, long long *count) {
14   if (!t) {
15     t = (BST *)malloc(sizeof(BST));
16     t->value = x;
17     t->larger = 0;
18     t->left = t->right = NULL;
19     return t;
20   }
21   if (t->value > x) {
22     (*count) += 1 + t->larger;
23     t->left = insert(t->left, x, count);
24   } else {
25     (t->larger)++;
26     t->right = insert(t->right, x, count);
27   }
28   return t;
29 }
30 
31 void release(BST *t) {
32   if (!t) {
33     return;
34   }
35   release(t->left);
36   release(t->right);
37   free(t);
38 }
39 
40 int main() {
41   int n;
42   while (scanf("%d", &n), n) {
43     int i, tmp;
44     long long res = 0;
45     BST *root = NULL;
46     for (i = 0; i < n; ++i) {
47       scanf("%d", &tmp);
48       root = insert(root, tmp, &res);
49     }
50     printf("%lld\n", res);
51     release(root);
52   }
53   return 0;
54 }

但是考虑到二叉排序树在最差情况下的性能为O(n2)的,所以用二叉排序树的算法会比采用归并排序的算法慢,时间为2700MS。这里要说明的是这里讲用二叉排序树来求逆序数只是讲这种思想,平均时间性能肯定没有归并排序快,但是这种思想还是需要仔细揣摩揣摩的。
posted on 2012-09-15 14:56 myjfm 阅读(2871) 评论(0)  编辑 收藏 引用 所属分类: 算法基础

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理