来源:力扣(LeetCode)
描述:
给你一个偶数 n
,已知存在一个长度为 n
的排列 perm
,其中 perm[i] == i
(下标 从 0 开始 计数)。
一步操作中,你将创建一个新数组 arr
,对于每个 i
:
- 如果
i % 2 == 0
,那么arr[i] = perm[i / 2]
- 如果
i % 2 == 1
,那么arr[i] = perm[n / 2 + (i - 1) / 2]
然后将 arr
赋值给 perm
。
要想使 perm
回到排列初始值,至少需要执行多少步操作?返回最小的 非零 操作步数。
示例 1:
输入:n = 2
输出:1
解释:最初,perm = [0,1]
第 1 步操作后,perm = [0,1]
所以,仅需执行 1 步操作
示例 2:
输入:n = 4
输出:2
解释:最初,perm = [0,1,2,3]
第 1 步操作后,perm = [0,2,1,3]
第 2 步操作后,perm = [0,1,2,3]
所以,仅需执行 2 步操作
示例 3:
输入:n = 6
输出:4
提示:
- 2 <= n <= 1000
- n 是一个偶数
方法一:直接模拟
思路与算法
题目要求,一步操作中,对于每个索引 i,变换规则如下:
- 如果 i 为偶数,那么 arr[i] = perm[ i / 2 ];
- 如果 i 为奇数,那么 arr[i] = perm[ n / 2+ (i − 1) / 2 ];
然后将 arr 赋值给 perm。
我们假设初始序列 perm = [0, 1, 2, ⋯, n − 1],按照题目上述要求的变换规则进行模拟,直到 perm 重新变回为序列 [0, 1, 2, ⋯, n − 1] 为止。每次将 perm 按照上述规则变化产生数组 arr,并将 arr 赋给 perm,然后我们检测 perm 是否回到原始状态并计数,如果回到原始状态则中止变换,否则继续变换。
代码:
class Solution {
public:
int reinitializePermutation(int n) {
vector<int> perm(n), target(n);
iota(perm.begin(), perm.end(), 0);
iota(target.begin(), target.end(), 0);
int step = 0;
while (true) {
vector<int> arr(n);
for (int i = 0; i < n; i++) {
if (i & 1) {
arr[i] = perm[n / 2 + (i - 1) / 2];
} else {
arr[i] = perm[i / 2];
}
}
perm = move(arr);
step++;
if (perm == target) {
break;
}
}
return step;
}
};
执行用时:32 ms, 在所有 C++ 提交中击败了19.33%的用户
内存消耗:25.2 MB, 在所有 C++ 提交中击败了5.04%的用户
复杂度分析
时间复杂度:O(n2)O,其中 n 表示给定的元素。根据方法二的推论可以知道最多需要经过 n 次变换即可回到初始状态,每次变换需要的时间复杂度为 O(n),因此总的时间复杂度为 O(n2)。
空间复杂度:O(n),其中 n 表示给定的元素。我们需要存储每次变换中的过程变量,需要的空间为O(n)。
方法二:数学
思路与算法
我们需要观察一下原排列中对应的索引变换关系。对于原排列中第 i 个元素,设 g(i) 为进行一次操作后,该元素的新的下标,题目转化规则如下:
-
如果 g(i) 为偶数,那么 arr[g(i)] = perm[ g(i) / 2 ],令 x = g(i) / 2 ,则该等式转换为 arr[2x] = perm[x],此时 x ∈ [0, (n − 1) / 2 ];
-
如果 g(i) 为奇数,那么 arr[g(i)] = perm[ n / 2 + (g(i) − 1) / 2 ],令 x = n / 2 + (g(i) − 1) / 2 ,则该等式转换为 arr[2x − n − 1] = perm[x],此时 x ∈ [ (n + 1) / 2 , n / 2 ];
因此根据题目的转换规则可以得到以下对应关系:
-
当 0 ≤ i< (n / 2) 时,此时 g(i) = 2i;
-
当 (n / 2) ≤ i < n 时,此时 g(i) = 2i - (n - 1);
其中原排列中的第 0 和 n - 1 个元素的下标不会变换,我们无需进行考虑。 对于其余元素 i ∈ [1, n − 1),上述两个等式可以转换为对 n - 1 取模,等式可以转换为 g(i) ≡ 2i(mod(n−1))。
记 gk(i) 表示原排列 perm 中第 i 个元素经过 k 次变换后的下标,即 g2(i) = g(g(i)), g3(i) = g(g(g(i))) 等等,那么我们可以推出:
为了让排列还原到初始值,原数组中索引为 i 的元素经过多次变换后索引变回 i,此时必须有:gk(i) ≡ 2ki ≡ i(mod(n − 1))。我们只需要找到最小的 k,使得上述等式对于 i ∈ [1, n − 1) 均成立,此时的 kk 即为所求的最小变换次数。
当 i = 1 时,我们有
如果存在 k 满足上式,那么将上式两侧同乘 i,得到 gk(i) ≡ 2ki (mod(n − 1)) 即对于 i ∈ [1, n − 1) 恒成立。因此原题等价于寻找最小的 k,使得 2k ≡ 1(mod(n − 1))。
由于 n 为偶数,则 n − 1 是奇数,2 和 n - 1 互质,那么根据「欧拉定理」:
即 k = φ(n − 1) 一定是一个解,其中 φ 为「欧拉函数」。因此,最小的 k 一定小于等于 φ(n−1),而欧拉函数 φ(n − 1) ≤ n − 1,因此可以知道 k ≤ n−1 的,因此总的时间复杂度不超过 O(n)。
根据上述推论,我们直接模拟即找到最小的 k 使得满足 2k ≡ 1(mod(n − 1)) 即可。
代码:
class Solution {
public:
int reinitializePermutation(int n) {
if (n == 2) {
return 1;
}
int step = 1, pow2 = 2;
while (pow2 != 1) {
step++;
pow2 = pow2 * 2 % (n - 1);
}
return step;
}
};
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:5.7 MB, 在所有 C++ 提交中击败了87.40%的用户
复杂度分析
时间复杂度:O(n),其中 n 表示给定的元素。根据推论可以知道最多需要进行计算的次数不超过 n,因此时间复杂度为 O(n)。
空间复杂度:O(1)。
author:LeetCode-Solution