第三十四~三十五章:格子取數,完美洗牌算法
時間:二零一三年八月二十三日。
題記
再過一個半月,即到2013年10月11日,便是本博客開通3周年之際,巧的是,那天剛好也是我的25歲生日。寫博近3年,訪問量趨近500萬,無法確切知道幫助了多少人影響了多少人,但有些文章和一些系列是我比較喜歡的,如這三篇:從B樹、B+樹、B*樹談到R 樹;教你如何迅速秒殺掉:99%的海量數據處理面試題;支持向量機通俗導論(理解SVM的三層境界)。
- 程序員編程藝術http://blog.csdn.net/column/details/taopp.html,
- 秒殺99%的海量數據處理面試題http://blog.csdn.net/v_july_v/article/details/7382693,
- 微軟面試100題系列http://blog.csdn.net/column/details/ms100.html,
- 第三十四章:格子取數問題;
- 第三十五章:完美洗牌算法的變形
第三十四章、格子取數問題
- 如果按照上面的局部貪優走法,那么第一次勢必會如圖二那樣走,導致的結果是第二次要么取到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=ϕ(2)=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互質的數集合。


而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個圈:
- 1
2
4
8
16
5
10
20
13
26
25
23
19
11
22
17
7
14
1;
- 3
6
12
24
21
15
3
- 9
18
9
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著,推薦理由:因寫上文中的完美洗牌算法遇到了一堆數論定理受了刺激,故推薦此書;