[1806] LeetCode 刷题笔记: 还原排列的最少操作步数 [M]

[1806] LeetCode 刷题笔记: 还原排列的最少操作步数 [M]

题目描述

给你一个偶数 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 回到排列初始值,至少需要执行多少步操作?返回最小的 非零 操作步数。

题解参考

直接模拟

题目要求,一步操作中,对于每个索引 ii,变换规则如下:

如果 i 为偶数,那么 arr[i] = perm[i / 2]
如果 i 为奇数,那么 arr[i] = permp[n / 2 + (i-1) / 2]
然后将 arr 赋值给 perm

我们假设初始序列 perm=[0,1,2,...,n−1],按照题目上述要求的变换规则进行模拟,直到 perm 重新变回为序列 [0,1,2,⋯,n−1] 为止。每次将 perm 按照上述规则变化产生数组 arr,并将 arr 赋给 perm,然后我们检测 perm 是否回到原始状态并计数,如果回到原始状态则中止变换,否则继续变换。

Algorithm [  还原排列的最少操作步数 : 直接模拟]
Input:  { number /* You are given an even integer n */ }
Output: { Return the minimum non-zero number of operations you need to perform on perm to return the permutation to its initial value. }
-------------------------------
/* You are given an even integer n​​​​​​. You initially have a permutation perm of size n​​ where perm[i] == i​ (0-indexed)​​​​. */
Function SimulationMethod
  perm[n:0], target[n:0] /* Init the two array, n items */
  step <- 0
  While True
    arr[n:0] /* Init the temp array */
    For i <- 0 to n
      If i & 1
        arr[i] <- perm[n /2 + (i - 1) / 2]
      Else
        arr[i] <- perm[i /2]
      EndIf
    EndFor
    perm = CppMove(arr)
    step++ /* increase the step */
    If target == perm
      Break
    EndIf
  EndWhile
  Return step
EndFunction

复杂度分析

  • 时间复杂度:\(O(n^2)\),其中 \(n\) 表示给定的元素。根据方法二的推论可以知道最多需要经过 \(n\) 次变换即可回到初始状态,每次变换需要的时间复杂度为 \(O(n)\),因此总的时间复杂度为 \(O(n^2)\)

  • 空间复杂度:\(O(n)\),其中 \(n\) 表示给定的元素。我们需要存储每次变换中的过程变量,需要的空间为 \(O(n)\)

数学

我们需要观察一下原排列中对应的索引变换关系。对于原排列中第 ``i 个元素,设 g(i) 为进行一次操作后,该元素的新的下标,题目转化规则如下:

  • 如果 g(i) 为偶数,那么 \(arr[g(i)]=perm[\frac{g(i)}{2}]\),令 \(x = \frac{g(i)}{2}\),则该等式转换为 \(arr[2x] = perm[x]\),此时 \(x\in[0,\frac{n-1}{2}]\)

  • 如果 g(i) 为奇数,那么 \(arr[g(i)] = perm[\frac{n}{2} + \frac{g(i)-1}{2}]\),令 \(x = \frac{n}{2} + \frac{g(i)-1}{2}\),则该等式转换为 \(arr[2x-n-1] = perm[x]\),此时 \(x \in[\frac{n+1}{2},\frac{n}{2}]\)

因此根据题目的转换规则可以得到以下对应关系:

  • \(0\le i < \frac{n}{2}\) 时,此时 \(g(i) = 2i\)

  • \(\frac{n}{2} \le i < n\)时,此时 \(g(i) = 2i-(n-1)\)

其中原排列中的第 0n−1 个元素的下标不会变换,我们无需进行考虑。 对于其余元素 \(i \in [1, n-1)\),上述两个等式可以转换为对 \(n−1\) 取模,等式可以转换为 \(g(i) \equiv 2i \mod {(n-1)}\)

\(g^k(i)\)表示原排列 perm 中第 i 个元素经过 k 次变换后的下标,即 \(g^2(i) = g(g(i))\), \(g^3(i) = g(g(g(i)))\)等等,那么我们可以推出:

\[g^k(i) \equiv 2^ki \mod {(n-1)} \]

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

\(i=1\) 时,我们有

\[g^k(1) \equiv 2^k \equiv 1 \mod {(n-1)} \]

如果存在 k 满足上式,那么将上式两侧同乘 i,得到 \(g^k(i) \equiv 2^ki \equiv i \mod {(n-1)}\) 即对于 \(i \in [1, n-1)\) 恒成立。因此原题等价于寻找最小的 k,使得 \(2^k \equiv 1 \mod {(n-1)}\)

由于 n 为偶数,则 n-1 是奇数,2n-1 互质,那么根据「欧拉定理」:

\[2^{\varphi(n-1)} \equiv 1 \mod {(n-1)} \]

\(k=\varphi(n-1)\) 一定是一个解,其中 \(\varphi\) 为「欧拉函数」。因此,最小的 k 一定小于等于 \(\varphi(n-1)\),而欧拉函数 \(\varphi(n-1) \le n -1\),因此可以知道 \(k \le n - 1\) 的,因此总的时间复杂度不超过 \(O(n)\)

根据上述推论,我们直接模拟即找到最小的 k 使得满足 \(2^k \equiv 1 \pmod {(n-1)}\) 即可。

Algorithm [  还原排列的最少操作步数 : 数学 ]
Input:  { number /* You are given an even integer n */ }
Output: { Return the minimum non-zero number of operations you need to perform on perm to return the permutation to its initial value. }
-------------------------------
/* You are given an even integer n​​​​​​. You initially have a permutation perm of size n​​ where perm[i] == i​ (0-indexed)​​​​. */
Function MathMethod
  If 2 == number
    Return 1
  EndIf
  step <- 1, pow2 <- 2
  While 1 != pow2
    step++ /* increase the step */
    pow2 <- pow2 * 2 Mod (n - 1)
  EndWhale
  Retrun step
EndFunction

复杂度分析

  • 时间复杂度:\(O(n)\),其中 \(n\) 表示给定的元素。根据推论可以知道最多需要进行计算的次数不超过 \(n\),因此时间复杂度为 \(O(n)\)

  • 空间复杂度:\(O(1)\)

参考题解

C/C++ 的相关参考

C/C++ 的相关的代码
class Solution {
    struct Simulation {};
    struct Math {};
public:
    int reinitializePermutation(int n) {
        return reinitializePermutationImpl(Math(), n);
    }
    int reinitializePermutationImpl(Simulation, 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;
    }
    int reinitializePermutationImpl(Math, int n) {
        if (n == 2) {
            return 1;
        }
        int step = 1, pow2 = 2;
        while (pow2 != 1) {
            step++;
            pow2 = pow2 * 2 % (n - 1);
        }
        return step;
    }
};

Rust 的相关参考

Rust 的相关的代码
impl Math for Solution {}

trait Math {
    fn reinitialize_permutation(n: i32) -> i32 {
        let (mut cnt, mut idx) = (1, n / 2);
        while idx != 1 {
            idx = if idx & 1 == 1 { n / 2 + (idx - 1) / 2 } else { idx / 2 };
            cnt += 1;
        }
        cnt
    }
}
posted @ 2023-01-10 17:08  杗玉  阅读(100)  评论(0)    收藏  举报