[leetcode]315. 计算右侧小于当前元素的个数


原文链接
力扣315,没有花里胡哨的题面,属于树状数组的模版题了。正好复习一下BIT。先看题目:

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

CategoryDifficultyLikesDislikes
algorithmsHard (43.45%)1032-

给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。

示例 1:

输入:nums = [5,2,6,1]
输出:[2,1,1,0] 
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素

示例 2:

输入:nums = [-1]
输出:[0]

示例 3:

输入:nums = [-1,-1]
输出:[0,0]

看完题目,很自然想到用树状数组来做。关于树状数组,可以参考这篇文章:算法学习笔记(2) : 树状数组 - 知乎 (zhihu.com)

思路大概是:

  • 通过排序获得num中每个数在数组中的由小到大排位,该排位值就是该数在树状数组里的下标。
  • 由于查询每个点时,我们只需要知道右侧元素中小于该数的元素数量。因此从右往左遍历数组,遍历到某一点时,可以保证右侧元素对应的树状数组位置已被更新过。先用query查询,再将用该数更新数组。
  • 遍历完成,返回答案。

下面是代码。

#include <algorithm>
#include <vector>
using namespace std;
class Solution {
 public:
  vector<int> tree;
  int n;
  int lowbit(int x) { return x & (-x); }
  void update(int loc) {
    for (; loc <= n; loc += lowbit(loc)) {
      tree[loc]++;
    }
  }
  int query(int loc) {
    int ans = 0;
    for (; loc > 0; loc -= lowbit(loc)) {
      ans += tree[loc];
    }
    return ans;
  }
  vector<int> countSmaller(vector<int>& nums) {
    n = nums.size();
    tree = vector<int>(n + 1, 0);
    vector<int> result(n);
    vector<pair<int, int>> sortNums;
    vector<int> locOfNum(n);
    for (int i = 0; i < n; i++) {
      sortNums.emplace_back(nums[i], i);
    }
    sort(sortNums.begin(), sortNums.end());
    for (int i = 0; i < n; i++) {
      locOfNum[sortNums[i].second] = i + 1;
    }
    for (int i = n - 1; i >= 0; i--) {
      result[i] = query(locOfNum[i]);
      update(locOfNum[i]);
    }
    return result;
  }
};

除了用树状数组,还有没有其它O(nlogn)的算法呢?答案是有的。用归并排序求逆序对的数量也能解决本题。

首先定义逆序对。对于数组nums的下标i, j,若i<jnums[i]>nums[j],则称(nums[i], nums[j])是一对逆序对。题目中所要求的count[i],就是以nums[i]为左边元素的逆序对的个数。这样问题就被转化为了求逆序对。

考虑由两个数组L, R,分别从两数组中取一个元素ab,求使(a, b)为逆序对的取法由多少种。如果暴力枚举,复杂度为O(n2),不符合我们的期望。然而,如果LR是两个从小到大排列好的数组,问题将会很好解决。首先定义两个指针p1p2,初始时分别指向LR的第一个元素,然后执行以下算法:

1、右移p2直到R[p2]>=L[p1]。由于R[p2]是数组R中第一个大于等于L[p1]的元素,即数组Rp2之前的元素均小于L[p1],均能与L[p1]组成逆序对。故数组R开头到p2的元素个数就是能与L[p1]组成逆序对的元素个数。
2、右移p1,回到第一步求L中下一个元素能组成的逆序对个数。

该算法p1和p2均只经历一次遍历,时间复杂度为O(n),符合我们的要求。然而这个算法只能求分别从两个数组各取一个元素所能组成的逆序对,不能求在同一个数组中元素形成的逆序对。为使用上述算法,我们希望找到一个办法,让数组中的任意两个元素都有且仅有一次地被置于两个数组之中,并按上述算法求判断它们能否组成逆序对。这样我们不会遗漏任一逆序对,也不会重复计算。归并排序恰好能实现我们的期望。归并排序每次都会将数组分成两个小数组,通过递归调用分别将这两个小数组排好序,再合并这两个数组。如果我们在两个小数组排序好之后,对这两个数组执行计算逆序对的算法,之后再合并,能否满足要求呢?下面我们证明确实可以。

  • 首先,证明任意两个元素都至少有一次被置于两个不同的数组中,且对这两个数组执行求逆序对算法。归并排序递归的终止状态是待排序的数组中只有一个元素。也就是说,数组中的每一个元素,最初都处于只含有自己一个元素的数组中。而在排序的最后,所有元素都处在同一个数组中。也就是说,数组中的任意两个元素,最初都在不同的数组中,然而有一个合并的过程,将它们所在的两个数组合并为一个数组。只要在该次合并之前执行求逆序对算法,就能保证任意两个元素都有一次被置于两个不同的数组中求逆序对。
  • 接着,证明任意两个元素被分别置于两个数组求逆序对的经历只有一次。我们已经证明的该过程必有一次。而经历了这一次之后,两个元素被合并到同一个数组之中,以后不可能再被置于不同数组中了。因此可以保证只有一次。
  • 容易知道,原数组中的任意两个元素ab,若a位于b的左侧,则在归并排序过程中,a所在的数组和b所在的数组在执行求逆序对过程中,a所在数组一定在b所在数组左侧。

证明完成。另外还有一点,排序之后元素顺序被打乱了,但我们需要知道每个元素在原数组中的下标,以将其所能组成的逆序对数量记录在数组counts的相应位置。我们可以将原数组中的元素换成二元组(元素值,下标)。如此我们在排序过程中就能知道元素在原数组中的下标了。至此,思路已经很清晰了,下面看代码。

#include <algorithm>
#include <map>
#include <vector>
#define pii pair<int, int>
using namespace std;
class Solution {
 public:
  vector<pii> tem;
  vector<int> result;
  void MergeSort(vector<pii>& nums, int begin, int end) {
    if (begin == end) return;
    int mid = (begin + end) / 2;
    MergeSort(nums, begin, mid);
    MergeSort(nums, mid + 1, end);
    int l = begin;
    int r = mid + 1;
    while (l <= mid) {
      while (r <= end && nums[r] < nums[l]) r++;
      result[nums[l].second] += (r - mid - 1);
      l++;
    }
    int k = begin;
    l = begin;
    r = mid + 1;
    while (l <= mid && r <= end) {
      if (nums[l] < nums[r]) {
        tem[k++] = nums[l++];
      } else {
        tem[k++] = nums[r++];
      }
    }
    while (l <= mid) {
      tem[k++] = nums[l++];
    }
    for (int i = begin; i < r; i++) {
      nums[i] = tem[i];
    }
  }
  vector<int> countSmaller(vector<int>& nums) {
    int n = nums.size();
    result = vector<int>(n);
    tem = vector<pii>(n);
    vector<pii> indexNums(n);
    for (int i = 0; i < n; i++) {
      indexNums[i] = {nums[i], i};
    }
    MergeSort(indexNums, 0, n - 1);
    return result;
  }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值