第三十四~三十五章:格子取數,完美洗牌算法
時間:二零一三年八月二十三日。
題記
再過一個半月,即到2013年10月11日,便是本博客開通3週年之際,巧的是,那天剛好也是我的25歲生日。寫博近3年,訪問量趨近500萬,無法確切知道幫助了多少人影響了多少人,但有些文章和一些系列是我比較喜歡的,如這三篇:從B樹、B+樹、B*樹談到R 樹;教你如何迅速秒殺掉:99%的海量數據處理面試題;支持向量機通俗導論(理解SVM的三層境界)。
- 第三十四章:格子取數問題;
- 第三十五章:完美洗牌算法的變形
第三十四章、格子取數問題
- 如果按照上面的局部貪優走法,那麼第一次勢必會如圖二那樣走,導致的結果是第二次要麼取到2,要麼取到3,
- 但若不按照上面的局部貪優走法,那麼第一次可以如圖三那樣走,從而第二次走的時候能取到2 4 4,很顯然,這種走法求得的最終SUM值更大;
解法一、直接搜索
//copyright@西芹_new 2013
#include "stdafx.h"
#include <iostream>
using namespace std;
#define N 5
int map[5][5]={
{2,0,8,0,2},
{0,0,0,0,0},
{0,3,2,0,0},
{0,0,0,0,0},
{2,0,8,0,2}};
int sumMax=0;
int p1x=0;
int p1y=0;
int p2x=0;
int p2y=0;
int curMax=0;
void dfs( int index){
if( index == 2*N-2){
if( curMax>sumMax)
sumMax = curMax;
return;
}
if( !(p1x==0 && p1y==0) && !(p2x==N-1 && p2y==N-1))
{
if( p1x>= p2x && p1y >= p2y )
return;
}
//right right
if( p1x+1<N && p2x+1<N ){
p1x++;p2x++;
int sum = map[p1x][p1y]+map[p2x][p2y];
curMax += sum;
dfs(index+1);
curMax -= sum;
p1x--;p2x--;
}
//down down
if( p1y+1<N && p2y+1<N ){
p1y++;p2y++;
int sum = map[p1x][p1y]+map[p2x][p2y];
curMax += sum;
dfs(index+1);
curMax -= sum;
p1y--;p2y--;
}
//rd
if( p1x+1<N && p2y+1<N ) {
p1x++;p2y++;
int sum = map[p1x][p1y]+map[p2x][p2y];
curMax += sum;
dfs(index+1);
curMax -= sum;
p1x--;p2y--;
}
//dr
if( p1y+1<N && p2x+1<N ) {
p1y++;p2x++;
int sum = map[p1x][p1y]+map[p2x][p2y];
curMax += sum;
dfs(index+1);
curMax -= sum;
p1y--;p2x--;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
curMax = map[0][0];
dfs(0);
cout <<sumMax-map[N-1][N-1]<<endl;
return 0;
}
解法二、動態規劃
上述解法一的搜索解法是的時間複雜度是指數型的,如果是隻走一次的話,是經典的dp。
2.1、DP思路詳解
故正如@綠色夾克衫所說:此題也可以用動態規劃求解,主要思路就是同時DP 2次所走的狀態。
1、先來分析一下這個問題,爲了方便討論,先對矩陣做一個編號,且以5*5的矩陣爲例(給這個矩陣起個名字叫M1):
M1
0 1 2 3 4
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
4 5 6 7 8
從左上(0)走到右下(8)共需要走8步(2*5-2)。我們設所走的步數爲s。因爲限定了只能向右和向下走,因此無論如何走,經過8步後(s = 8)都將走到右下。而DP的狀態也是依據所走的步數來記錄的。
再來分析一下經過其他s步後所處的位置,根據上面的討論,可以知道:
- 經過8步後,一定處於右下角(8);
- 那麼經過5步後(s = 5),肯定會處於編號爲5的位置;
- 3步後肯定處於編號爲3的位置;
- s = 4的時候,處於編號爲4的位置,此時對於方格中,共有5(相當於n)個不同的位置,也是所有編號中最多的。
故推廣來說,對於n*n的方格,總共需要走2n - 2步,且當s = n - 1時,編號爲n個,也是編號數最多的。
如果用DP[s,i,j]來記錄2次所走的狀態獲得的最大值,其中s表示走s步,i和j分別表示在s步後第1趟走的位置和第2趟走的位置。
2、爲了方便描述,再對矩陣做一個編號(給這個矩陣起個名字叫M2):
M2
0 0 0 0 0
1 1 1 1 1
2 2 2 2 2
3 3 3 3 3
4 4 4 4 4
把之前定的M1矩陣也再貼下:
M1
0 1 2 3 4
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
4 5 6 7 8
我們先看M1,在經過6步後,肯定處於M1中編號爲6的位置。而M1中共有3個編號爲6的,它們分別對應M2中的2 3 4。故對於M2來說,假設第1次經過6步走到了M2中的2,第2次經過6步走到了M2中的4,DP[s,i,j] 則對應 DP[6,2,4]。由於s = 2n - 2,0 <= i<= <= j <= n,所以這個DP共有O(n^3)個狀態。
M1
0 1 2 3 4
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
4 5 6 7 8
再來分析一下狀態轉移,以DP[6,2,3]爲例(就是上面M1中加粗的部分),可以到達DP[6,2,3]的狀態包括DP[5,1,2],DP[5,1,3],DP[5,2,2],DP[5,2,3]。
3、下面,我們就來看看這幾個狀態:DP[5,1,2],DP[5,1,3],DP[5,2,2],DP[5,2,3],用加粗表示位置DP[5,1,2] DP[5,1,3] DP[5,2,2] DP[5,2,3] (加紅表示要達到的狀態DP[6,2,3])
0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4
1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5
2 3 4 5 6 2 3 4 5 6 2 3 4 5 6 2 3 4 5 6
3 4 5 6 7 3 4 5 6 7 3 4 5 6 7 3 4 5 6 7
4 5 6 7 8 4 5 6 7 8 4 5 6 7 8 4 5 6 7 8
因此:
DP[6,2,3] = Max(DP[5,1,2] ,DP[5,1,3],DP[5,2,2],DP[5,2,3]) + 6,2和6,3格子中對應的數值 (式一)
上面(式一)所示的這個遞推看起來沒有涉及:“如果兩次經過同一個格子,那麼該數只加一次的這個條件”,討論這個條件需要換一個例子,以DP[6,2,2]爲例:DP[6,2,2]可以由DP[5,1,1],DP[5,1,2],DP[5,2,2]到達,但由於i = j,也就是2次走到同一個格子,那麼數值只能加1次。
所以當i = j時,
DP[6,2,2] = Max(DP[5,1,1],DP[5,1,2],DP[5,2,2]) + 6,2格子中對應的數值 (式二)
4、故,綜合上述的(式一),(式二)最後的遞推式就是
if(i != j)
DP[s, i ,j] = Max(DP[s - 1, i - 1, j - 1], DP[s - 1, i - 1, j], DP[s - 1, i, j - 1], DP[s - 1, i, j]) + W[s,i] + W[s,j]
else
DP[s, i ,j] = Max(DP[s - 1, i - 1, j - 1], DP[s - 1, i - 1, j], DP[s - 1, i, j]) + W[s,i]
2.2、DP方法實現
//copyright@caopengcs 2013
const int N = 202;
const int inf = 1000000000; //無窮大
int dp[N * 2][N][N];
bool isValid(int step,int x1,int x2,int n) { //判斷狀態是否合法
int y1 = step - x1, y2 = step - x2;
return ((x1 >= 0) && (x1 < n) && (x2 >= 0) && (x2 < n) && (y1 >= 0) && (y1 < n) && (y2 >= 0) && (y2 < n));
}
int getValue(int step, int x1,int x2,int n) { //處理越界 不存在的位置 給負無窮的值
return isValid(step, x1, x2, n)?dp[step][x1][x2]:(-inf);
}
//狀態表示dp[step][i][j] 並且i <= j, 第step步 兩個人分別在第i行和第j行的最大得分 時間複雜度O(n^3) 空間複雜度O(n^3)
int getAnswer(int a[N][N],int n) {
int P = n * 2 - 2; //最終的步數
int i,j,step;
//不能到達的位置 設置爲負無窮大
for (i = 0; i < n; ++i) {
for (j = i; j < n; ++j) {
dp[0][i][j] = -inf;
}
}
dp[0][0][0] = a[0][0];
for (step = 1; step <= P; ++step) {
for (i = 0; i < n; ++i) {
for (j = i; j < n; ++j) {
dp[step][i][j] = -inf;
if (!isValid(step, i, j, n)) { //非法位置
continue;
}
//對於合法的位置進行dp
if (i != j) {
dp[step][i][j] = max(dp[step][i][j], getValue(step - 1, i - 1, j - 1, n));
dp[step][i][j] = max(dp[step][i][j], getValue(step - 1, i - 1, j, n));
dp[step][i][j] = max(dp[step][i][j], getValue(step - 1, i, j - 1, n));
dp[step][i][j] = max(dp[step][i][j], getValue(step - 1, i, j,n));
dp[step][i][j] += a[i][step - i] + a[j][step - j]; //不在同一個格子,加兩個數
}
else {
dp[step][i][j] = max(dp[step][i][j], getValue(step - 1, i - 1, j - 1, n));
dp[step][i][j] = max(dp[step][i][j], getValue(step - 1, i - 1, j, n));
dp[step][i][j] = max(dp[step][i][j], getValue(step - 1, i, j, n));
dp[step][i][j] += a[i][step - i]; // 在同一個格子裏,只能加一次
}
}
}
}
return dp[P][n - 1][n- 1];
}
複雜度分析:狀態轉移最多需要統計4個變量的情況,看做是O(1)的,共有O(n^3)個狀態,所以總的時間複雜度是O(n^3)的,且dp數組開了N^3大小,故其空間複雜度亦爲O(n^3)。
2.3、DP實現優化版
即我們在推算dp[step]的時候,只依靠它上一次的狀態dp[step - 1],所以dp數組的第一維,我們只開到2就可以了。即step爲奇數時,我們用dp[1][i][j]表示狀態,step爲偶數我們用dp[0][i][j]表示狀態,這樣我們只需要O(n^2)的空間,這就是滾動數組的方法。滾動數組寫起來並不複雜,只需要對上面的代碼稍作修改即可,優化後的代碼如下:
//copyright@caopengcs 8/24/2013
int dp[2][N][N];
bool isValid(int step,int x1,int x2,int n) { //判斷狀態是否合法
int y1 = step - x1, y2 = step - x2;
return ((x1 >= 0) && (x1 < n) && (x2 >= 0) && (x2 < n) && (y1 >= 0) && (y1 < n) && (y2 >= 0) && (y2 < n));
}
int getValue(int step, int x1,int x2,int n) { //處理越界 不存在的位置 給負無窮的值
return isValid(step, x1, x2, n)?dp[step % 2][x1][x2]:(-inf);
}
//狀態表示dp[step][i][j] 並且i <= j, 第step步 兩個人分別在第i行和第j行的最大得分 時間複雜度O(n^3) 使用滾動數組 空間複雜度O(n^2)
int getAnswer(int a[N][N],int n) {
int P = n * 2 - 2; //最終的步數
int i,j,step,s;
//不能到達的位置 設置爲負無窮大
for (i = 0; i < n; ++i) {
for (j = i; j < n; ++j) {
dp[0][i][j] = -inf;
}
}
dp[0][0][0] = a[0][0];
for (step = 1; step <= P; ++step) {
for (i = 0; i < n; ++i) {
for (j = i; j < n; ++j) {
dp[step][i][j] = -inf;
if (!isValid(step, i, j, n)) { //非法位置
continue;
}
s = step % 2; //狀態下表標
//對於合法的位置進行dp
if (i != j) {
dp[s][i][j] = max(dp[s][i][j], getValue(step - 1, i - 1, j - 1, n));
dp[s][i][j] = max(dp[s][i][j], getValue(step - 1, i - 1, j, n));
dp[s][i][j] = max(dp[s][i][j], getValue(step - 1, i, j - 1, n));
dp[s][i][j] = max(dp[s][i][j], getValue(step - 1, i, j,n));
dp[s][i][j] += a[i][step - i] + a[j][step - j]; //不在同一個格子,加兩個數
}
else {
dp[s][i][j] = max(dp[s][i][j], getValue(step - 1, i - 1, j - 1, n));
dp[s][i][j] = max(dp[s][i][j], getValue(step - 1, i - 1, j, n));
dp[s][i][j] = max(dp[s][i][j], getValue(step - 1, i, j, n));
dp[s][i][j] += a[i][step - i]; // 在同一個格子裏,只能加一次
}
}
}
}
return dp[P % 2][n - 1][n- 1];
}
本第34章分析完畢。第三十五章、完美洗牌算法
解法一、蠻力變換
a1,a2,a3,a4,b1,b2,b3,b4
a1,b1,a2,b2,a3,b3,a4,b4
1.1、步步前移
a1,b1,a2,a3,a4,b2,b3,b4
a1,b1,a2,b2,a3,a4,b3,b4
a1,b1,a2,b2,a3,b3,a4,b4
1.2、中間交換
a1,a2,a3,b1,a4,b2,b3,b4
a1,a2,b1,a3,b2,a4,b3,b4
a1,b1,a2,b2,a3,b3,a4,b4
同樣,此法同解法1.1、步步前移一樣,時間複雜度依然爲O(N^2),我們得下點力氣了。
解法二、完美洗牌算法
a1,a2,a3,...an,b1,b2,b3..bn
b1,a1,b2,a2,b3,a3... bn,an
a1,b1,a2,b2,a3,b3....,an,bn
2.1、位置置換pefect_shuffle1算法
數組下標:1 2 3 4 5 6 7 8
最終序列:b1 a1 b2 a2 b3 a3 b4 a4
從上面的例子我們能看到,前n個元素中,
- 第1個元素a1到了原第2個元素a2的位置,即1->2;
- 第2個元素a2到了原第4個元素a4的位置,即2->4;
- 第3個元素a3到了原第6個元素b2的位置,即3->6;
- 第4個元素a4到了原第8個元素b4的位置,即4->8;
- 第5個元素b1到了原第1個元素a1的位置,即5->1;
- 第6個元素b2到了原第3個元素a3的位置,即6->3;
- 第7個元素b3到了原第5個元素b1的位置,即7->5;
- 第8個元素b4到了原第7個元素b3的位置,即8->7;
- 當0< i <n時, 原式= (2i) % (2 * n + 1) = 2i;
- 當i>n時,原式(2 * i) % (2 * n + 1)保持不變。
// 時間O(n),空間O(n) 數組下標從1開始
void pefect_shuffle1(int *a,int n) {
int n2 = n * 2, i, b[N];
for (i = 1; i <= n2; ++i) {
b[(i * 2) % (n2 + 1)] = a[i];
}
for (i = 1; i <= n2; ++i) {
a[i] = b[i];
}
}
但很明顯,它的時間複雜度雖然是O(n),但其空間複雜度卻是O(n),仍不符合本題所期待的時間O(n),空間O(1)。我們繼續尋找更優的解法。- 一個是1 -> 2 -> 4 -> 8 -> 7 -> 5 -> 1;
- 一個是3 -> 6 -> 3。
2.2、分而治之perfect_shuffle2算法
原始數組的下標:1....2n,即(1 .. n/2, n/2+1..n)(n+1 .. n+n/2, n+n/2+1 .. 2n)
前半段(1 .. n/2, n/2+1..n)和後半段(n+1 .. n+n/2, n+n/2+1 .. 2n)的長度皆爲n。
新的前n個元素A:(1..n/2 n+1.. n+n/2)
新的後n個元素B:(n/2+1 .. n n+n/2+1 .. 2n)
a1 a2 a3 a4 b1 b2 b3 b4
a1 a2 b1 b2 a3 a4 b3 b4
a1 a2 a3 a4 a5 b1 b2 b3 b4 b5
a1 a2 a3 a4 b1 b2 b3 b4 b5 a5
//copyright@caopengcs 8/23/2013
//時間O(nlogn) 空間O(1) 數組下標從1開始
void perfect_shuffle2(int *a,int n) {
int t,i;
if (n == 1) {
t = a[1];
a[1] = a[2];
a[2] = t;
return;
}
int n2 = n * 2, n3 = n / 2;
if (n % 2 == 1) { //奇數的處理
t = a[n];
for (i = n + 1; i <= n2; ++i) {
a[i - 1] = a[i];
}
a[n2] = t;
--n;
}
//到此n是偶數
for (i = n3 + 1; i <= n; ++i) {
t = a[i];
a[i] = a[i + n3];
a[i + n3] = t;
}
// [1.. n /2]
perfect_shuffle2(a, n3);
perfect_shuffle2(a + n, n3);
}
分析下此算法的複雜度: 每次,我們交換中間的n個元素,需要O(n)的時間,n是奇數的話,我們還需要O(n)的時間先把後兩個元素調整好,但這不影響總體時間複雜度。2.3、完美洗牌算法perfect_shuffle3
2.3.1、走圈算法cycle_leader
數組下標:1 2 3 4 5 6 7 8
最終序列:b1 a1 b2 a2 b3 a3 b4 a4
“於此同時,我也提醒下讀者,根據上面變換的節奏,我們可以看出有兩個圈,
- 一個是1 -> 2 -> 4 -> 8 -> 7 -> 5 -> 1;
- 一個是3 -> 6 -> 3。”
第一個圈:1 -> 2 -> 4 -> 8 -> 7 -> 5 -> 1
第二個圈:3 -> 6 -> 3:
原始數組:1 2 3 4 5 6 7 8
數組小標:1 2 3 4 5 6 7 8
走第一圈:5 1 3 2 7 6 8 4
走第二圈:5 1 6 2 7 3 8 4
//數組下標從1開始,from是圈的頭部,mod是要取模的數 mod 應該爲 2 * n + 1,時間複雜度O(圈長)
void cycle_leader(int *a,int from, int mod) {
int last = a[from],t,i;
for (i = from * 2 % mod;i != from; i = i * 2 % mod) {
t = a[i];
a[i] = last;
last = t;
}
a[from] = last;
}
2.3.2、神級結論:若2*n=(3^k - 1),則可確定圈的個數及各自頭部的起始位置
- 對於2*n = (3^k-1)這種長度的數組,恰好只有k個圈,且每個圈頭部的起始位置分別是1,3,9,...3^(k-1)。
也就是說,利用上述這個結論,我們可以解決這種特殊長度2*n = (3^k-1)的數組問題,那麼若給定的長度n是任意的咋辦呢?此時,我們可以借鑑2.2節、分而治之算法的思想,把整個數組一分爲二,即拆分成兩個部分:
- 讓一部分的長度滿足神級結論:若2*m = (3^k-1),則恰好k個圈,且每個圈頭部的起始位置分別是1,3,9,...3^(k-1)。其中m<n,m往神級結論所需的值上套;
- 剩下的n-m部分單獨計算;
當把n分解成m和n-m兩部分後,原始數組對應的下標如下(爲了方便描述,我們依然只需要看數組下標就夠了):
原始數組下標:1..m m+1.. n, n+1 .. n+m, n+m+1,..2*n
參照之前2.2節、分而治之算法的思路,且更爲了能讓前部分的序列滿足神級結論2*m = (3^k-1),我們可以把中間那兩段長度爲n-m和m的段交換位置,即相當於把m+1..n,n+1..n+m的段循環右移m次(爲什麼要這麼做?因爲如此操作後,數組的前部分的長度爲2m,而根據神級結論:當2m=3^k-1時,可知這長度2m的部分恰好有k個圈)。
而如果讀者看過本系列第一章、左旋轉字符串的話,就應該意識到循環位移是有O(N)的算法的,其思想即是把前n-m個元素(m+1.. n)和後m個元素(n+1 .. n+m)先各自翻轉一下,再將整個段(m+1.. n, n+1 .. n+m)翻轉下。
這個翻轉的代碼如下:
//翻轉字符串時間複雜度O(to - from)
void reverse(int *a,int from,int to) {
int t;
for (; from < to; ++from, --to) {
t = a[from];
a[from] = a[to];
a[to] = t;
}
}
//循環右移num位 時間複雜度O(n)
void right_rotate(int *a,int num,int n) {
reverse(a, 1, n - num);
reverse(a, n - num + 1,n);
reverse(a, 1, n);
}
翻轉後,得到的目標數組的下標爲:
目標數組下標:1..m n+1..n+m m+1 .. n n+m+1,..2*n
OK,理論講清楚了,再舉個例子便會更加一目瞭然。當給定n=7時,若要滿足神級結論2*n=3^k-1,k只能取2,繼而推得n‘=m=4。
原始數組:a1 a2 a3 a4 a5 a6 a7 b1 b2 b3 b4 b5 b6 b7
既然m=4,即讓上述數組中有下劃線的兩個部分交換,得到:
目標數組:a1 a2 a3 a4 b1 b2 b3 b4 a5 a6 a7 b5 b6 b7
繼而目標數組中的前半部分a1 a2 a3 a4 b1 b2 b3 b4部分可以用2.3.1、走圈算法cycle_leader搞定,於此我們最終求解的n長度變成了n’=3,即n的長度減小了4,單獨再解決後半部分a5 a6 a7 b5 b6 b7即可。
2.3.3、完美洗牌算法perfect_shuffle3
從上文的分析過程中也就得出了我們的完美洗牌算法,其算法流程爲:
- 輸入數組 A[1..2 * n]
- step 1 找到 2 * m = 3^k - 1 使得 3^k <= 2 * n < 3^(k +1)
- step 2 把a[m + 1..n + m]那部分循環移m位
- step 3 對每個i = 0,1,2..k - 1,3^i是個圈的頭部,做cycle_leader算法,數組長度爲m,所以對2 * m + 1取模。
- step 4 對數組的後面部分A[2 * m + 1.. 2 * n]繼續使用本算法, 這相當於n減小了m。
以上各個步驟對應的時間複雜度分析如下:
- 因爲循環不斷乘3的,所以時間複雜度O(logn)
- 循環移位O(n)
- 每個圈,每個元素只走了一次,一共2*m個元素,所以複雜度omega(m), 而m < n,所以 也在O(n)內。
- T(n - m)
此完美洗牌算法實現的參考代碼如下:
//copyright@caopengcs 8/24/2013
//時間O(n),空間O(1)
void perfect_shuffle3(int *a,int n) {
int n2, m, i, k,t;
for (;n > 1;) {
// step 1
n2 = n * 2;
for (k = 0, m = 1; n2 / m >= 3; ++k, m *= 3)
;
m /= 2;
// 2m = 3^k - 1 , 3^k <= 2n < 3^(k + 1)
// step 2
right_rotate(a + m, m, n);
// step 3
for (i = 0, t = 1; i < k; ++i, t *= 3) {
cycle_leader(a , t, m * 2 + 1);
}
//step 4
a += m * 2;
n -= m;
}
// n = 1
t = a[1];
a[1] = a[2];
a[2] = t;
}
2.3.4、perfect_shuffle3算法解決其變形問題
啊哈!以上代碼即解決了完美洗牌問題,那麼針對本章要解決的其變形問題呢?是的,如本章開頭所說,在完美洗牌問題的基礎上對它最後的序列swap兩兩相鄰元素即可,代碼如下:
//copyright@caopengcs 8/24/2013
//時間複雜度O(n),空間複雜度O(1),數組下標從1開始,調用perfect_shuffle3
void shuffle(int *a,int n) {
int i,t,n2 = n * 2;
perfect_shuffle3(a,n);
for (i = 2; i <= n2; i += 2) {
t = a[i - 1];
a[i - 1] = a[i];
a[i] = t;
}
}
上述的這個“在完美洗牌問題的基礎上對它最後的序列swap兩兩相鄰元素”的操作(當然,你也可以讓原數組第一個和最後一個不變,中間的2 * (n - 1)項用原始的標準完美洗牌算法做),只是在完美洗牌問題時間複雜度O(N)空間複雜度O(1)的基礎上再增加O(N)的時間複雜度,故總的時間複雜度O(N)不變,且理所當然的保持了空間複雜度O(1)。至此,咱們的問題得到了圓滿解決!
2.3.5、神級結論是如何來的?
我們的問題得到了解決,但本章尚未完,即決定完美洗牌算法的神級結論:若2*n=(3^k - 1),則恰好只有k個圈,且每個圈頭部的起始位置分別是1,3,9,...3^(k-1),是如何來的呢?
要證明這個結論的關鍵就是:這所有的圈合併起來必須包含從1到M之間的所有證書,一個都不能少。這個證明有點麻煩,因爲證明過程中會涉及到羣論等數論知識,但再遠的路一步步走也能到達。
首先,讓咱們明確以下相關的概念,定理,及定義(搞清楚了這些東西,咱們便證明了一大半):
- 概念1 mod表示對一個數取餘數,比如3 mod 5 =3,5 mod 3 =2;
- 定義1 歐拉函數ϕ(m) 表示爲不超過m(即小於等於m)的數中,與m互素的正整數個數
- 定義2 若ϕ(m)=Ordm(a) 則稱a爲m的原根,其中Ordm(a)定義爲:a ^d ( mod m),其中d=0,1,2,3…,但取讓等式成立的最小的那個d。
結合上述定義1、定義2可知,2是3的原根,因爲2^0 mod 3 = 1, 2^1 mod 3 = 2, 2^2 mod 3 = 1, 2^3 mod 3 = 2,{a^0 mod m,a^1 mod m,a^2}得到集合S={1,2},包含了所有和3互質的數,也即d=ϕ(3)=2,滿足原根定義。
而2不是7的原根,這是因爲2^0 mod 7 = 1, 2^1 mod 7 = 2, 2^2 mod 7 = 4, 2^3 mod 7 = 1,2^4 mod 7 = 2,2^5 mod 7 = 4,2^6 mod 7 = 1,從而集合S={1,2,4}中始終只有1、2、4三種結果,而沒包含全部與7互質的數(3、6、5便不包括),,即d=3,但ϕ(7)=6,從而d != ϕ(7),不滿足原根定義。
再者,如果說一個數a,是另外一個數m的原根,代表集合S = {a^0 mod m, a^1 mod m, a^2 mod m…… },得到的集合包含了所有小於m並且與m互質的數,否則a便不是m的原根。而且集合S = {a^0 mod m, a^1 mod m, a^2 mod m…… }中可能會存在重複的餘數,但當a與m互質的時候,得到的{a^0 mod m, a^1 mod m, a^2 mod m}集合中,保證了第一個數是a^0 mod m,故第一次發現重複的數時,這個重複的數一定是1,也就是說,出現餘數循環一定是從開頭開始循環的。
- 定義3 對模指數,a對模m的原根定義爲 ,st:中最小的正整數d
再比如,2是9的原根,因爲,爲了讓除以9的餘數恆等於1,可知最小的正整數d=6,而ϕ(m)=6,滿足原根的定義。
- 定理1 同餘定理:兩個整數a,b,若它們除以正整數m所得的餘數相等,則稱a,b對於模m同餘,記作,讀做a與b關於模m同餘。
- 定理2 當p爲奇素數且a是的原根時⇒ a也是的原根
- 定理3 費馬小定理:如果a和m互質,那麼a^ϕ(m) mod m = 1
- 定理4 若(a,m)=1 且a爲m的原根,那麼a是(Z/mZ)*的生成元。
我們知道2是3的原根,2是9的原根,我們定義S(k)表示上述的集合S,並且取x = 3^k(x表示爲集合S中的數)。
所以:
S(1) = {1, 2}
S(2) = {1, 2, 4, 8, 7, 5}
也就是說S(k - t)裏每個數x* 3^t形成的新集合包含了所有與3^k的最大公約數爲3^t的數,它也是一個圈,原先圈的頭部是1,這個圈的頭部是3^t。
於是,對所有的小於 3^k的數,根據它和3^k的最大公約數,我們都把它分配到了一個圈裏去了,且k個圈包含了所有的小於3^k的數。
下面,舉個例子,如caopengcs所說,當我們取“a = 2, m = 3時,
所以S(1) = {1, 2}
S(2) = {1, 2, 4, 8, 7, 5}
- S(3) = {1, 2 ,4 , 8, 16, 5, 10, 20, 13, 26, 25, 23, 19, 11, 22, 17, 7, 14} 包含了小於27且與27互質的所有數,圈的首部爲1,這是原根定義決定的。
- 那麼與27最大公約數爲3的數,我們用S(2)中的數乘以3得到。 S(2) * 3 = {3, 6, 12, 24, 21, 15}, 圈中元素的順序沒變化,圈的首部是3。
- 與27最大公約數爲9的數,我們用S(1)中的數乘以9得到。 S(1) * 9 = {9, 18}, 圈中得元素的順序沒變化,圈的首部是9。
換言之,若定義爲整數,假設/N定義爲整數Z除以N後全部餘數的集合,包括{0...N-1}等N個數,而(/N)*則定義爲這Z/N中{0...N-1}這N個餘數內與N互質的數集合。
則當n=13時,2n+1=27,即得/N ={0,1,2,3,.....,26},(/N)*相當於就是{0,1,2,3,.....,26}中全部與27互素的數的集合;而2^k(mod 27)可以把(/27)*取遍,故可得這些數分別在以下3個圈內:
- 取頭爲1,(/27)*={1,2,4,8,16,5,10,20,13,26,25,23,19,11,22,17,7,14},也就是說,與27互素且小於27的正整數集合爲{1,2,4,8,16,5,10,20,13,26,25,23,19,11,22,17,7,14},因此ϕ(m) = ϕ(27)=18, 從而滿足的最小d = 18,故得出2爲27的原根;
- 取頭爲3,就可以得到{3,6,12,24,21,15},這就是以3爲頭的環,這個圈的特點是所有的數都是3的倍數,且都不是9的倍數。爲什麼呢?因爲2^k和27互素。
具體點則是:如果3×2^k除27的餘數能夠被9整除,則有一個n使得3*2^k=9n(mod 27),即3*2^k-9n能夠被27整除,從而3*2^k-9n=27m,其中n,m爲整數,這樣一來,式子約掉一個3,我們便能得到2^k=9m+3n,也就是說,2^k是3的倍數,這與2^k與27互素是矛盾的,所以,3×2^k除27的餘數不可能被9整除。
此外,2^k除以27的餘數可以是3的倍數以外的所有數,所以,2^k除以27的餘數可以爲1,2,4,5,7,8,當餘數爲1時,即存在一個k使得2^k-1=27m,m爲整數。
式子兩邊同時乘以3得到:3*2^k-3=81m是27的倍數,從而3*2^k除以27的餘數爲3;
同理,當餘數爲2時,2^k - 2 = 27m,=> 3*2^k- 6 =81m,從而3*2^k除以27的餘數爲6;
當餘數爲4時,2^k - 4 = 37m,=> 3*2^k - 12 =81m,從而3*2^k除以27的餘數爲12;
同理,可以取到15,21,24。從而也就印證了上面的結論:取頭爲3,就可以得到{3,6,12,24,21,15}。
- 取9爲頭,這就很簡單了,這個圈就是{9,18}
因爲,故:
i = 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
由於n=13,2n+1 = 27,據此公式可知,上面第 i 位置的數將分別變成下述位置的:
i = 2 4 6 8 10 12 14 16 18 20 22 24 26 1 3 5 7 9 11 13 15 17 19 21 23 25 0
根據i 和 i‘ 前後位置的變動,我們將得到3個圈:
- 1248165102013262523191122177141;
- 36122421153
- 9189
2.3.6、完美洗牌問題的幾個擴展
至此,本章開頭提出的問題解決了,完美洗牌算法的證明也證完了,是否可以止步了呢?OH,NO!讀者有無思考過下述問題:
- 既然完美洗牌問題是給定輸入:a1,a2,a3,……aN,b1,b2,b3,……bN,要求輸出:b1,a1,b2,a2,……bN,aN;那麼有無考慮過它的逆問題:即給定b1,a1,b2,a2,……bN,aN,,要求輸出a1,a2,a3,……aN,b1,b2,b3,……bN ?
- 完美洗牌問題是兩手洗牌,假設有三隻手同時洗牌呢?那麼問題將變成:輸入是a1,a2,……aN, b1,b2,……bN, c1,c2,……cN,要求輸出是c1,b1,a1,c2,b2,a2,……cN,bN,aN,這個時候,怎麼處理?
本第35章完。
參考鏈接
- huangxy10,http://blog.csdn.net/huangxy10/article/details/8071242;
- @綠色夾克衫,http://www.51nod.com/answer/index.html#!answerId=598;
- 格子取數的蠻力窮舉法:http://wenku.baidu.com/view/681c853b580216fc700afd9a.html;
- @陳立人,http://mp.weixin.qq.com/mp/appmsg/show?__biz=MjM5ODIzNDQ3Mw==&appmsgid=10000141&itemidx=1&sign=4f1aa1a2269a1fac88be49c8cba21042;
- caopengcs,http://blog.csdn.net/caopengcs/article/details/10196035;
- 完美洗牌算法的原始論文“A Simple In-Place Algorithm for In-Shuffle”,http://att.newsmth.net/att.php?p.1032.47005.1743.pdf;
- 原始根模:http://en.wikipedia.org/wiki/Primitive_root_modulo_n;
- 洗牌的學問:http://www.thecodeway.com/blog/?p=680;
- 關於完美洗牌算法:http://cs.stackexchange.com/questions/332/in-place-algorithm-for-interleaving-an-array/400#400;
- 關於完美洗牌算法中圈的說明:http://www.emis.de/journals/DMTCS/pdfpapers/dm050111.pdf;
- 關於神級結論的討論:http://math.stackexchange.com/questions/477125/how-to-prove-algebraic-structure-of-the-perfect-shuffle(左邊鏈接中的討論中有錯誤,以在本文2.3.5節進行了相關修正);
- caopengcs關於神級結論的證明:http://blog.csdn.net/caopengcs/article/details/10429013;
- 同餘的概念:http://zh.wikipedia.org/wiki/%E5%90%8C%E9%A4%98;
- 神奇的費馬小定理:http://www.xieguofang.cn/Maths/Number_Theory/Fermat's_Little_Theorem_1.htm;
- 完美洗牌問題的幾個擴展:http://blog.csdn.net/caopengcs/article/details/10521603;
- 原根與指數的介紹:http://wenku.baidu.com/view/bbb88ffc910ef12d2af9e738;
- 《數論概論》Joseph H. Silverman著,推薦理由:因寫上文中的完美洗牌算法遇到了一堆數論定理受了刺激,故推薦此書;