[LC优选算法#9] 分治:归并 | 计算右侧小于当前元素的个数 | 翻转对

1. 计算右侧小于当前元素的个数

计算右侧小于当前元素的个数

解题思路:

本题思路和leetcode LCR170.交易逆序对类似,只不过在返回值上有所差异,本题需要返回一个数组 ,存储的是对应下标元素逆序对的个数。LCR170解法详见:

LC优选算法#7 分治:快排&归并 | 库存管理 III | 排序数组 | 交易逆序对的总数

  1. 暴力枚举 O(N^2):逐个枚举数组中的元素,计算当前元素右侧小于当前元素的个数,放入数组中。
  2. 归并排序 O(NlogN):逆序对的找法可以根据LCR170的方式解决,即利用归并排序的方式优化暴力解法,依旧采用降序 的方式排序。

    Q :但是要如何将逆序对的个数存入数组中呢?ret数组的下标应该如何对应到元素的下标呢?需要注意的是,由于归并排序改变了元素的位置,因此不能直接用ret[cur1]记录,否则就是刻舟求剑了。
    A :解决方法是新建一个index数组,专门用于存放下标 ,改变nums中元素位置的同时,也改变index数组中对应元素下标的位置即可。注意index数组也需要创建对应的tmp数组:

这样算下来,我们就需要操作5个数组:nums,tmp_nums,index,tmp_in,ret。看上去很复杂,但捋顺思路后还是很好解决的:

cpp 复制代码
class Solution {
public:
    vector<int> ret;
    vector<int> index;
    vector<int> tmp_in;
    vector<int> tmp_nums;

    vector<int> countSmaller(vector<int>& nums)
    {
        int n = nums.size();
        ret.resize(n);
        index.resize(n);
        tmp_in.resize(n);
        tmp_nums.resize(n);

        for(int i=0; i<n; i++)
        {
            index[i] = i;
        }

        Merge_Sort(nums, 0, n - 1);

        return ret;
    }

    void Merge_Sort(vector<int>& nums, int left, int right)
    {
        if(left >= right) return;

        int mid = (left + right) / 2;

        Merge_Sort(nums, left, mid);
        Merge_Sort(nums, mid + 1, right);

        int cur1 = left;
        int cur2 = mid + 1;
        int i = 0;
        int j = 0;

        while(cur1 <= mid && cur2 <= right)
        {
            if(nums[cur1] > nums[cur2])
            {
                tmp_nums[i++] = nums[cur1];
                ret[index[cur1]] += right - cur2 + 1;
                tmp_in[j++] = index[cur1++];
            }
            else
            {
                tmp_nums[i++] = nums[cur2];
                tmp_in[j++] = index[cur2++];
            }
        }

        while(cur1 <= mid)
        {
            tmp_nums[i++] = nums[cur1];
            tmp_in[j++] = index[cur1++];
        }

        while(cur2 <= right)
        {
            tmp_nums[i++] = nums[cur2];
            tmp_in[j++] = index[cur2++];
        }

        for(int k=left; k<=right; k++)
        {
            nums[k] = tmp_nums[k - left];
            index[k] = tmp_in[k - left];
        }
    }
};

2. 翻转对

翻转对

本题在LCR170的基础上增加了nums[i] > nums[j] * 2的限制条件,因此依然可以借助归并排序优化暴力算法。

解题思路:

  1. 暴力枚举 O(N^2):逐个枚举数组中的元素,统计所有的翻转对总数。
  2. 归并排序 O(NlogN):大致的思路和求逆序对的思路一致,不同之处在于,上一题我们可以在归并排序的同时 统计逆序对的数量,这道题由于统计规则的改变,不能同时进行 ,因此需要分步骤:先对统计左右区间翻转对的数量,再归并两个区间。

降序统计:

注意cur2指针在移动时是不必回退 的,因为cur1在不断减小cur2前面的区间一定不满足 翻转对的条件:

升序统计:

时间复杂度 :由于两个指针在移动过程中都是不回退 的,在统计翻转对 时遍历了数组,归并排序 时再次遍历了数组,因此每层消耗的时间级数为N递归深度logN,因此总的时间复杂度为O(logN)

cpp 复制代码
class Solution {
public:
    vector<int> tmp;

    int reversePairs(vector<int>& nums)
    {
        int n = nums.size();
        tmp.resize(n);

        return Merge_Sort(nums, 0, n - 1);
    }

    //降序
    int Merge_Sort(vector<int>& nums, int left, int right)
    {
        if(left >= right) return 0;

        int mid = (left + right) / 2;
        int ret = 0;

        ret += Merge_Sort(nums, left, mid);
        ret += Merge_Sort(nums, mid + 1, right);

        int cur1 = left;
        int cur2 = mid + 1;
        int i = 0;

        //计算翻转对的数量(双指针)
        while(cur1 <= mid)
        {
            while(cur2 <= right && nums[cur1] / 2.0 <= nums[cur2]) //除以2避免越界
            {
                cur2++;
            }

            if(cur2 > right) break; //优化:没有翻转对的时候直接退出

            ret += right - cur2 + 1;
            cur1++;
        }

        //合并两个有序数组
        cur1 = left;
        cur2 = mid + 1;
        while(cur1 <= mid && cur2 <= right)
        {
            tmp[i++] = nums[cur1] >= nums[cur2] ? nums[cur1++] : nums[cur2++];
        }

        while(cur1 <= mid) tmp[i++] = nums[cur1++];
        while(cur2 <= right) tmp[i++] = nums[cur2++];

        for(int j=left; j<=right; j++)
        {
            nums[j] = tmp[j - left];
        }

        return ret;
    }
};

// 本期内容就到这里啦,如果对你有帮助,请三连支持!我是青云,我们下期见^_~