第三十四~三十五章:格子取數,完美洗牌算法
再過一個半月,即到2013年10月11日,即是本博客開通3週年之際,巧的是,那天恰好也是個人25歲生日。寫博近3年,訪問量趨近500萬,沒法確切知道幫助了多少人影響了多少人,但有些文章和一些系列是我比較喜歡的,如這三篇:從B樹、B+樹、B*樹談到R 樹;教你如何迅速秒殺掉:99%的海量數據處理面試題;支持向量機通俗導論(理解SVM的三層境界)。php
//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。html
故正如@綠色夾克衫所說:此題也能夠用動態規劃求解,主要思路就是同時DP 2次所走的狀態。ios
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步後所處的位置,根據上面的討論,能夠知道:程序員
故推廣來講,對於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時,app
DP[6,2,2] = Max(DP[5,1,1],DP[5,1,2],DP[5,2,2]) + 6,2格子中對應的數值 (式二)
函數
四、故,綜合上述的(式一),(式二)最後的遞推式就是
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]
//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)。
//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
a1, b1,a2,a3,a4, b2,b3,b4
a1, b1,a2, b2,a3,a4, b3,b4
a1, b1,a2, b2,a3, b3,a4, b4
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.一、步步前移同樣,時間複雜度依然爲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
// 時間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....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)的時間先把後兩個元素調整好,但這不影響整體時間複雜度。
「 於此同時,我也提醒下讀者,根據上面變換的節奏,咱們能夠看出有兩個圈,
第一個圈: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*n = (3^k-1)的數組問題,那麼若給定的長度n是任意的咋辦呢?此時,咱們能夠借鑑2.2節、分而治之算法的思想,把整個數組一分爲二,即拆分紅兩個部分:
當把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.一、走圈算法cycle_leader搞定,於此咱們最終求解的n長度變成了n’=3,即n的長度減少了4,單獨再解決後半部分a5 a6 a7 b5 b6 b7便可。
從上文的分析過程當中也就得出了咱們的完美洗牌算法,其算法流程爲:
以上各個步驟對應的時間複雜度分析以下:
此完美洗牌算法實現的參考代碼以下:
//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; }
啊哈!以上代碼即解決了完美洗牌問題,那麼針對本章要解決的其變形問題呢?是的,如本章開頭所說,在完美洗牌問題的基礎上對它最後的序列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*n=(3^k - 1),則剛好只有k個圈,且每一個圈頭部的起始位置分別是1,3,9,...3^(k-1),是如何來的呢?
要證實這個結論的關鍵就是:這全部的圈合併起來必須包含從1到M之間的全部證書,一個都不能少。這個證實有點麻煩,由於證實過程當中會涉及到羣論等數論知識,但再遠的路一步步走也能到達。
首先,讓我們明確如下相關的概念,定理,及定義(搞清楚了這些東西,我們便證實了一大半):
結合上述定義一、定義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}中始終只有一、二、4三種結果,而沒包含所有與7互質的數(三、六、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,也就是說,出現餘數循環必定是從開頭開始循環的。
再好比,2是9的原根,由於,爲了讓除以9的餘數恆等於1,可知最小的正整數d=6,而ϕ(m)=6,知足原根的定義。
S(1) = {1, 2}
S(2) = {1, 2, 4, 8, 7, 5}
下面,舉個例子,如caopengcs所說,當咱們取「a = 2, m = 3時,
換言之,若定義爲整數,假設/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個圈內:
具體點則是:若是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}。
由於,故:
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個圈:
至此,本章開頭提出的問題解決了,完美洗牌算法的證實也證完了,是否能夠止步了呢?OH,NO!讀者有無思考過下述問題:
本第35章完。