《編程之美》讀書筆記(一):中國象棋將帥問題
作者:薛笛 EMail:jxuedi#gmail.com
千呼萬喚始出來,在跳票了快一個月之後,雖然明知道書裡還有不少錯誤沒改過來(附了一整頁的勘誤),但是感覺已經不能等下一版了。趕快去書店買回來,吃完飯躺床上舒舒服服地看。大致翻看之後,總體感覺是書中的內容沒有“脫離群眾”,很多都是我們平時生活、工作中經常能遇到的。題目不見得難,基本上給一本《演算法導論》和足夠的時間,大多數人都能解決其中的問題。但注意副標題--“微軟技術面試心得”,這就給這本書定下一個基調:面對這些我們並不陌生、也並非特別困難的問題,在有限的時間裡,(可能)比較緊張的心情之下,如何充分發揮自己分析問題和解決問題的能力,如何正確且漂亮地解決問題才是關鍵。我想,在平時學習的時候或許我們左手《演算法導論》,右手《編程之美》效果會更好一些。
中國象棋將帥問題由於比較簡單,所以我們暫時不用請出《演算法導論》。該問題的具體描述是:(根據中國象棋的基本原則)在只有雙的將帥棋盤上,找出所有雙方可以落子的位置(將帥不能碰面),但只能使用一個變數。直覺上我們想到,只要遍曆將帥所有可能的位置,去除將帥衝突的位置即可。可見,剩下的問題就在於如何使用一個變數來做二重迴圈的遍曆。書中解法一給出的方法是將一個Byte變數拆成兩個用,前一半代表“帥”可以走的位置,後一個變數代表“將”可以走的位置(事先已經將“將”和“帥”可以走的3*3的位置進行了編號),利用位操作即可獲得兩個計數器的功能。書中的解法三採用結構體來解決一個變數遍曆二重迴圈的問題,思想上換湯不換藥。真正有趣的是解法二,它的代碼如下:
int var = 81;
while( var-- )
{
if( var / 9 % 3 == var % 9 % 3 )//發生衝突
continue;
else
printf(/** 列印可行的位置 **/);
}
當看到這個解法的時候,我心裡有一些感慨。在前幾個月,我一直未MSRA面試沒通過而惱火。但看到這個解法之後,我覺得我確實還要再努力一些才行。短短几行,體現了簡約之美,僅看看這個就值回錢了(開玩笑)。雖然可能有牛人說這沒什麼了不起,但我覺得如果我在面試這個問題的時候能寫下這樣的代碼,我會很有成就感。在大多數時候我們無需知道希爾排序的時間複雜度的一點幾次方是怎麼算出來的,也無需去證明一個最佳化問題是否滿足“擬陣”的條件,我們只需要在這樣一個“簡單”的問題上做得漂亮,就夠了。
回過頭來分析這個解法。“將”和“帥”各在自己的3*3的格子間裡面走動,我們共需要驗證9*9=81種位置關係,這也是i=81的由來。此外我們要明白i/9和i%9的含義。我們知道,整數i可以由部兩分組成,即var=(var/9)*9+var%9 ,其中var<n。我們注意到,在i從81到0變化的過程中,var%9的變化相當於內層迴圈,var/9的變話相對於內層迴圈。這樣,作者就妙地用一個變數i同時獲得了兩個變數的數值。
簡單即是美,相對於解法一的大段代碼,我更希望我以後再面試中寫出解法二。
其實這個問題還可以進行一些擴充,即如何利用一個變數達到三重迴圈的效果。也就是說,如果給定下面的迴圈:
int counter = 0;
for( int i = 0; i < 5; i++ )
for( int j = 0; j < 4; j++ )
for( int k = 0; k < 3; k++ )
{
System.out.println("counter="+counter+"/t, i="+i+", j="+j+", k="+k);
counter++;
}
其結果如下:
counter=0 , i=0, j=0, k=0
counter=1 , i=0, j=0, k=1
counter=2 , i=0, j=0, k=2
counter=3 , i=0, j=1, k=0
counter=4 , i=0, j=1, k=1
....中間略
counter=59 , i=4, j=3, k=2
問題是(1)我們如何用一個列印出相同的結果?(2)如果是N重迴圈呢?
面對第一個問題,實際上就是對原始的中國象棋將帥問題進行了一個擴充,即在棋盤上添加一個“王”,其行走規則和將帥 一樣。於是棋盤變成了三國爭霸:-) ,將帥王可以走動的格子數分別為3、4、5,它們之間的互斥條件可以按需要設定。
這時,就需要只用一個變數遍曆一個三重迴圈。直觀的方法是像方法一那樣把一個4位元組的INT拆開來用。我這裡只關注方法二。
只用一個變數解決擴充的中國象棋將帥問題,我們的代碼應該是如下的樣子:
int var = 3*4*5;
while( var-- )
{
if( /** 衝突條件 **/ )//發生衝突
continue;
else
printf(/** 列印可行的位置 **/);
}
在衝突條件中,我們需要知道var取得某個特定的值(即第var+1次迴圈)的時候的i,j,k分別是多少(這樣我們才能判定將帥位置是否衝突)
從上例的結果中我們可以看到,counter的值(即當前的迴圈次數)和三元組(i,j,k)是一一對應的,越是外層的迴圈變化越慢,他們滿足什麼關係呢?
k的取值最好確定,我們都知道是var%3。
在原始的將帥問題中我們知道,j的值應該是 var/3,但是由於j上面還有一層迴圈,就需要做些調整,變成var/3%4
最外層迴圈i的值則為(var/(3*4))%5.
即:k=var%3 //其下沒有迴圈了
j=var/3 //其下有幾個迴圈長度為3的迴圈
i=var/(3*4). //其下有幾個迴圈長度為3*4的迴圈
於是4重迴圈的公式我們也可以輕鬆得出:
for( int i = 0; i < 5; i++ )
for( int j = 0; j < 4; j++ )
for( int k = 0; k < 3; k++ )
for( int p = 0; p < 3; p++ )
p=var%2 //其下沒有迴圈了
k=var/2 //其下有幾個迴圈長度為2的迴圈
j=var/(2*3)) //其下有幾個迴圈長度為2*3的迴圈
i=var/(2*3*4)//其下有幾個迴圈長度2*3*4的迴圈
下面就是一個變數實現三重迴圈
int var = 2*3*4*5;
while( var-- > 0)...{
System.out.println("var="+var+" , i="+((var/(2*3*4))%5)+
", j ="+((var/(2*3))%4)+",
k="+((var/2)%3)+",
p="+var%2);
}
結果是:
var=119 , i=4, j=3, k=2, p=1
var=118 , i=4, j=3, k=2, p=0
var=117 , i=4, j=3, k=1, p=1
...中間略
var=5 , i=0, j=0, k=2, p=1
var=4 , i=0, j=0, k=2, p=0
var=3 , i=0, j=0, k=1, p=1
var=2 , i=0, j=0, k=1, p=0
var=1 , i=0, j=0, k=0, p=1
var=0 , i=0, j=0, k=0, p=0
N重迴圈原理也是一樣,就不再贅述了。
PS:看到最後一例的結果是不是與《演算法導論》中平攤分析一章的二進位計數器很像?只不過這裡進位不一樣而已:-)
[勘誤: P19 代碼清單1-7的第七行,應該改為if(i.a%3 != i.b%3)]
謹以此文與大家共勉 2008/04/05