给你一个长度为 n 的整数数组 nums ,表示由范围 [0, n - 1] 内所有整数组成的一个排列。
全局倒置 的数目等于满足下述条件不同下标对 (i, j) 的数目:
局部倒置 的数目等于满足下述条件的下标 i 的数目:
当数组 nums 中 全局倒置 的数量等于 局部倒置 的数量时,返回 true ;否则,返回 false 。
提示
局部倒置的数量可以通过一次遍历求出,全局倒置的数量等同于求逆序对数量,可以用归并排序来做。
最后对比局部倒置数量和全局倒置数量是否相等即可。
// C++ 300ms
typedef long long LL;
class Solution {
public:
vector<int> temp;
LL merge_sort(vector<int>& nums, int l, int r) {
if (l >= r) return 0;
int mid = l + r >> 1;
LL ans = merge_sort(nums, l, mid) + merge_sort(nums, mid + 1, r);
int i = l, j = mid + 1, k = 0;
while (i <= mid && j <= r) {
if (nums[i] <= nums[j]) temp[k++] = nums[i++];
else {
ans += mid - i + 1;
temp[k++] = nums[j++];
}
}
while (i <= mid) temp[k++] = nums[i++];
while (j <= r) temp[k++] = nums[j++];
for (i = l, j = 0; i <= r; i++, j++) nums[i] = temp[j];
return ans;
}
bool isIdealPermutation(vector<int>& nums) {
LL cnt = 0, n = nums.size();
for (int i = 0; i < n - 1; i++) {
if (nums[i] > nums[i + 1]) cnt++;
}
temp.resize(n);
LL tot_cnt = merge_sort(nums, 0, n - 1);
return cnt == tot_cnt;
}
};
仔细审题,由于每个局部倒置,一定是一个全局倒置。而要局部倒置和全局倒置的数量相等,则说明所有的全局倒置就是所有的局部倒置。
那么如何能满足所有全局倒置就是局部倒置呢?
假设在某个位置i
,出现了局部倒置nums[i] > nums[i + 1]
,我们不妨设nums[i] = A, nums[i + 1] = B
,则有A > B
。由于所有的局部倒置就是全局倒置。那么A
只能与B
形成一组倒置关系,也就是说,A
不能和B
之外的任何数再形成一组倒置关系;类似的,B
也只能与A
形成倒置关系,不能和A
之外的任何数再形成倒置关系。
我们将整个数组画出来:
??????AB?????
对于A
,由于A > B
,且A
只能和B
形成倒置,那么对于所有B
右侧的数
A
+
A^+
A+,一定有
A
+
≥
A
A^+ \ge A
A+≥A ,也就是B
右侧的所有数都不可能再和A
组成倒置关系,同样,B
左侧的所有数
B
−
B^-
B− ,一定有
B
−
≤
A
B^- \le A
B−≤A。
对于B
,其只能和A
形成倒置,那么在A
左侧的数,都一定是小于等于B
的,B
右侧的数都是大于等于B
的。
整理一下,我们设B
右侧的数为
A
+
A^+
A+,A
左侧的数为
B
−
B^-
B−,根据我们的命名就能体现出来,要使得A
和B
能组成一对唯一的倒置关系(A
不能和B
以外的任何数再组成倒置,B
也不能和A
以外的任何数再组成倒置)。只有满足
由于本身有 A > B A > B A>B,所以A一定是大于等于A左侧的所有数的( B − B^- B−),B也一定是小于等于B右侧的所有数的( A + A^+ A+),所以这两个条件我们没有列出来。
则我们遍历一次整个数组,每次判断当前的数nums[i]
是否违反上述约束即可。如何判断呢?
我们先看当前的数与前一个数的大小关系。
nums[i] >= nums[i - 1]
,说明[i - 1, i]
不构成逆序对,但我们还要判断i
是否能和i - 1
左侧的某个数构成逆序对,如何判断呢?假设[0, i - 2]
的最大值为m
,则只要nums[i] >= m
,那么nums[i]
一定是大于[0, i - 2]
的所有数的,即一定不存在逆序对。nums[i] < nums[i - 1]
,说明遇到了逆序对,则我们要判断i
除了与i - 1
构成逆序对,是否还能和[0, i - 2]
中的某个数构成逆序对,同样的,只要nums[i] >= m
,就不存在逆序对。上面只论述了i
与左侧的数是否构成逆序对,而对于i
右侧的数不必考虑,因为在遍历到右侧某个数时,会往左看,是会把i
考虑进去的。
当我们遍历到i
时,
nums[i] < nums[i - 1]
,则我们需要[0, i - 2]
的最大值m
,因为我们要判断nums[i]
和m
的大小关系;nums[i] >= nums[i - 1]
,则我们也需要[0, i - 2]
的最大值m
,因为我们要判断nums[i]
和m
的大小关系;我们用两个变量来维护这个最大值,curMax
和prevMax
。
我们在处理完i
这个位置时,更新prevMax = curMax
,并更新curMax = max(curMax, nums[i])
,这样,在下次循环处理i + 1
这个位置时,curMax
存的就是[0, i]
的最大值,prevMax
存的就是[0, i - 1]
的最大值。
代码如下
// C++ 100ms
class Solution {
public:
bool isIdealPermutation(vector<int>& nums) {
int curMax = -1, prevMax = -1;
for (int i = 0; i < nums.size(); i++) {
if (i - 1 >= 0) {
if (nums[i] >= nums[i - 1] && nums[i] < curMax) return false; // 这里 curMax 也可以替换成 prevMax
if (nums[i] < nums[i - 1] && nums[i] < prevMax) return false;
}
prevMax = curMax;
curMax = max(curMax, nums[i]);
}
return true;
}
};
注意到,这道题目的数据具有特殊性。比如题目中规定了,nums
的全部的数就是[0, n - 1]
的一个排列。
根据上面的分析,要局部倒置等于全局倒置,则数组整体来看是要单调递增的,如果出现递减,则一定是相邻两个数。并且相邻两个数互换后,一定有nums[i] = i
。则我们可以遍历整个数组,判断nums[i]
是否等于i
即可,若不等于,则一定能和i - 1
或i + 1
这两个位置的数互换,换完后得到nums[i] = i
;若无法使得nums[i] = i
,则直接return false
。
若没有这个特殊的条件,则思路二才是更加通用的做法。
// C++ 100ms
class Solution {
public:
bool isIdealPermutation(vector<int>& nums) {
for (int i = 0; i < nums.size(); i++) {
if (nums[i] != i) {
// 需要和相邻的互换
if (nums[i] == i - 1 && nums[i - 1] == i) swap(nums[i], nums[i - 1]);
else if (nums[i] == i + 1 && nums[i + 1] == i) swap(nums[i], nums[i + 1]);
else return false;
}
}
return true;
}
};