SlideShare a Scribd company logo
第1章

                        遊戲之樂
                           ——遊戲中碰到的題目




研究院舉辦過幾屆手足球(foosball)公開賽,第一屆的冠軍是一位文靜的女實習生
程式之美-微軟技術面試心得




     這一章的題目原本預計叫做「Problem Solving」——運用所學的知識解決問題,直譯為
     「解決問題」,但似乎不太理想。事實上,這裡面大部分題目都是和遊戲相關的,因此,
     本章改名為「遊戲之樂」。這些題目從遊戲和作者平時遇到的有趣問題出發,向程式設
     計師提出挑戰。

     個人電腦(PC)在蹣跚起步的時候,就被當時的主流觀點視為玩具。PC 的確有各種各
     樣的遊戲,電腦上的遊戲是給人玩的,如果你願意,CPU 也可以讓人「玩」。

     筆者曾經用「CPU 使用率」這個問題問了十幾個應聘者,一個典型的模式是:

     我: 筆試考得怎麼樣?發揮了多少實力?

     答: 我不習慣在紙上寫程式,平時都在電腦上寫……

     我: 那你對 Windows、作業系統這些東西熟悉嗎?

     答: 還算是相當熟悉……

     我: 好,那你是否能在這台筆記型電腦上幫我解決一個問題——讓 CPU 的使用率劃出
        一條水平線,比如就在 50%的地方。

     這個時候可以觀察應聘者如下好幾個方面:

      應聘者面對這個陌生問題時,如何開始分析。

      有人知道觀察「工作管理員」如何執行,有人在紙上寫寫畫畫,有人明顯沒有
      什麼想法。

      當提示可以在網路上搜尋資料時,應聘者如何尋找資料,如何學習。

      比如,有一位學生很快地用快捷鍵在 IE 中開啟了幾個 Tab 視窗,然後在每個視
      窗輸入不同的搜尋關鍵字。當我提示在 MSDN 上找一些函數的時候,有些人根
      本不知道 MSDN 網站應該怎麼用。有些人反覆讀了函數的說明,仍不知如何下
      手。

      在電腦上是如何寫程式,如何進行除錯的。

      有人能很嫺熟地使用 C/C#的各種語言特性,很快地寫出程式,有人寫的程式編
      譯了好幾次都不能通過,對編譯錯誤束手無策。程式第一次執行的時候,工作
      管理員的 CPU 使用率不如預期,這時候有人就十分慌亂,在程式中瞎改一通,
      希望能「矇」對。有人則有條理地分析,最後找到並解決問題。



2
第 1 章:遊戲之樂



我想,45 分鐘下來,應聘者的思考能力、學習能力、技術能力如何,應該都很清楚了。
行還是不行,雙方也都明白了。

這一章的其他題目大多和遊戲有關,同學們在玩《接龍》、《俄羅斯方塊》,甚至《魔
獸世界》的時候,有沒有產生好奇心——這個程式為什麼這麼酷,如果是我來寫,應該
怎麼做?有沒有把好奇心轉化為行動?

喜歡玩電腦、會玩電腦的人,也會運用電腦解決實際問題,這也是我們要找的人才。




                                             3
程式之美-微軟技術面試心得




    1.1       ★★★
              讓 CPU 使用率曲線聽你指揮

    寫一個程式,讓使用者決定 Windows 工作管理員(Task Manager)的 CPU 使用率。
    程式越精簡越好,電腦語言不限。例如,可以實現下列三種情況:

      CPU 的使用率固定在 50%,為一條直線。

      CPU 的使用率為一條直線,但是實際使用率由命令行參數決定(參數範圍 1~
      100)。

      CPU 的使用率狀態是一條正弦曲線。




4
第 1 章:遊戲之樂




分析與解法1

有一名學生寫了如下的程式碼:
while(true)
{
    if(busy)
        i++;
    else


}


然後她就陷入了苦苦思索:else 要做什麼呢?怎樣才能讓電腦不做事情呢?CPU 使用
率為 0 的時候,到底是什麼東西在用 CPU?另一名學生花了很多時間構想如何「深入
核心,以控制 CPU 使用率」——可是事情真的有這麼複雜嗎?

MSRA IEG(Microsoft Research Asia, Innovation Engineering Group)的一些實習生寫了
各種解法,他們寫的簡單程式可以達到如圖 1-1 所示的效果。




                   圖 1-1:寫程式控制 CPU 使用率呈現正弦曲線


    作者注:當面試的同學聽到這個問題的時候,很多人都有點意外。我把我的筆記型電腦交給他們說,這是開卷考
    試,你可以上網查資料,做什麼都可以。大部分面試者在電腦上的第一個動作就是上網搜尋「CPU 使用率 50%」
    這樣的關鍵字,當然沒有找到什麼直接的結果。不過本書出版以後,情況可能就不一樣了。



                                                                          5
程式之美-微軟技術面試心得



    看來這並不是不可能完成的任務。讓我們仔細地回想一下寫程式時曾經碰到的問題,
    如果我們不小心寫了一個無窮迴圈,CPU 使用率就會跳到最高,而且一直保持在
    100%。我們也可以開啟工作管理員 ,實際觀察一下它是如何變動的。憑肉眼觀察,
                                  2




    它大約是 1 秒鐘更新一次。一般情況下,CPU 使用率會很低。但是,當使用者執行一
    個程式,進行一些複雜操作的時候,CPU 的使用率會急遽升高。當使用者移動滑鼠時,
    CPU 的使用率也有小幅度的變化。

    當工作管理員報告 CPU 使用率為 0 的時候,誰在使用 CPU 呢?透過工作管理員的「處
    理程序(Process)
               」一頁可以看到,System Idle Process 佔用了 CPU 空閒的時間——這
    時候大家該回憶起在「作業系統」這門課上所學到的一些知識了吧。系統中有那麼多
              ,它們什麼時候能「閒下來」呢?答案很簡單,這些程式或者在等待使
    process(行程)
    用者的輸入,或者在等待某些事件的發生 ,或者主動進入休眠狀態 。  3        4




    在工作管理員的一個更新週期內,CPU 忙(執行應用程式)的時間和更新週期總時間
    的比率,就是 CPU 的使用率,也就是說,工作管理員中顯示的是每個更新週期內 CPU
    使用率的統計平均值。因此,我們可以寫一個程式,讓它在工作管理員的更新期間內
    一會兒忙,一會兒閒,然後透過調整忙/閒的比例,就可以控制工作管理員中顯示的
    CPU 使用率。


    解法一:簡單的解法

    要控制 CPU 的使用率曲線,就需要讓 CPU 在一段時間內(根據 Task Manager 的採樣率)
    跑 busy 和 idle 兩個不同的迴圈(loop)
                              ,從而透過不同的時間比例,來調節 CPU 使用率。

    Busy loop 可以透過執行空迴圈來實現,idle 可以透過 Sleep()來實現。

    問題的關鍵在於如何控制兩個 loop 的時間,我們先試驗一下 Sleep 一段時間,然後迴
    圈 n 次,計算 n 的值。

    那麼,對於一個空迴圈 for(i = 0; i < n; i++);又該如何計算這個最合適的 n 值
    呢?我們都知道 CPU 執行的是機器指令,而最接近於機器指令的語言是組合語言,所
    以我們可以先把這個空迴圈簡單地寫成如下組合語言程式碼後,再進行分析:



      如果應聘者從來沒有研究過工作管理員,最好還是不要在簡歷上寫「精通 Windows」比較好。
      例如 WaitForSingleObject()。
      可以透過 Sleep()來實現。



6
第 1 章:遊戲之樂



loop:
mov dx i     ;將 i 置入 dx 暫存器
inc dx              ;將 dx 暫存器加 1
mov i dx            ;將 dx 中的值放回 i
cmp i n             ;比較 i 和 n
jl loop             ;i 小於 n 時則重複迴圈


假設這段程式碼要執行的 CPU 是 P4 2.4Ghz(2.4 * 10 的 9 次方個時鐘週期/每秒)。
現在 CPU 每個時鐘週期可以執行兩個以上的程式碼,那麼我們就取平均值兩個,於是
讓(2,400,000,000 * 2)/5=960,000,000(迴圈/秒),也就是說,CPU 1 秒鐘可以執行這
個空迴圈 960,000,000 次 不過 我們還是不能單純地將 n = 960,000,000 然後 Sleep(1000)
                  。 ,                          ,
了事。如果我們讓 CPU 工作 1 秒鐘,然後休息 1 秒鐘,波形很有可能就是鋸齒狀的——
先達到一個峰值(>50%),然後跌到一個很低的使用率。

我 們 嘗 試 著 降 低 兩 個 數 量 級 , 令 n = 9,600,000 , 而 睡 眠 時 間 對 應 改 為 10 毫 秒
(Sleep(10))
          。選擇 10 毫秒是因為它不大也不小,比較接近 Windows 排程時間的間
隔。如果選得太小(如 1 毫秒)
               ,則會造成執行緒頻繁地被喚醒和暫停,無形中又增加
了核心時間的不確定性。最後我們可以得到程式碼清單 1-1。

 程式碼清單 1-1
int main()
{
    for(; ; )
    {
          for(int i = 0; i < 9600000; i++)
                ;
          Sleep(10);
    }
    return 0;
}

在不斷調整 9,600,000 的參數後,我們就可以在一台指定的機器上獲得一條大致穩定的
50% CPU 使用率直線。

使用這種方法要注意兩點影響:

        儘量減少 sleep/awake 的頻率,減少作業系統核心排程程式的干擾。

        儘量不要呼叫系統函數(比如 I/O 這些 privilege instruction)
                                                  ,因為它也會導致很
        多不可控制的核心執行時間。



                                                                       7
程式之美-微軟技術面試心得



    此方法的缺點也很明顯:不能在不同的機器上通用。一旦換了一個 CPU,我們又得重
    新計算 n 值。有沒有辦法動態地瞭解 CPU 的運算能力,然後自動調節忙/閒的時間比
    呢?請看下一個解法。


    解法二:使用 GetTickCount()和 Sleep()

    我們知道 GetTickCount()可以得到「系統啟動到現在」所經歷時間的毫秒值,最多能
    夠統計到 49.7 天。我們可以利用 GetTickCount()來判斷 busy loop 要迴圈多久,程式碼
    如清單 1-2 所示。

    程式碼清單 1-2
    int busyTime = 10;           // 10 ms
    int idleTime = busyTime;    // same ratio will lead to 50% cpu usage


    Int64 startTime = 0;
    while(true)
    {
        startTime = GetTickCount();
        // busy loop
        while((GetTickCount() - startTime) <= busyTime)
           ;

        // idle loop
        Sleep(idleTime);
    }

    這兩種解法都是假設目前系統上只有當前程式在執行,但實際上,作業系統中有很多
    程式會同時執行各式各樣的任務,如果此刻其他行程(process)使用了 10% 的 CPU,
    那我們的程式應該只能使用 40%的 CPU,這樣才能達到 50%的效果。

    怎麼做呢?這就要用到另一個工具來幫忙——Perfmon.exe。

    Perfmon 是從 Windows NT 開始就包含在 Windows 管理工具組中的專業檢測工具之一
                                                       (如
    圖 1-2 所示)。Perfmon 可獲取有關作業系統、應用程式和硬體的各種效能計數器(perf
    counter)。Perfmon 的用法相當直接,只要選擇你所要檢測的物件(如處理器、RAM 或
    硬碟),然後選擇效能計數器(如監視磁片的平均佇列長度)即可。




8
第 1 章:遊戲之樂




                            圖1-2:系統監視器(Perfmon)


我 們 可 以 寫 程 式 來 查 詢 Perfmon 的 值 , Microsoft .Net Framework 提 供 了
PerformanceCounter 這一物件,可以方便地得到各種效能資料,包括 CPU 的使用率。
例如下面這個程式(見程式碼清單 1-3)。


 解法三:能動態調整的解法

 程式碼清單 1-3
// C# code
static void MakeUsage(float level)
{
   PerformanceCounter p = new PerformanceCounter(quot;Processorquot;,
       quot;%Processor Timequot;, quot;_Totalquot;);


   if(p==NULL)
   {
         return
   }


   while(true)
   {
         if(p.NextValue() > level)
              System.Threading.Thread.Sleep(10);




                                                                             9
程式之美-微軟技術面試心得



         }
     }

     上面的解法能方便地處理各種 CPU 使用率參數 這個程式可以解答前面提到的問題 2。
                            ,

     有了前面的知識累積,我們應該可以讓工作管理員畫出優美的正弦曲線了,見程式碼清
     單 1-4。


      解法四:正弦曲線

      程式碼清單 1-4
     // C++ code to make task manager generate sine graph
     #include quot;Windows.hquot;
     #include quot;stdlib.hquot;
     #include quot;math.hquot;


     const double SPLIT = 0.01;
     const int COUNT = 200;
     const double PI = 3.14159265;
     const int INTERVAL = 300;


     int _tmain(int argc, _TCHAR* argv[])
     {
         DWORD busySpan[COUNT];      // array of busy times
         DWORD idleSpan[COUNT];      // array of idle times
         int half = INTERVAL / 2;
         double radian = 0.0;
         for(int i = 0; i < COUNT; i++)
         {
              busySpan[i] = (DWORD)(half + (sin(PI * radian) * half));
              idleSpan[i] = INTERVAL - busySpan[i];
              radian += SPLIT;
         }


         DWORD startTime = 0;
         int j = 0;
         while(true)
         {
              j = j % COUNT;
              startTime = GetTickCount();
              while((GetTickCount() - startTime) <= busySpan[j])
                  ;
              Sleep(idleSpan[j]);



10
第 1 章:遊戲之樂



              j++;
        }
        return 0;
}




討論

如果機器是多 CPU,上面的程式會出現什麼結果?如何在多個 CPU 時顯示同樣的狀
態?例如,在雙核心的機器上,如果讓一個單執行緒的程式進行無窮迴圈,能讓兩個
CPU 的使用率達到 50%的水準嗎?為什麼?

多 CPU 的問題首先需要獲得系統的 CPU 資訊 可以使用 GetProcessorInfo()獲得多
                         。
處理器的資訊,然後指定行程在哪一個處理器上執行。其中指定執行使用的 CPU 是透
過 SetThreadAffinityMask()函數。

另外,還可以使用 RDTSC 指令獲取當前 CPU 核心執行週期數。

在 x86 平臺上定義函數:

inline __int64 GetCPUTickCount()
{
    __asm
    {
            rdtsc;
    }
}


在 x64 平臺上定義:

#define GetCPUTickCount() __rdtsc()


使用 CallNtPowerInformation API 得到 CPU 頻率,從而將週期數轉化為毫秒數,例如程
式碼清單 1-5 所示。

 程式碼清單 1-5
_PROCESSOR_POWER_INFORMATION info;


CallNTPowerInformation(11,    // query processor power information
    NULL,                     // no input buffer
    0,                        // input buffer size is zero



                                                                              11
程式之美-微軟技術面試心得



       &info,                    // output buffer
       Sizeof(info));            // outbuf size


       __int64 t_begin = GetCPUTickCount();


       // do something


       __int64 t_end = GetCPUTickCount();
       double millisec = ((double)t_end –(double)t_begin)
          /(double)info.CurrentMhz;


     RDTSC 指令讀取目前 CPU 的週期數,在多 CPU 系統中,這個週期數在不同的 CPU 之
     間基數不同,頻率也有可能不同。利用從兩個不同的 CPU 得到的週期數來計算,會得
     出沒有意義的值。如果執行緒在執行中被調動到了不同的 CPU,就會出現上述情況。
     可用 SetThreadAffinityMask 避免執行緒遷移。另外,CPU 的頻率會隨系統供電及負荷
     情況有所調整。


     總結

     能幫助你瞭解當前執行緒/行程/系統效能的 API 大致有以下這些。

        Sleep():這個方法能讓當前執行緒「停」下來。

        WaitForSingleObject():自己停下來,等待某個事件發生。

        GetTickCount():有人把 Tick 翻譯成「滴答」,很傳神。

        QueryPerformanceFrequency()、QueryPerformanceCounter():讓你取得更
        精細的 CPU 資料。

        timeGetSystemTime():是另一個得到高精度時間的方法。

        PerformanceCounter:效能計數器。

        GetProcessorInfo()/SetThreadAffinityMask() :遇到多核心的問題怎麼辦
        呢?這兩個方法能夠幫你更有效地控制 CPU。

        GetCPUTickCount():想拿到 CPU 核心執行週期數嗎?試試這個方法吧。

     瞭解並應用了上面的 API,就可以考慮在履歷表中寫上「精通 Windows」了。




12
第 1 章:遊戲之樂




1.2        ★★★
           中國象棋將帥問題

下過象棋的人都知道,雙方的「將」和「帥」相隔遙遠,而且它們不能碰面。在象棋
殘局中,許多高手能利用這一規則走出巧妙的必殺招數。假設棋盤上只有「將」 「帥」
                                   和
二子(如圖 1-3 所示)(為了下面敘述方便,我們約定用 A 表示「將」 表示
                                    ,B 「帥」 。
                                          )


                           將




                                 帥


                      圖 1-3:只有將帥的棋盤


A、B 二子被限制在己方 3×3 的格子裡移動。例如,在如上的表格裡,A 被正方形{d10,
f10, d8, f8}包圍,而 B 被正方形{d3, f3, d1, f1}包圍。每一步,A、B 分別可以橫向或縱向移
動一格,但不能沿對角線移動。另外,A 不能面對 B,也就是說,A 和 B 不能處於同一
縱向直線上(如 A 在 d10 的位置,那麼 B 就不能在 d1、d2 以及 d3 的位置上)。

請寫一個程式,輸出 A、B 所有合法位置,並要求在程式碼中只能使用一個變數。




                                                               13
程式之美-微軟技術面試心得




     分析與解法

     問題的本身並不複雜,只要把所有 A、B 互相排斥的條件列舉出來就可以完成本題的要
     求。由於本題要求只能使用一個變數,所以必須先想清楚在寫程式時,有哪些資訊需
     要儲存,並且儘量高效率地儲存資訊。稍微思考一下,可以知道這個程式的框架大致
     上是:
     走遍 A 的位置
        走遍 B 的位置
            判斷 A、B 的位置組合是否滿足要求。
           如果滿足,則輸出。


     因此,需要儲存的是 A、B 的位置資訊,並且每次迴圈都要更新。為了能夠進行判斷,
     首先需要建立一個邏輯的座標系統,以便檢測 A 何時會面對 B。這裡我們想到的方法
     是用 1~9 的數字,按照行的優先順序來表示每個格點的位置(如圖 1-4 所示)。這樣,
     只需要用「求餘數運算」就可以得到當前的列號,從而判斷 A、B 是否互斥。




                       圖 1-4:用 1~9 的數字表示 A、B 的座標


     第二,題目要求只用一個變數,但是我們卻要儲存 A 和 B 兩個子的位置資訊,該怎麼
     辦呢?

     可以先把已知變數類型列舉一下,然後做些分析。

     對於 bool 類型,是沒有辦法做任何擴展的,因為它只能表示 true 和 false 兩個值;而
     byte 或 int 類型,它們能夠表達的資訊則更多。事實上,對本題來說,每個子都只需要
     9 種可能值,就可以表達它的全部位置。

     一個 8 位元的 byte 類型能夠表達 28=256 個值,所以用它來表示 A、B 的位置資訊綽綽有
     餘,因此,可以把這個位元組的變數(設為 b)分成兩部分。用前面的 4 bit 表示 A 的位
     置,用後面的 4 bit 表示 B 的位置,那麼 4 個 bit 可以表示 16 種可能值,這已經足夠了。


14
第 1 章:遊戲之樂



問題在於如何使用 bit 級的運算,將資料從這一 byte 變數的左邊和右邊分別存入和讀出。

下面是做法:

  將 byte b(10100101)的右邊 4 bit(0101)設為 n(0011):

  首先清除 b 右邊的 bits,同時保持左邊的 bits:
     11110000(LMASK)
   & 10100101(b)
     ---------------
     10100000

  然後將上一步得到的結果與 n 做「或」運算
     10100000(LMASK & b)
   ^ 00000011(n)
     ------------
     10100011

  將 byte b(10100101)左邊的 4 bit(1010)設為 n(0011):

  首先,清除 b 左邊的 bits,同時保持右邊的 bits:
     00001111(RMASK)
   & 10100101(b)
     -----------
     00000101

  現在,把 n 移動到 byte 資料的左邊
      n << 4 = 00110000

  然後對以上兩步得到的結果做「或」運算,即可得到最終結果。
     00000101(RMASK & b)
   ^ 00110000(n << 4)
     -----------
     00110101

  得到 byte 資料的右邊 4 bits 或左邊 4 bits(e.g. 10100101 中的 1010 以及 0101):

  清除 b 左邊的 bits,同時保持右邊的 bits:
     00001111(RMASK)
   & 10100101(b)
     -----------
     00000101



                                                                    15
程式之美-微軟技術面試心得



         清除 b 右邊的 bits,同時保持左邊的 bits:
            11110000(LMASK)
          & 10100101(b)
            -----------
            10100000

         將結果右移 4 bits
              10100000 >> 4 = 00001010

     最後的挑戰是如何在不宣告其他變數條件的前提下,建立一個 for 迴圈。可以重複利
     用 1byte 的儲存單元,把它作為迴圈計數器,並用前面提到的存取和讀入技術進行操
     作。還可以用巨集來抽象化程式碼,例如:
     for (LSET(b, 1); LGET(b) <= GRIDW * GRIDW; LSET(b, (LGET(b) + 1)))


     解法一

     程式碼清單 1-6
     #define HALF_BITS_LENGTH 4
     // 這個值是記憶儲存單元長度的一半,在這道題裡是 4bit
     #define FULLMASK 255
     // 這個數字表示一個全部 bit 的 mask,以二進位表示是 11111111。
     #define LMASK (FULLMASK << HALF_BITS_LENGTH)
     // 這個巨集表示左 bits 的 mask,以二進位表示是 11110000。
     #define RMASK (FULLMASK >> HALF_BITS_LENGTH)
     // 這個數字表示右 bits 的 mask,以二進位表示是 00001111。
     #define RSET(b, n) (b = ((LMASK & b) ^ n))
     // 這個巨集將 b 的右邊設置成 n
     #define LSET(b, n) (b = ((RMASK & b) ^ (n << HALF_BITS_LENGTH)))
     // 這個巨集,將 b 的左邊設置成 n
     #define RGET(b) (RMASK & b)
     // 這個巨集得到 b 的右邊的值
     #define LGET(b) ((LMASK & b) >> HALF_BITS_LENGTH)
     // 這個巨集得到 b 的左邊的值
     #define GRIDW 3
     // 這個數字表示將帥移動範圍的行寬度。
     #include <stdio.h>
     #define HALF_BITS_LENGTH 4
     #define FULLMASK 255
     #define LMASK (FULLMASK << HALF_BITS_LENGTH)
     #define RMASK (FULLMASK >> HALF_BITS_LENGTH)
     #define RSET(b, n) (b = ((LMASK & b) ^ n))



16
第 1 章:遊戲之樂



#define LSET(b, n) (b = ((RMASK & b) ^ (n << HALF_BITS_LENGTH)))
#define RGET(b) (RMASK & b)
#define LGET(b) ((LMASK & b) >> HALF_BITS_LENGTH)
#define GRIDW 3


int main()
{
    unsigned char b;
    for(LSET(b, 1); LGET(b) <= GRIDW * GRIDW; LSET(b, (LGET(b) + 1)))
        for(RSET(b, 1); RGET(b) <= GRIDW * GRIDW; RSET(b, (RGET(b) + 1)))
              if(LGET(b) % GRIDW != RGET(b) % GRIDW)
                  printf(quot;A = %d, B = %dnquot;, LGET(b), RGET(b));


    return 0;
}


輸出

格子的位置用 N 來表示,N = 1, 2, …, 8, 9,依照行的優先順序,如圖 1-5 所示。




                「將」(A)的格子




                「帥」(B)的格子




                            圖 1-5:「將」與「帥」的格子位置

             A = 1, B = 2        A = 4, B = 2        A = 7, B = 2
             A = 1, B = 3        A = 4, B = 3        A = 7, B = 3
             A = 1, B = 5        A = 4, B = 5        A = 7, B = 5
             A = 1, B = 6        A = 4, B = 6        A = 7, B = 6
             A = 1, B = 8        A = 4, B = 8        A = 7, B = 8
             A = 1, B = 9        A = 4, B = 9        A = 7, B = 9




                                                                                 17
程式之美-微軟技術面試心得



                   A = 2, B = 1          A = 5, B = 1       A = 8, B = 1
                   A = 2, B = 3          A = 5, B = 3       A = 8, B = 3
                   A = 2, B = 4          A = 5, B = 4       A = 8, B = 4
                   A = 2, B = 6          A = 5, B = 6       A = 8, B = 6
                   A = 2, B = 7          A = 5, B = 7       A = 8, B = 7
                   A = 2, B = 9          A = 5, B = 9       A = 8, B = 9
                   A = 3, B = 1          A = 6, B = 1       A = 9, B = 1
                   A = 3, B = 2          A = 6, B = 2       A = 9, B = 2
                   A = 3, B = 4          A = 6, B = 4       A = 9, B = 4
                   A = 3, B = 5          A = 6, B = 5       A = 9, B = 5
                   A = 3, B = 7          A = 6, B = 7       A = 9, B = 7
                   A = 3, B = 8          A = 6, B = 8       A = 9, B = 8

     考慮了這麼多因素,總算得到了本題的一個解法,但是 MSRA 裡卻有人說,下面的一
     小段程式碼也能達到同樣的目的:
     BYTE i = 81;
     while(i--)
     {
            if(i / 9 % 3 == i % 9 % 3)
                continue;
            printf(「A = %d, B = %dn」, i / 9 + 1, i % 9 + 1);
     }


     但是很快又有另一個人說他的解法才是效率最高的:

     程式碼清單 1-7
     struct {
            unsigned char a:4;
            unsigned char b:4;
     } i;


     for(i.a = 1; i.a <= 9; i.a++)
         for(i.b = 1; i.b <= 9; i.b++)
                if(i.a % 3 != i.b % 3)
                    printf(「A = %d, B = %dn」, i.a, i.b);

     讀者能自己證明一下嗎?                  5




         這一題目由微軟亞洲研究院工程師 Matt Scott 提供,他在學習象棋時想出了這個題目,後來一位應聘者給了比他
         的「正解」簡明很多的答案,他們現在成了同事。



18

More Related Content

What's hot

入門啟示錄Ch08簡報
入門啟示錄Ch08簡報入門啟示錄Ch08簡報
入門啟示錄Ch08簡報Chiou WeiHao
 
入門啟示錄Ch07簡報
入門啟示錄Ch07簡報入門啟示錄Ch07簡報
入門啟示錄Ch07簡報Chiou WeiHao
 
【12-E-2】 SEC流品質作りこみESQR 組込みソフトウェア開発向け品質作り込みガイドの紹介
【12-E-2】 SEC流品質作りこみESQR 組込みソフトウェア開発向け品質作り込みガイドの紹介【12-E-2】 SEC流品質作りこみESQR 組込みソフトウェア開発向け品質作り込みガイドの紹介
【12-E-2】 SEC流品質作りこみESQR 組込みソフトウェア開発向け品質作り込みガイドの紹介devsumi2009
 
張茂桂 再論公與私
張茂桂 再論公與私張茂桂 再論公與私
張茂桂 再論公與私科幻嘴泡
 
入門啟示錄Ch02簡報
入門啟示錄Ch02簡報入門啟示錄Ch02簡報
入門啟示錄Ch02簡報Chiou WeiHao
 
CRE-004-引領企業創新
CRE-004-引領企業創新CRE-004-引領企業創新
CRE-004-引領企業創新handbook
 
第4章 作業系統 (Update)
第4章 作業系統 (Update)第4章 作業系統 (Update)
第4章 作業系統 (Update)Seng Chi Ao
 
PMT-005-生產作業管理 製程選擇與設施佈置
PMT-005-生產作業管理 製程選擇與設施佈置PMT-005-生產作業管理 製程選擇與設施佈置
PMT-005-生產作業管理 製程選擇與設施佈置handbook
 
醫師公會全聯會醫療政策建言書
醫師公會全聯會醫療政策建言書醫師公會全聯會醫療政策建言書
醫師公會全聯會醫療政策建言書honan4108
 
97年研發替代役管考獎懲作業說明簡報(公告版)
97年研發替代役管考獎懲作業說明簡報(公告版)97年研發替代役管考獎懲作業說明簡報(公告版)
97年研發替代役管考獎懲作業說明簡報(公告版)Mu Chun Wang
 
DS-012-產品設計與製程選擇
DS-012-產品設計與製程選擇DS-012-產品設計與製程選擇
DS-012-產品設計與製程選擇handbook
 
PMT-007-生產管理
PMT-007-生產管理PMT-007-生產管理
PMT-007-生產管理handbook
 
台灣經濟新藍圖系列之二 - 產業再造及全球連結
台灣經濟新藍圖系列之二 - 產業再造及全球連結台灣經濟新藍圖系列之二 - 產業再造及全球連結
台灣經濟新藍圖系列之二 - 產業再造及全球連結ma19
 
Persona design method / ペルソナ概論
Persona design method / ペルソナ概論Persona design method / ペルソナ概論
Persona design method / ペルソナ概論Katsumi TAZUKE
 
Ds 018 機械產業專業人才認證考試
Ds 018 機械產業專業人才認證考試Ds 018 機械產業專業人才認證考試
Ds 018 機械產業專業人才認證考試handbook
 
Beocom2 Userguide Chinese Traditional
Beocom2 Userguide Chinese TraditionalBeocom2 Userguide Chinese Traditional
Beocom2 Userguide Chinese Traditionalguest8759309
 
Web技術勉強会11回目
Web技術勉強会11回目Web技術勉強会11回目
Web技術勉強会11回目龍一 田中
 

What's hot (20)

入門啟示錄Ch08簡報
入門啟示錄Ch08簡報入門啟示錄Ch08簡報
入門啟示錄Ch08簡報
 
test
testtest
test
 
入門啟示錄Ch07簡報
入門啟示錄Ch07簡報入門啟示錄Ch07簡報
入門啟示錄Ch07簡報
 
【12-E-2】 SEC流品質作りこみESQR 組込みソフトウェア開発向け品質作り込みガイドの紹介
【12-E-2】 SEC流品質作りこみESQR 組込みソフトウェア開発向け品質作り込みガイドの紹介【12-E-2】 SEC流品質作りこみESQR 組込みソフトウェア開発向け品質作り込みガイドの紹介
【12-E-2】 SEC流品質作りこみESQR 組込みソフトウェア開発向け品質作り込みガイドの紹介
 
張茂桂 再論公與私
張茂桂 再論公與私張茂桂 再論公與私
張茂桂 再論公與私
 
入門啟示錄Ch02簡報
入門啟示錄Ch02簡報入門啟示錄Ch02簡報
入門啟示錄Ch02簡報
 
CRE-004-引領企業創新
CRE-004-引領企業創新CRE-004-引領企業創新
CRE-004-引領企業創新
 
第4章 作業系統 (Update)
第4章 作業系統 (Update)第4章 作業系統 (Update)
第4章 作業系統 (Update)
 
PMT-005-生產作業管理 製程選擇與設施佈置
PMT-005-生產作業管理 製程選擇與設施佈置PMT-005-生產作業管理 製程選擇與設施佈置
PMT-005-生產作業管理 製程選擇與設施佈置
 
醫師公會全聯會醫療政策建言書
醫師公會全聯會醫療政策建言書醫師公會全聯會醫療政策建言書
醫師公會全聯會醫療政策建言書
 
97年研發替代役管考獎懲作業說明簡報(公告版)
97年研發替代役管考獎懲作業說明簡報(公告版)97年研發替代役管考獎懲作業說明簡報(公告版)
97年研發替代役管考獎懲作業說明簡報(公告版)
 
DS-012-產品設計與製程選擇
DS-012-產品設計與製程選擇DS-012-產品設計與製程選擇
DS-012-產品設計與製程選擇
 
PMT-007-生產管理
PMT-007-生產管理PMT-007-生產管理
PMT-007-生產管理
 
台灣經濟新藍圖系列之二 - 產業再造及全球連結
台灣經濟新藍圖系列之二 - 產業再造及全球連結台灣經濟新藍圖系列之二 - 產業再造及全球連結
台灣經濟新藍圖系列之二 - 產業再造及全球連結
 
Persona design method / ペルソナ概論
Persona design method / ペルソナ概論Persona design method / ペルソナ概論
Persona design method / ペルソナ概論
 
Ds 018 機械產業專業人才認證考試
Ds 018 機械產業專業人才認證考試Ds 018 機械產業專業人才認證考試
Ds 018 機械產業專業人才認證考試
 
Beocom2 Userguide Chinese Traditional
Beocom2 Userguide Chinese TraditionalBeocom2 Userguide Chinese Traditional
Beocom2 Userguide Chinese Traditional
 
認識腸病毒
認識腸病毒認識腸病毒
認識腸病毒
 
Web技術勉強会11回目
Web技術勉強会11回目Web技術勉強会11回目
Web技術勉強会11回目
 
H1n1
H1n1H1n1
H1n1
 

程式之美-微軟技術面試心得

  • 1. 第1章 遊戲之樂 ——遊戲中碰到的題目 研究院舉辦過幾屆手足球(foosball)公開賽,第一屆的冠軍是一位文靜的女實習生
  • 2. 程式之美-微軟技術面試心得 這一章的題目原本預計叫做「Problem Solving」——運用所學的知識解決問題,直譯為 「解決問題」,但似乎不太理想。事實上,這裡面大部分題目都是和遊戲相關的,因此, 本章改名為「遊戲之樂」。這些題目從遊戲和作者平時遇到的有趣問題出發,向程式設 計師提出挑戰。 個人電腦(PC)在蹣跚起步的時候,就被當時的主流觀點視為玩具。PC 的確有各種各 樣的遊戲,電腦上的遊戲是給人玩的,如果你願意,CPU 也可以讓人「玩」。 筆者曾經用「CPU 使用率」這個問題問了十幾個應聘者,一個典型的模式是: 我: 筆試考得怎麼樣?發揮了多少實力? 答: 我不習慣在紙上寫程式,平時都在電腦上寫…… 我: 那你對 Windows、作業系統這些東西熟悉嗎? 答: 還算是相當熟悉…… 我: 好,那你是否能在這台筆記型電腦上幫我解決一個問題——讓 CPU 的使用率劃出 一條水平線,比如就在 50%的地方。 這個時候可以觀察應聘者如下好幾個方面: 應聘者面對這個陌生問題時,如何開始分析。 有人知道觀察「工作管理員」如何執行,有人在紙上寫寫畫畫,有人明顯沒有 什麼想法。 當提示可以在網路上搜尋資料時,應聘者如何尋找資料,如何學習。 比如,有一位學生很快地用快捷鍵在 IE 中開啟了幾個 Tab 視窗,然後在每個視 窗輸入不同的搜尋關鍵字。當我提示在 MSDN 上找一些函數的時候,有些人根 本不知道 MSDN 網站應該怎麼用。有些人反覆讀了函數的說明,仍不知如何下 手。 在電腦上是如何寫程式,如何進行除錯的。 有人能很嫺熟地使用 C/C#的各種語言特性,很快地寫出程式,有人寫的程式編 譯了好幾次都不能通過,對編譯錯誤束手無策。程式第一次執行的時候,工作 管理員的 CPU 使用率不如預期,這時候有人就十分慌亂,在程式中瞎改一通, 希望能「矇」對。有人則有條理地分析,最後找到並解決問題。 2
  • 3. 第 1 章:遊戲之樂 我想,45 分鐘下來,應聘者的思考能力、學習能力、技術能力如何,應該都很清楚了。 行還是不行,雙方也都明白了。 這一章的其他題目大多和遊戲有關,同學們在玩《接龍》、《俄羅斯方塊》,甚至《魔 獸世界》的時候,有沒有產生好奇心——這個程式為什麼這麼酷,如果是我來寫,應該 怎麼做?有沒有把好奇心轉化為行動? 喜歡玩電腦、會玩電腦的人,也會運用電腦解決實際問題,這也是我們要找的人才。 3
  • 4. 程式之美-微軟技術面試心得 1.1 ★★★ 讓 CPU 使用率曲線聽你指揮 寫一個程式,讓使用者決定 Windows 工作管理員(Task Manager)的 CPU 使用率。 程式越精簡越好,電腦語言不限。例如,可以實現下列三種情況: CPU 的使用率固定在 50%,為一條直線。 CPU 的使用率為一條直線,但是實際使用率由命令行參數決定(參數範圍 1~ 100)。 CPU 的使用率狀態是一條正弦曲線。 4
  • 5. 第 1 章:遊戲之樂 分析與解法1 有一名學生寫了如下的程式碼: while(true) { if(busy) i++; else } 然後她就陷入了苦苦思索:else 要做什麼呢?怎樣才能讓電腦不做事情呢?CPU 使用 率為 0 的時候,到底是什麼東西在用 CPU?另一名學生花了很多時間構想如何「深入 核心,以控制 CPU 使用率」——可是事情真的有這麼複雜嗎? MSRA IEG(Microsoft Research Asia, Innovation Engineering Group)的一些實習生寫了 各種解法,他們寫的簡單程式可以達到如圖 1-1 所示的效果。 圖 1-1:寫程式控制 CPU 使用率呈現正弦曲線 作者注:當面試的同學聽到這個問題的時候,很多人都有點意外。我把我的筆記型電腦交給他們說,這是開卷考 試,你可以上網查資料,做什麼都可以。大部分面試者在電腦上的第一個動作就是上網搜尋「CPU 使用率 50%」 這樣的關鍵字,當然沒有找到什麼直接的結果。不過本書出版以後,情況可能就不一樣了。 5
  • 6. 程式之美-微軟技術面試心得 看來這並不是不可能完成的任務。讓我們仔細地回想一下寫程式時曾經碰到的問題, 如果我們不小心寫了一個無窮迴圈,CPU 使用率就會跳到最高,而且一直保持在 100%。我們也可以開啟工作管理員 ,實際觀察一下它是如何變動的。憑肉眼觀察, 2 它大約是 1 秒鐘更新一次。一般情況下,CPU 使用率會很低。但是,當使用者執行一 個程式,進行一些複雜操作的時候,CPU 的使用率會急遽升高。當使用者移動滑鼠時, CPU 的使用率也有小幅度的變化。 當工作管理員報告 CPU 使用率為 0 的時候,誰在使用 CPU 呢?透過工作管理員的「處 理程序(Process) 」一頁可以看到,System Idle Process 佔用了 CPU 空閒的時間——這 時候大家該回憶起在「作業系統」這門課上所學到的一些知識了吧。系統中有那麼多 ,它們什麼時候能「閒下來」呢?答案很簡單,這些程式或者在等待使 process(行程) 用者的輸入,或者在等待某些事件的發生 ,或者主動進入休眠狀態 。 3 4 在工作管理員的一個更新週期內,CPU 忙(執行應用程式)的時間和更新週期總時間 的比率,就是 CPU 的使用率,也就是說,工作管理員中顯示的是每個更新週期內 CPU 使用率的統計平均值。因此,我們可以寫一個程式,讓它在工作管理員的更新期間內 一會兒忙,一會兒閒,然後透過調整忙/閒的比例,就可以控制工作管理員中顯示的 CPU 使用率。 解法一:簡單的解法 要控制 CPU 的使用率曲線,就需要讓 CPU 在一段時間內(根據 Task Manager 的採樣率) 跑 busy 和 idle 兩個不同的迴圈(loop) ,從而透過不同的時間比例,來調節 CPU 使用率。 Busy loop 可以透過執行空迴圈來實現,idle 可以透過 Sleep()來實現。 問題的關鍵在於如何控制兩個 loop 的時間,我們先試驗一下 Sleep 一段時間,然後迴 圈 n 次,計算 n 的值。 那麼,對於一個空迴圈 for(i = 0; i < n; i++);又該如何計算這個最合適的 n 值 呢?我們都知道 CPU 執行的是機器指令,而最接近於機器指令的語言是組合語言,所 以我們可以先把這個空迴圈簡單地寫成如下組合語言程式碼後,再進行分析: 如果應聘者從來沒有研究過工作管理員,最好還是不要在簡歷上寫「精通 Windows」比較好。 例如 WaitForSingleObject()。 可以透過 Sleep()來實現。 6
  • 7. 第 1 章:遊戲之樂 loop: mov dx i ;將 i 置入 dx 暫存器 inc dx ;將 dx 暫存器加 1 mov i dx ;將 dx 中的值放回 i cmp i n ;比較 i 和 n jl loop ;i 小於 n 時則重複迴圈 假設這段程式碼要執行的 CPU 是 P4 2.4Ghz(2.4 * 10 的 9 次方個時鐘週期/每秒)。 現在 CPU 每個時鐘週期可以執行兩個以上的程式碼,那麼我們就取平均值兩個,於是 讓(2,400,000,000 * 2)/5=960,000,000(迴圈/秒),也就是說,CPU 1 秒鐘可以執行這 個空迴圈 960,000,000 次 不過 我們還是不能單純地將 n = 960,000,000 然後 Sleep(1000) 。 , , 了事。如果我們讓 CPU 工作 1 秒鐘,然後休息 1 秒鐘,波形很有可能就是鋸齒狀的—— 先達到一個峰值(>50%),然後跌到一個很低的使用率。 我 們 嘗 試 著 降 低 兩 個 數 量 級 , 令 n = 9,600,000 , 而 睡 眠 時 間 對 應 改 為 10 毫 秒 (Sleep(10)) 。選擇 10 毫秒是因為它不大也不小,比較接近 Windows 排程時間的間 隔。如果選得太小(如 1 毫秒) ,則會造成執行緒頻繁地被喚醒和暫停,無形中又增加 了核心時間的不確定性。最後我們可以得到程式碼清單 1-1。 程式碼清單 1-1 int main() { for(; ; ) { for(int i = 0; i < 9600000; i++) ; Sleep(10); } return 0; } 在不斷調整 9,600,000 的參數後,我們就可以在一台指定的機器上獲得一條大致穩定的 50% CPU 使用率直線。 使用這種方法要注意兩點影響: 儘量減少 sleep/awake 的頻率,減少作業系統核心排程程式的干擾。 儘量不要呼叫系統函數(比如 I/O 這些 privilege instruction) ,因為它也會導致很 多不可控制的核心執行時間。 7
  • 8. 程式之美-微軟技術面試心得 此方法的缺點也很明顯:不能在不同的機器上通用。一旦換了一個 CPU,我們又得重 新計算 n 值。有沒有辦法動態地瞭解 CPU 的運算能力,然後自動調節忙/閒的時間比 呢?請看下一個解法。 解法二:使用 GetTickCount()和 Sleep() 我們知道 GetTickCount()可以得到「系統啟動到現在」所經歷時間的毫秒值,最多能 夠統計到 49.7 天。我們可以利用 GetTickCount()來判斷 busy loop 要迴圈多久,程式碼 如清單 1-2 所示。 程式碼清單 1-2 int busyTime = 10; // 10 ms int idleTime = busyTime; // same ratio will lead to 50% cpu usage Int64 startTime = 0; while(true) { startTime = GetTickCount(); // busy loop while((GetTickCount() - startTime) <= busyTime) ; // idle loop Sleep(idleTime); } 這兩種解法都是假設目前系統上只有當前程式在執行,但實際上,作業系統中有很多 程式會同時執行各式各樣的任務,如果此刻其他行程(process)使用了 10% 的 CPU, 那我們的程式應該只能使用 40%的 CPU,這樣才能達到 50%的效果。 怎麼做呢?這就要用到另一個工具來幫忙——Perfmon.exe。 Perfmon 是從 Windows NT 開始就包含在 Windows 管理工具組中的專業檢測工具之一 (如 圖 1-2 所示)。Perfmon 可獲取有關作業系統、應用程式和硬體的各種效能計數器(perf counter)。Perfmon 的用法相當直接,只要選擇你所要檢測的物件(如處理器、RAM 或 硬碟),然後選擇效能計數器(如監視磁片的平均佇列長度)即可。 8
  • 9. 第 1 章:遊戲之樂 圖1-2:系統監視器(Perfmon) 我 們 可 以 寫 程 式 來 查 詢 Perfmon 的 值 , Microsoft .Net Framework 提 供 了 PerformanceCounter 這一物件,可以方便地得到各種效能資料,包括 CPU 的使用率。 例如下面這個程式(見程式碼清單 1-3)。 解法三:能動態調整的解法 程式碼清單 1-3 // C# code static void MakeUsage(float level) { PerformanceCounter p = new PerformanceCounter(quot;Processorquot;, quot;%Processor Timequot;, quot;_Totalquot;); if(p==NULL) { return } while(true) { if(p.NextValue() > level) System.Threading.Thread.Sleep(10); 9
  • 10. 程式之美-微軟技術面試心得 } } 上面的解法能方便地處理各種 CPU 使用率參數 這個程式可以解答前面提到的問題 2。 , 有了前面的知識累積,我們應該可以讓工作管理員畫出優美的正弦曲線了,見程式碼清 單 1-4。 解法四:正弦曲線 程式碼清單 1-4 // C++ code to make task manager generate sine graph #include quot;Windows.hquot; #include quot;stdlib.hquot; #include quot;math.hquot; const double SPLIT = 0.01; const int COUNT = 200; const double PI = 3.14159265; const int INTERVAL = 300; int _tmain(int argc, _TCHAR* argv[]) { DWORD busySpan[COUNT]; // array of busy times DWORD idleSpan[COUNT]; // array of idle times int half = INTERVAL / 2; double radian = 0.0; for(int i = 0; i < COUNT; i++) { busySpan[i] = (DWORD)(half + (sin(PI * radian) * half)); idleSpan[i] = INTERVAL - busySpan[i]; radian += SPLIT; } DWORD startTime = 0; int j = 0; while(true) { j = j % COUNT; startTime = GetTickCount(); while((GetTickCount() - startTime) <= busySpan[j]) ; Sleep(idleSpan[j]); 10
  • 11. 第 1 章:遊戲之樂 j++; } return 0; } 討論 如果機器是多 CPU,上面的程式會出現什麼結果?如何在多個 CPU 時顯示同樣的狀 態?例如,在雙核心的機器上,如果讓一個單執行緒的程式進行無窮迴圈,能讓兩個 CPU 的使用率達到 50%的水準嗎?為什麼? 多 CPU 的問題首先需要獲得系統的 CPU 資訊 可以使用 GetProcessorInfo()獲得多 。 處理器的資訊,然後指定行程在哪一個處理器上執行。其中指定執行使用的 CPU 是透 過 SetThreadAffinityMask()函數。 另外,還可以使用 RDTSC 指令獲取當前 CPU 核心執行週期數。 在 x86 平臺上定義函數: inline __int64 GetCPUTickCount() { __asm { rdtsc; } } 在 x64 平臺上定義: #define GetCPUTickCount() __rdtsc() 使用 CallNtPowerInformation API 得到 CPU 頻率,從而將週期數轉化為毫秒數,例如程 式碼清單 1-5 所示。 程式碼清單 1-5 _PROCESSOR_POWER_INFORMATION info; CallNTPowerInformation(11, // query processor power information NULL, // no input buffer 0, // input buffer size is zero 11
  • 12. 程式之美-微軟技術面試心得 &info, // output buffer Sizeof(info)); // outbuf size __int64 t_begin = GetCPUTickCount(); // do something __int64 t_end = GetCPUTickCount(); double millisec = ((double)t_end –(double)t_begin) /(double)info.CurrentMhz; RDTSC 指令讀取目前 CPU 的週期數,在多 CPU 系統中,這個週期數在不同的 CPU 之 間基數不同,頻率也有可能不同。利用從兩個不同的 CPU 得到的週期數來計算,會得 出沒有意義的值。如果執行緒在執行中被調動到了不同的 CPU,就會出現上述情況。 可用 SetThreadAffinityMask 避免執行緒遷移。另外,CPU 的頻率會隨系統供電及負荷 情況有所調整。 總結 能幫助你瞭解當前執行緒/行程/系統效能的 API 大致有以下這些。 Sleep():這個方法能讓當前執行緒「停」下來。 WaitForSingleObject():自己停下來,等待某個事件發生。 GetTickCount():有人把 Tick 翻譯成「滴答」,很傳神。 QueryPerformanceFrequency()、QueryPerformanceCounter():讓你取得更 精細的 CPU 資料。 timeGetSystemTime():是另一個得到高精度時間的方法。 PerformanceCounter:效能計數器。 GetProcessorInfo()/SetThreadAffinityMask() :遇到多核心的問題怎麼辦 呢?這兩個方法能夠幫你更有效地控制 CPU。 GetCPUTickCount():想拿到 CPU 核心執行週期數嗎?試試這個方法吧。 瞭解並應用了上面的 API,就可以考慮在履歷表中寫上「精通 Windows」了。 12
  • 13. 第 1 章:遊戲之樂 1.2 ★★★ 中國象棋將帥問題 下過象棋的人都知道,雙方的「將」和「帥」相隔遙遠,而且它們不能碰面。在象棋 殘局中,許多高手能利用這一規則走出巧妙的必殺招數。假設棋盤上只有「將」 「帥」 和 二子(如圖 1-3 所示)(為了下面敘述方便,我們約定用 A 表示「將」 表示 ,B 「帥」 。 ) 將 帥 圖 1-3:只有將帥的棋盤 A、B 二子被限制在己方 3×3 的格子裡移動。例如,在如上的表格裡,A 被正方形{d10, f10, d8, f8}包圍,而 B 被正方形{d3, f3, d1, f1}包圍。每一步,A、B 分別可以橫向或縱向移 動一格,但不能沿對角線移動。另外,A 不能面對 B,也就是說,A 和 B 不能處於同一 縱向直線上(如 A 在 d10 的位置,那麼 B 就不能在 d1、d2 以及 d3 的位置上)。 請寫一個程式,輸出 A、B 所有合法位置,並要求在程式碼中只能使用一個變數。 13
  • 14. 程式之美-微軟技術面試心得 分析與解法 問題的本身並不複雜,只要把所有 A、B 互相排斥的條件列舉出來就可以完成本題的要 求。由於本題要求只能使用一個變數,所以必須先想清楚在寫程式時,有哪些資訊需 要儲存,並且儘量高效率地儲存資訊。稍微思考一下,可以知道這個程式的框架大致 上是: 走遍 A 的位置 走遍 B 的位置 判斷 A、B 的位置組合是否滿足要求。 如果滿足,則輸出。 因此,需要儲存的是 A、B 的位置資訊,並且每次迴圈都要更新。為了能夠進行判斷, 首先需要建立一個邏輯的座標系統,以便檢測 A 何時會面對 B。這裡我們想到的方法 是用 1~9 的數字,按照行的優先順序來表示每個格點的位置(如圖 1-4 所示)。這樣, 只需要用「求餘數運算」就可以得到當前的列號,從而判斷 A、B 是否互斥。 圖 1-4:用 1~9 的數字表示 A、B 的座標 第二,題目要求只用一個變數,但是我們卻要儲存 A 和 B 兩個子的位置資訊,該怎麼 辦呢? 可以先把已知變數類型列舉一下,然後做些分析。 對於 bool 類型,是沒有辦法做任何擴展的,因為它只能表示 true 和 false 兩個值;而 byte 或 int 類型,它們能夠表達的資訊則更多。事實上,對本題來說,每個子都只需要 9 種可能值,就可以表達它的全部位置。 一個 8 位元的 byte 類型能夠表達 28=256 個值,所以用它來表示 A、B 的位置資訊綽綽有 餘,因此,可以把這個位元組的變數(設為 b)分成兩部分。用前面的 4 bit 表示 A 的位 置,用後面的 4 bit 表示 B 的位置,那麼 4 個 bit 可以表示 16 種可能值,這已經足夠了。 14
  • 15. 第 1 章:遊戲之樂 問題在於如何使用 bit 級的運算,將資料從這一 byte 變數的左邊和右邊分別存入和讀出。 下面是做法: 將 byte b(10100101)的右邊 4 bit(0101)設為 n(0011): 首先清除 b 右邊的 bits,同時保持左邊的 bits: 11110000(LMASK) & 10100101(b) --------------- 10100000 然後將上一步得到的結果與 n 做「或」運算 10100000(LMASK & b) ^ 00000011(n) ------------ 10100011 將 byte b(10100101)左邊的 4 bit(1010)設為 n(0011): 首先,清除 b 左邊的 bits,同時保持右邊的 bits: 00001111(RMASK) & 10100101(b) ----------- 00000101 現在,把 n 移動到 byte 資料的左邊 n << 4 = 00110000 然後對以上兩步得到的結果做「或」運算,即可得到最終結果。 00000101(RMASK & b) ^ 00110000(n << 4) ----------- 00110101 得到 byte 資料的右邊 4 bits 或左邊 4 bits(e.g. 10100101 中的 1010 以及 0101): 清除 b 左邊的 bits,同時保持右邊的 bits: 00001111(RMASK) & 10100101(b) ----------- 00000101 15
  • 16. 程式之美-微軟技術面試心得 清除 b 右邊的 bits,同時保持左邊的 bits: 11110000(LMASK) & 10100101(b) ----------- 10100000 將結果右移 4 bits 10100000 >> 4 = 00001010 最後的挑戰是如何在不宣告其他變數條件的前提下,建立一個 for 迴圈。可以重複利 用 1byte 的儲存單元,把它作為迴圈計數器,並用前面提到的存取和讀入技術進行操 作。還可以用巨集來抽象化程式碼,例如: for (LSET(b, 1); LGET(b) <= GRIDW * GRIDW; LSET(b, (LGET(b) + 1))) 解法一 程式碼清單 1-6 #define HALF_BITS_LENGTH 4 // 這個值是記憶儲存單元長度的一半,在這道題裡是 4bit #define FULLMASK 255 // 這個數字表示一個全部 bit 的 mask,以二進位表示是 11111111。 #define LMASK (FULLMASK << HALF_BITS_LENGTH) // 這個巨集表示左 bits 的 mask,以二進位表示是 11110000。 #define RMASK (FULLMASK >> HALF_BITS_LENGTH) // 這個數字表示右 bits 的 mask,以二進位表示是 00001111。 #define RSET(b, n) (b = ((LMASK & b) ^ n)) // 這個巨集將 b 的右邊設置成 n #define LSET(b, n) (b = ((RMASK & b) ^ (n << HALF_BITS_LENGTH))) // 這個巨集,將 b 的左邊設置成 n #define RGET(b) (RMASK & b) // 這個巨集得到 b 的右邊的值 #define LGET(b) ((LMASK & b) >> HALF_BITS_LENGTH) // 這個巨集得到 b 的左邊的值 #define GRIDW 3 // 這個數字表示將帥移動範圍的行寬度。 #include <stdio.h> #define HALF_BITS_LENGTH 4 #define FULLMASK 255 #define LMASK (FULLMASK << HALF_BITS_LENGTH) #define RMASK (FULLMASK >> HALF_BITS_LENGTH) #define RSET(b, n) (b = ((LMASK & b) ^ n)) 16
  • 17. 第 1 章:遊戲之樂 #define LSET(b, n) (b = ((RMASK & b) ^ (n << HALF_BITS_LENGTH))) #define RGET(b) (RMASK & b) #define LGET(b) ((LMASK & b) >> HALF_BITS_LENGTH) #define GRIDW 3 int main() { unsigned char b; for(LSET(b, 1); LGET(b) <= GRIDW * GRIDW; LSET(b, (LGET(b) + 1))) for(RSET(b, 1); RGET(b) <= GRIDW * GRIDW; RSET(b, (RGET(b) + 1))) if(LGET(b) % GRIDW != RGET(b) % GRIDW) printf(quot;A = %d, B = %dnquot;, LGET(b), RGET(b)); return 0; } 輸出 格子的位置用 N 來表示,N = 1, 2, …, 8, 9,依照行的優先順序,如圖 1-5 所示。 「將」(A)的格子 「帥」(B)的格子 圖 1-5:「將」與「帥」的格子位置 A = 1, B = 2 A = 4, B = 2 A = 7, B = 2 A = 1, B = 3 A = 4, B = 3 A = 7, B = 3 A = 1, B = 5 A = 4, B = 5 A = 7, B = 5 A = 1, B = 6 A = 4, B = 6 A = 7, B = 6 A = 1, B = 8 A = 4, B = 8 A = 7, B = 8 A = 1, B = 9 A = 4, B = 9 A = 7, B = 9 17
  • 18. 程式之美-微軟技術面試心得 A = 2, B = 1 A = 5, B = 1 A = 8, B = 1 A = 2, B = 3 A = 5, B = 3 A = 8, B = 3 A = 2, B = 4 A = 5, B = 4 A = 8, B = 4 A = 2, B = 6 A = 5, B = 6 A = 8, B = 6 A = 2, B = 7 A = 5, B = 7 A = 8, B = 7 A = 2, B = 9 A = 5, B = 9 A = 8, B = 9 A = 3, B = 1 A = 6, B = 1 A = 9, B = 1 A = 3, B = 2 A = 6, B = 2 A = 9, B = 2 A = 3, B = 4 A = 6, B = 4 A = 9, B = 4 A = 3, B = 5 A = 6, B = 5 A = 9, B = 5 A = 3, B = 7 A = 6, B = 7 A = 9, B = 7 A = 3, B = 8 A = 6, B = 8 A = 9, B = 8 考慮了這麼多因素,總算得到了本題的一個解法,但是 MSRA 裡卻有人說,下面的一 小段程式碼也能達到同樣的目的: BYTE i = 81; while(i--) { if(i / 9 % 3 == i % 9 % 3) continue; printf(「A = %d, B = %dn」, i / 9 + 1, i % 9 + 1); } 但是很快又有另一個人說他的解法才是效率最高的: 程式碼清單 1-7 struct { unsigned char a:4; unsigned char b:4; } i; for(i.a = 1; i.a <= 9; i.a++) for(i.b = 1; i.b <= 9; i.b++) if(i.a % 3 != i.b % 3) printf(「A = %d, B = %dn」, i.a, i.b); 讀者能自己證明一下嗎? 5 這一題目由微軟亞洲研究院工程師 Matt Scott 提供,他在學習象棋時想出了這個題目,後來一位應聘者給了比他 的「正解」簡明很多的答案,他們現在成了同事。 18