簡體   English   中英

為什么數組大小必須是 3^k+1 才能使循環前導迭代算法工作?

[英]Why does array size have to be 3^k+1 for cycle leader iteration algorithm to work?

循環前導迭代算法是一種通過將所有偶數條目移到前面並將所有奇數條目移到后面同時保留它們的相對順序來打亂數組的算法。 例如,給定以下輸入:

a 1 b 2 c 3 d 4 e 5

輸出將是

a b c d e 1 2 3 4 5

該算法在 O(n) 時間內運行並且僅使用 O(1) 空間。

該算法的一個不尋常的細節是它的工作原理是將數組分成大小為 3 k +1 的塊。 顯然,這對於算法正常工作至關重要,但我不知道為什么會這樣。

為什么算法中必須選擇3k +1?

謝謝!

這將是一個很長的答案。 你的問題的答案並不簡單,需要一些數論才能完全回答。 我花了大約半天時間研究算法,現在我有了一個很好的答案,但我不確定我能不能簡潔地描述它。

簡短版本:

  • 將輸入分成大小為 3 k + 1 的塊實際上將輸入分成大小為 3 k - 1 的塊,這些塊由兩個最終不會移動的元素包圍。

  • 塊中剩余的 3 k -1 個元素根據一個有趣的模式移動:每個元素移動到通過將索引除以兩個模 3 k給定的位置。

  • 這種特殊的運動模式與數論和群論中稱為原始根的概念有關

  • 因為數字 2 是一個原始根模 3 k ,從數字1、3、9、27等開始,並且運行模式保證循環遍歷數組的所有元素一次,並將它們放入適當的位置.

  • 這種模式高度依賴於這樣一個事實,即對於任何 k ≥ 1,2 是 3 k的原始根。將數組的大小更改為另一個值幾乎肯定會破壞這一點,因為保留了錯誤的屬性。

長版

為了提出這個答案,我將逐步進行。 首先,我將介紹循環分解作為算法的動機,該算法將有效地以正確的順序打亂元素,但需要注意一個重要的警告。 接下來,我將指出一個有趣的特性,即當您應用這種排列時,元素是如何在數組中移動的。 然后,我會將其與稱為原始根的數論概念聯系起來,以解釋正確實現該算法所涉及的挑戰。 最后,我將解釋為什么這會導致選擇 3 k + 1 作為塊大小。

循環分解

假設您有一個數組 A 和該數組元素的排列。 按照標准的數學符號,我們將該數組的排列表示為 σ(A)。 我們可以將初始數組 A 排列在置換數組 σ(A) 的頂部,以了解每個元素的最終位置。 例如,這是一個數組及其排列之一:

   A    0 1 2 3 4
 σ(A)   2 3 0 4 1

我們可以描述排列的一種方法是列出該排列中的新元素。 然而,從算法的角度來看,將置換表示為循環分解通常更有幫助,這是一種通過顯示如何通過從初始數組開始然后循環置換其某些元素來形成該置換來寫出置換的方法。

看看上面的排列。 首先,看看 0 在哪里結束。 在 σ(A) 中,元素 0 最終取代了元素 2 原來所在的位置。 反過來,元素 2 最終取代了元素 0 原來所在的位置。 我們用 (0 2) 來表示這一點,表示 0 應該去 2 過去所在的地方,而 2 應該去 0 過去所在的地方。

現在,看看元素 1。元素 1 最終變成了 4 以前所在的位置。 然后數字 4 最終出現在 3 之前,元素 3 最終出現在 1 之前。 我們通過寫作 (1 4 3) 來表示這一點,即 1 應該到 4 以前所在的地方,4 應該到 3 以前所在的地方,而 3 應該去 1 以前所在的地方。

將這些組合在一起,我們可以將上述元素的整體排列表示為 (0 2)(1 4 3) - 我們應該交換 0 和 2,然后循環排列 1、4 和 3。如果我們從初始值開始數組,我們最終會得到我們想要的排列數組。

循環分解對於就地置換數組非常有用,因為可以在 O(C) 時間和 O(1) 輔助空間中置換任何單個循環,其中 C 是循環中的元素數。 例如,假設您有一個循環 (1 6 8 4 2)。 您可以使用如下代碼置換循環中的元素:

int[] cycle = {1, 6, 8, 4, 2};

int temp = array[cycle[0]];
for (int i = 1; i < cycle.length; i++) {
    swap(temp, array[cycle[i]]);
}
array[cycle[0]] = temp;

這只是通過交換所有東西直到一切都靜止下來。 除了存儲循環本身所需的空間使用外,它只需要 O(1) 輔助存儲空間。

通常,如果您想設計一種算法,將特定排列應用於元素數組,通常可以使用循環分解來實現。 一般算法如下:

for (each cycle in the cycle decomposition algorithm) {
   apply the above algorithm to cycle those elements;
}

該算法的整體時間和空間復雜度取決於以下因素:

  1. 我們可以多快確定我們想要的循環分解?
  2. 我們在內存中存儲循環分解的效率如何?

為了獲得解決手頭問題的 O(n) 時間、O(1) 空間算法,我們將展示有一種方法可以確定 O(1) 時間和空間中的循環分解。 由於一切都將被移動一次,因此整體運行時間將為 O(n),整體空間復雜度將為 O(1)。 正如您將看到的,到達那里並不容易,但話說回來,這也並不可怕。

排列結構

這個問題的首要目標是取一個包含 2n 個元素的數組並對其進行洗牌,以便偶數位置的元素在數組的前面結束,奇數位置的元素在數組的末尾結束。 現在讓我們假設我們有 14 個元素,如下所示:

 0  1  2  3  4  5  6  7  8  9 10 11 12 13

我們想打亂元素,使它們像這樣出來:

 0  2  4  6  8 10 12  1  3  5  7  9 11 13

關於這種排列的產生方式,我們可以進行一些有用的觀察。 首先,請注意第一個元素在這個排列中不會移動,因為偶數索引元素應該出現在數組的前面,而且它是第一個偶數索引元素。 接下來,請注意最后一個元素在這個排列中沒有移動,因為奇數索引元素應該在數組的后面結束,而且它是最后一個奇數索引元素。

這兩個觀察結果放在一起,意味着如果我們想以所需的方式排列數組的元素,我們實際上只需要排列由整個數組組成的子數組,其中第一個和最后一個元素被丟棄。 因此,接下來,我們將純粹關注置換中間元素的問題。 如果我們能解決這個問題,那么我們就解決了整個問題。

現在,讓我們只看數組的中間元素。 從我們上面的例子來看,這意味着我們將從一個這樣的數組開始:

 Element    1  2  3  4  5  6  7  8  9 10 11 12
 Index      1  2  3  4  5  6  7  8  9 10 11 12

我們想讓數組看起來像這樣:

 Element    2  4  6  8 10 12  1  3  5  7  9 11
 Index      1  2  3  4  5  6  7  8  9 10 11 12

因為這個數組是通過取一個索引為 0 的數組並切掉第一個和最后一個元素而形成的,所以我們可以將其視為一個索引數組 這對未來至關重要,因此請務必牢記這一點。

那么我們究竟如何才能產生這種排列呢? 嗯,對於初學者來說,查看每個元素並嘗試弄清楚它從哪里開始和結束並沒有什么壞處。 如果我們這樣做,我們可以這樣寫:

  • 位置 1 的元素最終出現在位置 7。
  • 位置 2 的元素最終位於位置 1。
  • 位置 3 處的元素最終出現在位置 8 處。
  • 位置 4 處的元素最終出現在位置 2 處。
  • 位置 5 處的元素最終出現在位置 9 處。
  • 位置 6 的元素最終位於位置 3。
  • 位置 7 的元素結束於位置 10。
  • 位置 8 的元素結束於位置 4。
  • 位置 9 處的元素最終出現在位置 11 處。
  • 位置 10 的元素最終位於位置 5。
  • 位置 11 處的元素在位置 12 處結束。
  • 位置 12 的元素結束於位置 6。

如果您查看此列表,您可以發現一些模式。 首先,請注意所有偶數元素的最終索引始終是該元素位置的一半。 例如,位置 4 的元素在位置 2 結束,位置 12 的元素在位置 6 結束,等等。這是有道理的 - 我們將所有偶數元素推到數組的前面,所以一半的元素出現在他們之前,他們將被轉移並移開。

現在,奇數元素呢? 嗯,總共有 12 個元素。 每個奇數元素被推送到后半部分,因此位置 2k+1 處的奇數元素將被推送到至少位置 7。它在后半部分的位置由 k 的值給出。 因此,奇數位置 2k+1 處的元素被映射到位置 7 + k。

我們可以花一點時間來概括這個想法。 假設我們要置換的數組長度為 2n。 位置 2x 的元素將映射到位置 x(同樣,偶數減半),位置 2x+1 的元素將映射到位置 n + 1 + x。 重申這一點:

位置 p 處元素的最終位置確定如下:

  • 如果對於某個整數 x,p = 2x,則 2x ↦ x
  • 如果對於某個整數 x,p = 2x+1,則 2x+1 ↦ n + 1 + x

現在我們要做一些完全瘋狂和出乎意料的事情。 現在,我們有一個分段規則來確定每個元素在哪里結束:我們要么除以 2,要么做一些涉及 n + 1 的奇怪事情。 然而,從數論的角度來看,有一個單一的、統一的規則來解釋在哪里所有元素都應該結束。

我們需要的洞察力是,在這兩種情況下,在某種程度上,我們似乎將索引除以二。 對於偶數情況,新索引實際上是通過除以二而形成的。 對於奇數的情況下,新的索引還挺看起來它是由除以二形成的(注意,2X + 1去X +(N + 1)),但是那里面有一個額外的項。 然而,在數論意義上,這兩者實際上都對應於除以二。 這是為什么。

不是取索引並除以二得到目標索引,如果我們取目標索引並乘以二呢? 如果我們這樣做,就會出現一個有趣的模式。

假設我們的原始數字是 2x。 目標是 x,如果我們將目標索引加倍以得到 2x,我們最終會得到源索引。

現在假設我們的原始數字是 2x+1。 目的地是 n + 1 + x。 現在,如果我們將目標索引加倍會發生什么? 如果我們這樣做,我們會得到 2n + 2 + 2x。 如果我們重新排列它,我們也可以將其重寫為 (2x+1) + (2n+1)。 換句話說,我們已經找回了原始索引,再加上一個額外的 (2n+1) 項。

現在是踢球者:如果我們所有的算術都以模 2n + 1 完成怎么辦? 在這種情況下,如果我們的原始數字是 2x + 1,那么目標索引的兩倍是 (2x+1) + (2n+1) = 2x + 1(模 2n+1)。 換句話說,目標索引真的是源索引的一半,只是做模2n + 1!

這給我們帶來了一個非常非常有趣的見解: 2n 元素數組中每個元素的最終目的地是通過將該數字除以 2,模 2n+1 給出的 這意味着確實有一個很好的統一規則來確定一切的去向。 我們只需要能夠除以兩個模 2n+1。 只是碰巧算出,在偶數情況下,這是正常的整數除法,而在奇數情況下,它會采用 n + 1 + x 的形式。

因此,我們可以通過以下方式重構我們的問題:給定一個 1-indexed 的 2n 個元素的數組,我們如何排列這些元素,使每個最初位於索引 x 的元素最終在位置 x/2 mod (2n+1 )?

重新審視循環分解

在這一點上,我們已經取得了很大的進步。 給定任何元素,我們知道該元素應該在哪里結束。 如果我們能找到一個很好的方法來獲得整體排列的循環分解,我們就完成了。

不幸的是,這就是事情變得復雜的地方。 例如,假設我們的數組有 10 個元素。 在這種情況下,我們希望像這樣轉換數組:

 Initial:  1  2  3  4  5  6  7  8  9 10
 Final:    2  4  6  8 10  1  3  5  7  9

這個排列的循環分解是(1 6 3 7 9 10 5 8 4 2)。 如果我們的數組有 12 個元素,我們想像這樣轉換它:

 Initial:  1  2  3  4  5  6  7  8  9 10 11 12
 Final:    2  4  6  8 10 12  1  3  5  7  9 11

這具有循環分解(1 7 10 5 9 11 12 6 3 8 4 2 1)。 如果我們的數組有 14 個元素,我們想像這樣轉換它:

 Initial:  1  2  3  4  5  6  7  8  9 10 11 12 13 14
 Final:    2  4  6  8 10 12 14  1  3  5  7  9 11 13

這有循環分解 (1 8 4 2)(3 9 12 6)(5 10)(7 11 13 14)。 如果我們的數組有 16 個元素,我們想像這樣轉換它:

 Initial:  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16
 Final:    2  4  6  8 10 12 14 16  1  3  5  7  9 11 13 15

這有循環分解 (1 9 13 15 16 8 4 2)(3 10 5 11 14 7 12 6)。

這里的問題是這些周期似乎沒有遵循任何可預測的模式。 如果我們要嘗試在 O(1) 空間和 O(n) 時間內解決這個問題,這是一個真正的問題。 即使給定任何單個元素,我們可以找出包含它的循環,並且可以有效地打亂該循環,但我們不清楚如何確定哪些元素屬於哪些循環,有多少個不同的循環等。

原始根

這就是數論的用武之地。請記住,每個元素的新位置是通過將該數字除以 2 以模 2n+1 來形成的。 反過來想,我們可以通過乘以兩個模 2n+1 來找出哪個數字將取代每個數字。 因此,我們可以通過反向查找循環分解來考慮這個問題:我們選擇一個數字,將它乘以 2 並乘以 2n+1,然后重復直到我們完成循環。

這產生了一個經過充分研究的問題。 假設我們從數字 k 開始,考慮序列 k, 2k, 2 2 k, 2 3 k, 2 4 k 等,都做了模 2n+1。 這樣做會根據您正在修改的奇數 2n+1 提供不同的模式。 這就解釋了為什么上述循環模式看起來有些隨意。

我不知道有人是怎么想出來的,但事實證明,數論中有一個很好的結果,它討論了如果你對某個數字 k 使用這個模式 mod 3 k會發生什么:

定理:考慮序列 3 s , 3 s ·2, 3 s ·2 2 , 3 s ·2 3 , 3 s ·2 4等。對於某些 k ≥ s,所有模數為 3 k 該序列循環遍歷 1 到 3 k之間的每個數字,包括 3 s整除但不能被 3 s+1整除。

我們可以在幾個例子中嘗試一下。 讓我們對 27 = 3 2 取模。 定理說,如果我們看 3、3·2、3·4 等所有的模 27,那么我們應該看到所有小於 27 的能被 3 整除但不能被 9 整除的數。好吧,讓我們看看我們得到了什么:

  • 3 · 2 0 = 3 · 1 = 3 = 3 mod 27
  • 3 · 2 1 = 3 · 2 = 6 = 6 mod 27
  • 3 · 2 2 = 3 · 4 = 12 = 12 mod 27
  • 3 · 2 3 = 3 · 8 = 24 = 24 mod 27
  • 3 · 2 4 = 3 · 16 = 48 = 21 mod 27
  • 3 · 2 5 = 3 · 32 = 96 = 15 mod 27
  • 3 · 2 6 = 3 · 64 = 192 = 3 mod 27

我們最終看到了 3、6、12、15、21 和 24(雖然不是按這個順序),它們確實是所有小於 27 的能被 3 整除但不能被 9 整除的數。

我們也可以試試這個工作模 27 並考慮 1, 2, 2 2 , 2 3 , 2 4 mod 27,我們應該看到所有小於 27 的能被 1 整除但不能被 3 整除的數。換句話說,這應該返回所有不能被 3 整除的小於 27 的數字。讓我們看看這是不是真的:

  • 2 0 = 1 = 1 模 27
  • 2 1 = 2 = 2 模 27
  • 2 2 = 4 = 4 模 27
  • 2 3 = 8 = 8 模 27
  • 2 4 = 16 = 16 mod 27
  • 2 5 = 32 = 5 模 27
  • 2 6 = 64 = 10 模 27
  • 2 7 = 128 = 20 模 27
  • 2 8 = 256 = 13 mod 27
  • 2 9 = 512 = 26 mod 27
  • 2 10 = 1024 = 25 mod 27
  • 2 11 = 2048 = 23 mod 27
  • 2 12 = 4096 = 19 mod 27
  • 2 13 = 8192 = 11 mod 27
  • 2 14 = 16384 = 22 mod 27
  • 2 15 = 32768 = 17 mod 27
  • 2 16 = 65536 = 7 mod 27
  • 2 17 = 131072 = 14 mod 27
  • 2 18 = 262144 = 1 模 27

對這些進行排序,我們得到了數字 1、2、4、5、7、8、10、11、13、14、16、17、19、20、22、23、25、26(雖然不是這個順序) . 這些正是1 到 26 之間的數字,它們不是三的倍數!

由於以下原因,該定理對算法至關重要:如果對於某個數字 k 2n+1 = 3 k ,那么如果我們處理包含 1 的循環,它將正確地打亂所有不是 3 倍數的數字。 如果我們從 3 開始循環,它將正確地打亂所有能被 3 整除但不能被 9 整除的數字。如果我們然后從 9 開始循環,它將正確地打亂所有能被 9 整除但不能被 27 整除的數字。更一般地說,如果我們對數字 1、3、9、27、81 等使用循環洗牌算法,那么我們將正確地重新定位數組中的所有元素一次,而不必擔心我們錯過任何東西。

那么這如何連接到 3 k + 1 呢? 好吧,我們需要有 2n + 1 = 3 k ,所以我們需要有 2n = 3 k - 1。但是請記住 - 當我們這樣做時,我們刪除了數組的第一個和最后一個元素! 將這些重新添加告訴我們,我們需要大小為3 k + 1 的塊才能使此過程正常工作。 如果塊是這個大小,那么我們肯定知道循環分解將由一個包含 1 的循環、一個包含 3 的非重疊循環、一個包含 9 的非重疊循環等組成,並且這些循環將包含數組的所有元素. 因此,我們可以開始循環 1、3、9、27 等,並且絕對保證一切都正確地進行了洗牌。 太棒了!

為什么這個定理是真的? 事實證明,一個數k為其中1,K,K 2,K 3等模p n至所有不屬於P的倍數的數字,周期(假定p是素數)被稱為原始根的編號 p n 有一個定理說 2 是所有數字 k 的 3 k的原始根,這就是這個技巧起作用的原因。 如果我有時間,我想回來編輯這個答案以包含這個結果的證明,但不幸的是,我的數論還沒有達到我知道如何做到這一點的水平。

概括

這個問題很有趣。 它涉及到除以奇數模數、循環分解、原始根和三的冪的可愛技巧。 我很感謝這篇 arXiv 論文,它描述了一種類似(盡管完全不同)的算法,並讓我了解了該技術背后的關鍵技巧,然后讓我計算出您描述的算法的細節。

希望這可以幫助!

這是 templatetypedef 的答案中缺少的大部分數學參數。 (剩下的比較無聊。)


引理:對於所有整數k >= 1 ,我們有2^(2*3^(k-1)) = 1 + 3^k mod 3^(k+1)

證明:通過對k進行歸納。

基本情況( k = 1 ):我們有2^(2*3^(1-1)) = 4 = 1 + 3^1 mod 3^(1+1)

歸納情況 ( k >= 2 ):如果2^(2*3^(k-2)) = 1 + 3^(k-1) mod 3^k ,則q = (2^(2*3^(k-2)) - (1 + 3^(k-1)))/3^k

 2^(2*3^(k-1)) = (2^(2*3^(k-2)))^3
               = (1 + 3^(k-1) + 3^k*q)^3
               = 1 + 3*(3^(k-1)) + 3*(3^(k-1))^2 + (3^(k-1))^3
                   + 3*(1+3^(k-1))^2*(3^k*q) + 3*(1+3^(k-1))*(3^k*q)^2 + (3^k*q)^3
               = 1 + 3^k mod 3^(k+1).

定理:對於所有整數i >= 0k >= 1 ,我們有2^i = 1 mod 3^k當且僅當i = 0 mod 2*3^(k-1)

證明:“if”方向遵循引理。 如果i = 0 mod 2*3^(k-1) ,則

2^i = (2^(2*3^(k-1)))^(i/(2*3^(k-1)))
    = (1+3^k)^(i/(2*3^(k-1))) mod 3^(k+1)
    = 1 mod 3^k.

“僅當”方向是通過對k進行歸納。

基本情況( k = 1 ):如果i != 0 mod 2 ,則i = 1 mod 2 ,並且

2^i = (2^2)^((i-1)/2)*2
    = 4^((i-1)/2)*2
    = 2 mod 3
    != 1 mod 3.

歸納情況( k >= 2 ):如果2^i = 1 mod 3^k ,則2^i = 1 mod 3^(k-1) ,歸納假設意味着i = 0 mod 2*3^(k-2) j = i/(2*3^(k-2)) 根據引理,

1 = 2^i mod 3^k
  = (1+3^(k-1))^j mod 3^k
  = 1 + j*3^(k-1) mod 3^k,

其中刪除的項可被(3^(k-1))^2整除,因此j = 0 mod 3i = 0 mod 2*3^(k-1)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM