思路
简要来说,归并分治的思路就是,在归并排序的基础上,统计答案。
具体而言,可以从以下步骤进行考虑:
- 答案是否可以从左区间、右区间和左跨右区间得到。
- 排序是否对寻找答案有利。
例题分析
翻转对
给定一个数组 nums ,如果 i < j
且 nums[i] > 2*nums[j]
我们就将 (i, j) 称作一个重要翻转对。
你需要返回给定数组中的重要翻转对的数量。
依题意,实际为找满足nums[左] > 2*nums[右]
的个数。
考虑暴力方法,对于每个坐标,依次向右遍历,寻找是否满足nums[i] > 2*nums[j]
的坐标,同时进行计数。显然,暴力解的时间复杂度是O(n^2)
的。
尝试使用归并分治的思想进行求解,注意到,如果取中点mid
,答案可以从左区间、右区间和左跨右区间得到。接下来,考虑第二点,排序是否对寻找答案有利?对于两个已经排好序的数组而言,可以分别看成左边元素的集合和右边元素的集合,对于单个集合中的元素而言,此时坐标顺序已不重要,因为在下游的递归中,我们已经得到了那部分的结果。此时,我们可以使用滑动窗口的思想,即一次遍历,寻找答案。设定两个指针,i
和j
,分别在左部分和右部分中迭代,满足条件nums[左] > 2*nums[右]
时,j
向右边遍历,直到越界。因为对于左部分,顺序是按从小到大排序的,即是说,*左部分[l, mid]的元素如果在右部分[mid+1, r]*有三个元素满足,对于左部分的下一个元素而言,它至少有三个元素满足。由此,我们可以用如下代码,统计答案。
1 2 3 4 5 6 7
| int ans = 0; for (int i = l, j = mid+1; i <= mid; ++i) { while (j <= r && 1LL * nums[i] > 1LL * nums[j] << 1) { j++; } ans += j - mid - 1; }
|
这道题的整体代码则如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| class Solution { public: int help[50000] = {0}; int merge(vector<int>& nums, int l, int mid, int r) { int ans = 0; for (int i = l, j = mid+1; i <= mid; ++i) { while (j <= r && 1LL * nums[i] > 1LL * nums[j] << 1) { j++; } ans += j - mid - 1; } int i = l, a = l, b = mid + 1; while (a <= mid && b <= r) { help[i++] = nums[a] <= nums[b] ? nums[a++] : nums[b++]; } while (a <= mid) { help[i++] = nums[a++]; } while (b <= r) { help[i++] = nums[b++]; } for (int j = l; j <= r; ++j) { nums[j] = help[j]; } return ans; } int mergeSort(vector<int>& nums, int l, int r) { if (l == r) { return 0; } int mid = l + (r - l) / 2; return mergeSort(nums, l, mid) + mergeSort(nums, mid+1, r) + merge(nums, l, mid, r); } int reversePairs(vector<int>& nums) { int n = nums.size(); return mergeSort(nums, 0, n-1); } };
|
时间复杂度
一般情况下,归并分治的时间复杂度和归并排序一致,为O(n*log n)