序列逆序对问题详解

有关逆序对的概念,请参考百度百科逆序对

知道了逆序对的概念,那么在程序中可以很容易地用枚举法暴力求出逆序对,时间复杂度为 O(n2) O ( n 2 ) ,对于 n<104 n < 10 4 的数据,这种方法是吃得消的,但是当数据变大时,这种方法就很容易超时,因此要进行优化。

这里讲两种方法,一种是改变统计的算法,即归并排序求逆序;另一种是改变储存的数据结构,使其在统计的时候效率变高,即树状数组逆序线段树求逆序。由于树状数组的编码比线段树简单,且两者的思想差不多,因此这里只介绍树状数组求逆序。

由于需要编码,因此借用牛客网的一个平台来检验代码的正确性,故这里给出一个题目背景:数组中的逆序对

归并排序求逆序

用这种方法首先要知道归并排序的原理与细节,具体请看归并排序

思想

统计逆序对的思想就如下图所示:
这里写图片描述

在归并排序中,首先要把序列划分成两部分,然后这两部分分别排序,最后合并两个有序序列;统计逆序对的过程就在其中,首先把序列分成两部分,这两部分内部的逆序对递归统计,最后在合并的时候要处理分散在两部分的逆序对(合并时,两个子序列内部已经是有序的了),在合并的时候,要关注并枚举右边子序列的每一个元素,对于每一个元素,要在左边的子序列中找出比当前元素大的元素的个数,把这个个数并入答案。

具体的操作,可以结合代码与图中给出的例子,一步一步分析。

代码

class Solution {
public:
    int InversePairs(vector<int> data) {
        return (int)dfs(data, 0, data.size());
    }

    long long dfs(vector<int> &data, int l, int r) {
        if (r - l == 1)
            return 0;
        const long long mod = 1000000007;
        int mid = (l + r) >> 1, ret = (dfs(data, l, mid) % mod + dfs(data, mid, r) % mod) % mod;
        vector<int> arr;
        int p_l = l, p_r = mid;
        for (; p_l < mid && p_r < r; )
            if (data[p_l] > data[p_r])
                arr.push_back(data[p_r++]), ret = (ret + mid - p_l) % mod;
            else
                arr.push_back(data[p_l++]);
        for (; p_l < mid; arr.push_back(data[p_l++]));
        for (; p_r < r; arr.push_back(data[p_r++]));
        for (int i = 0; l < r; data[l++] = arr[i++]);
        return ret % mod;
    }
};

由于是基于归并排序的算法,因此时间复杂度为 O(nlogn) O ( n l o g n ) ,空间复杂度为 O(nlogn) O ( n l o g n )

树状数组求逆序

树状数组的思路与实现细节请参考树状数组简单易懂的详解

思路

这里面树状数组并不是重点,它只是一种存储手段,用了树状数组只不过可以提升这个算法的效率而已;

思路其实很简单,就是数组中的元素(元素的值做新数组的下标)按照从左往右的顺序一次映射对应到新数组上,举个例子,数组arr = [1, 3, 0, 5],映射到新的数组new_arr = [1, 1, 0, 1, 0, 1],也就是说新数组初始化为零,原数组中的元素作为下标对应到新数组的对应位置上的值置1。注意,这个映射的过程要逐步进行,因为要在这个过程统计逆序对,例如,第一个元素是arr[0] = 1,故new_arr[arr[0]] = 1,这个时候统计new_arr1 ~ 4中有多少个1,然后第二个元素是arr[1] = 3,故new_arr[arr[1]] = 1,这个时候再次统计new_arr3 ~ 4有多少个1……依次进行下去,最后可以得到答案。

这里的原理就是利用原数组中元素出现的顺序与映射后元素的顺序来确定数组中的大小关系。

注意,上面的统计这个过程就要用到树状数组了,这也就是我所说的,树状数组在这里只是为了提高效率而已,如果实际问题中对效率要求不是很严格,那么完全可以不用树状数组。

由于数组中的元素可正可负,因此要用一个偏移值offset把所有的数都变成非负数;又因为树状数组要求下标都是正数,因此要加1使所有数都变成正数。

代码

class Solution {
public:
    int InversePairs(vector<int> data) {
        int offset = *min_element(data.begin(), data.end());
        int maxdata = *max_element(data.begin(), data.end());
        long long c[(const int)(maxdata - offset + 2)];
        memset(c, 0, sizeof(c));
        long long ret = 0;
        for (int i = 0; i < (int)data.size(); i++) {
            add(c, data[i] - offset + 1, maxdata - offset + 1);
            ret = (ret + i + 1 - getsum(c, data[i] - offset + 1)) % 1000000007;
        }
        return (int)ret;
    }

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

    long long getsum(long long c[], int i)
    {
        long long ret = 0;
        for (; i > 0; i -= lowbit(i))
            ret = (ret + c[i]) % 1000000007;
        return ret; 
    }

    void add(long long c[], int i, int n)
    {
        for (; i <= n; i += lowbit(i))
            ++c[i];
    }
};

没有离散化前的时间复杂度为 O(nlogn) O ( n l o g n ) ,空间复杂度为 O(max{a[i]}min{a[j]}) O ( m a x { a [ i ] } − m i n { a [ j ] } )

离散化思路

首先,离散化是个很基本的操作,我们都应该要熟悉这个操作,把稀疏的数字映射到一个数组中,这个数组的数字是紧凑的,这样可以节约空间。

假如,上面提到的数组arr = [1, 3, 0, 5],映射到新的数组new_arr = [1, 1, 0, 1, 0, 1],可以看到,中间有两个0是没有用的,离散化的具体表现就是去掉这些0,使存储空间更加紧凑;

由于去掉了没用的元素,因此,下标的意义就发生了变化,new_arr中的下标表示大小顺序,下标对应的值表示元素在排序完后所处的位置,还是举例子吧,arr = [1, 3, 0, 5]排完序后对应数组sorted_arr = [0, 1, 3, 5]new_arr = [1, 2, 0, 3],也就是说,arr[0] = 1sorted_arr对应在第二位,因此new_arr[0] = 1arr[1] = 3sorted_arr对应在第三位,因此new_arr[1] = 2,……依次类推。这样就完成了离散化。

离散化部分代码如下:

sort(data.begin(), data.end());
for (int i = 0; i < (int)pos.size(); i++)
    pos[i] = lower_bound(data.begin(), data.end(), arr[i]) - data.begin();

至于离散化后的事情,就和上面的思想一样了。

离散化代码

class Solution {
public:
    int InversePairs(vector<int> data) {
        vector<int> pos(data.size()), arr(data); 
        sort(data.begin(), data.end());
        for (int i = 0; i < (int)pos.size(); i++)
            pos[i] = lower_bound(data.begin(), data.end(), arr[i]) - data.begin();

        int c[(const int)data.size() + 1];
        memset(c, 0, sizeof(c));
        long long ret = 0;
        for (int i = 0; i < (int)data.size(); i++)
            add(c, pos[i] + 1, data.size()),
            ret = (ret + i + 1 - getsum(c, pos[i] + 1)) % 1000000007;
        return (int)ret;
    }

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

    long long getsum(int c[], int i)
    {
        long long ret = 0;
        for (; i > 0; i -= lowbit(i))
            ret = (ret + c[i]) % 1000000007;
        return ret; 
    }

    void add(int c[], int i, int n)
    {
        for (; i <= n; i += lowbit(i))
            ++c[i];
    }

};

离散化后的时间复杂度为 O(nlogn) O ( n l o g n ) ,空间复杂度为 O(n) O ( n )

总结

上面说的两种方法,时间复杂度都为 O(nlogn) O ( n l o g n ) ,对于数据规模较大的情况,效果良好。当然,在使用树状数组的时候同样可以使用线段树。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值