算法录 之 逆序对。

  问题如下:

  给一个长度为N的数组,a1,a2。。。aN,问其中逆序对的个数,其中逆序对的定义是 i<j 且 ai>aj 。

 

  1,比较朴素的想法是直接for循环,内层再一个for循环找到这个数前面比他大的个数。

1 int getans() {
2     int ret=0;
3     for(int i=1;i<=N;++i)
4         for(int j=1;j<i;++j)
5             if(a[j]>a[i])
6                 ++ret;
7     return ret;
8 }

  这样的复杂度是N^2的,比较慢。

 

  2,前面的算法比较慢,能不能优化?

  对于外层的for循环的每个ai,如果不用内层for循环而是用其他的方法更快的算出前面比ai大的个数,那么就快多了。

  用数据结构BST来维护。

  对于每个数,求出BST中比他大的个数,然后ret直接加上,然后再在BST中插入这个数。

 1 int getans() {
 2     BIT tree;
 3     int ret=0;
 4 
 5     for(int i=1;i<=N;++i) {
 6         ret+=tree.findMax(a[i]);
 7         tree.insert(a[i]);
 8     }
 9 
10     return ret;
11 }

  这样的话复杂度就是NlogN了,已经足够快了。

 

  3,还有一种也是NlogN的算法,而且速度比较稳定。需要用到归并排序的知识。

  先来看看归并排序,把a1,a2,a3。。。aN拆成两半,分别递归排序,然后再把这两个数组用O(N)来进行合并。

  那么在其中再加上一些代码。

  假设要求N个数的逆序对个数,如果先递归求好a1,a2。。。aN/2 和 aN/2+1。。。aN 这两个子数组的逆序对个数。

  然后在求i在左边且j在右边的逆序对个数,加起来就是答案了。

 

  因为i在左边,j在右边,如果用for循环来算的话,又变成N^2来算了。

  现在注意一个性质:因为左边那个和右边那个已经分别递归求好了,那么现在求i在左边,j在右边的逆序对的时候,两个子数组中数的的顺序已经没所谓了,所以先分别给左边的数和右边的数排好序。

  那么for循环右边那个,对于每个数二分搜索找到左边的有多少个比他大的。

 1 int getans(int L,int R) {
 2     if(R==L) return 0;
 3 
 4     int M=(L+R)/2;
 5     int ret=getans(L,M)+getans(M+1,R);            // 递归求解两个子序列并且排序(归并排序的内容)
 6 
 7     for(int i=M+1;i<=R;++i)
 8         ret+=M-find(L,M,a[i])+1;            // 二分查找L到M中比ai大的第一个数的位置。
 9 
10     merge(L,R);            // 归并排序的合并步骤。
11 }

  例子如下:

  求 2 4 3 5 1 7 9 8 的逆序对个数:

  先分别递归求出 2 4 3 5 和 1 7 9 8 的逆序对个数是1和1,然后求i在左边,j在右边的。先分别排序 2 3 4 5 和 1 7 8 9

  然后求左边中比1大的个数(因为是有序的,所以直接二分就行了。)是 4 ,所以ret加上4。

  再求左边中比7大的,再求比8大的,再求比9大的。都加起来的话就是答案了。

 

  这样在合并的时候是NlogN(二分搜索)的复杂度,那么根据主定理来求 T(N)=2*T(N/2)+NlogN 得 T(N)=NlogNlogN的复杂度。

 

  4,上面的算法还是效率不高,还能再优化。

  find这个二分搜索需要log的复杂度,可以优化到O(1)来。

  因为左右两个都是有序的,如果对于右数组的 ai 来说,设左数组中比他大的第一个数的位置是 bi,那么可以证明 bi 是递增的。

  比如例子:

  2 4 5 7 和 1 3 6 9

  比1大的左数组的第一个数的位置是1,比3大的第一个位置是2,比6大的第一个位置是4,比9大的第一个位置是5(超出表示没有),也就是这四个b是递增的。

  那么每次找左数组中比x大的数的位置时,可以不用find函数了,而是直接设一个指针递增。

  代码如下:

 1 int getans(int L,int R) {
 2     if(R==L) return 0;
 3 
 4     int M=(L+R)/2;
 5     int ret=getans(L,M)+getans(M+1,R);            // 递归求解两个子序列并且排序(归并排序的内容)
 6 
 7     int p=L;
 8     for(int i=M+1;i<=R;++i) {
 9         while(p<=M && a[p]<=a[i]) ++p;            // 因为位置是递增的。
10         ret+=M-p+1;
11     }
12 
13     merge(L,R);            // 归并排序的合并步骤。
14 }

  复杂度的话,因为p只会递增不会递减,所以while最多N次,均摊下来的话复杂度也是O(1)的对于每个for,那么总复杂度就是O(N)的来进行合并计算。

  T(N)=2*T(N/2)+N 求出 T(N)=NlogN 的复杂度。

posted @ 2015-12-22 13:53  WhyWhy。  阅读(1795)  评论(0编辑  收藏  举报