【1806. 还原排列的最少操作步数】

来源:力扣(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))) 等等,那么我们可以推出:

g^k^(i) ≡ 2^k^i (mod(n − 1))

为了让排列还原到初始值,原数组中索引为 i 的元素经过多次变换后索引变回 i,此时必须有:gk(i) ≡ 2ki ≡ i(mod(n − 1))。我们只需要找到最小的 k,使得上述等式对于 i ∈ [1, n − 1) 均成立,此时的 kk 即为所求的最小变换次数。

当 i = 1 时,我们有

g^k^(i) ≡ 2^k^ ≡ 1(mod(n − 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 互质,那么根据「欧拉定理」:

2^φ(n−1)^ ≡ 1(mod(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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千北@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值