赞
踩
原文链接
力扣315,没有花里胡哨的题面,属于树状数组的模版题了。正好复习一下BIT。先看题目:
Category | Difficulty | Likes | Dislikes |
---|---|---|---|
algorithms | Hard (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<j
且nums[i]>nums[j]
,则称(nums[i], nums[j])
是一对逆序对。题目中所要求的count[i]
,就是以nums[i]
为左边元素的逆序对的个数。这样问题就被转化为了求逆序对。
考虑由两个数组L
, R
,分别从两数组中取一个元素a
和b
,求使(a, b)
为逆序对的取法由多少种。如果暴力枚举,复杂度为O(n2),不符合我们的期望。然而,如果L
和R
是两个从小到大排列好的数组,问题将会很好解决。首先定义两个指针p1
和p2
,初始时分别指向L
和R
的第一个元素,然后执行以下算法:
1、右移p2
直到R[p2]>=L[p1]
。由于R[p2]
是数组R
中第一个大于等于L[p1]
的元素,即数组R
中p2
之前的元素均小于L[p1]
,均能与L[p1]
组成逆序对。故数组R
开头到p2
的元素个数就是能与L[p1]
组成逆序对的元素个数。
2、右移p1
,回到第一步求L中下一个元素能组成的逆序对个数。
该算法p1和p2均只经历一次遍历,时间复杂度为O(n),符合我们的要求。然而这个算法只能求分别从两个数组各取一个元素所能组成的逆序对,不能求在同一个数组中元素形成的逆序对。为使用上述算法,我们希望找到一个办法,让数组中的任意两个元素都有且仅有一次地被置于两个数组之中,并按上述算法求判断它们能否组成逆序对。这样我们不会遗漏任一逆序对,也不会重复计算。归并排序恰好能实现我们的期望。归并排序每次都会将数组分成两个小数组,通过递归调用分别将这两个小数组排好序,再合并这两个数组。如果我们在两个小数组排序好之后,对这两个数组执行计算逆序对的算法,之后再合并,能否满足要求呢?下面我们证明确实可以。
a
和b
,若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; } };
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。