天平稱重謎題

 

前段時間抽空看了《說謊者悖論和漢諾塔遊戲》([加拿大]馬塞爾·丹尼斯著,程雲琦譯)一書,作者在第一個謎題斯芬克斯之謎中介紹了法國耶穌會詩人 Claude Gaspard Bachet de Méziriac1581-1638)的一個經典謎題:

若天平兩端可以任意放置砝碼,要稱量從140的整磅重的糖,天平所需要的砝碼個數最少是多少?

換句話說,我們需要確定若干個讀數互不相同的砝碼,用它們在天平上組合出140的數。當然,前提是這是臺特殊的天平——你不能開玩笑說天平上還有遊碼,也不能說砝碼的規格是有限定的。我們假定砝碼值可以是任意正整數。傳統上我們可能認爲1"Times New Roman";" lang="EN-US">24816327個砝碼,這樣最高可以稱出63的糖。不過這個謎題有意思的地方就在於它使用的是天平quot;Times New Roman";" lang="EN-US">——兩邊都可以放砝碼。於是我們可以這樣來稱出2重的糖:把3重的砝碼放在天平的左邊,1重的砝碼放在天平的右邊,在天平右邊放上糖直到天平平衡。這樣,只需要13927的砝碼就可以稱出140的糖。

注意到這些砝碼的重量都是3的冪:1=303=319=3227=33。依次類推,我們可以使用1392781的砝碼稱出1121的糖。

那麼針對具體數量的糖,怎樣放置各個砝碼呢?作者給出瞭如下圖所示的稱法。

 

 

1砝碼放置

作者似乎想表達的是,具體的砝碼放置法的獲得是來源於所謂的“頓悟思維(insight thinking)”。不過寫個具體的算法來實現針對具體數值的砝碼放置方案並不難。

我們同樣假定稱量時將糖放置在天平右邊。那麼算法就是要實現,在輸入砝碼個數和需要稱量的最大數值後,輸出如圖1所示的列表。若稱量的數值超過砝碼所能稱量的最大數值,則提示出錯(比如四個砝碼不能稱出41糖)。

我們假設要稱量的數值爲n。算法的基本思想是,通過判斷n所在的“區間”,迭代地把砝碼加到天平左右兩側。這裏的“區間”指的是,將砝碼的值按從小到大累積相加,形成一個新的數列,其中n所在的前開後閉區間。如,對於砝碼13927,其累積和形成的數列爲141340。如3所在的區間爲(14],而13所在的區間爲(413]

仍假定n=5。算法的運行過程如下:

1.判斷得n(413]13139的累積和,將最大的砝碼9放入左邊天平。砝碼9的符號爲正。

2.令n = n – 9 = 5 – 9 = - 4,判斷得|n|(14]413的累積和,且由於n的符號爲負,故將最大的砝碼3放入右邊天平。砝碼3的符號爲負。

3.繼續令n = n – (-3) = (-4) – (-3) = - 1,判斷得|n|(11]11的累積和,且由於n的符號爲負,故將最大的砝碼1放入右邊天平。砝碼1的符號爲負。

4.繼續令n = n – (-1) = (-1) – (-1) = 0。結果爲零,算法結束。

在以上的每一步中我們都得到了一個所謂的“最大的砝碼”,以下這個砝碼我們稱之爲上界砝碼

算法是一個迭代的過程,用僞代碼描述如下。

輸入:數值n

輸出:各個砝碼的符號

Procedure setBalance( int n) {

While (n != 0) {

      找出n的上界砝碼,令其符號與n相同。

      n = n –上界砝碼。

}

}

 

從僞代碼來看,算法非常簡潔。不過要成爲可運行的程序,還需要有一些輔助性工作。以下是用Java實現該算法的參考代碼。

/*
 * @(#)Balance.java  1.0 10/06/13
 *
 * Copyright 2010 Onion Lab. All rights reserved.
 */
package problem.balance;
/**
 * Solves the classic problem: balance.
 * @author Chungtow Lai - Zhejiang University
 * @version $Date: 2010-06-13
 */
public class Balance {
       private static final int NUM_OF_POISE = 4;
       int number;
       int[] left, right;
       int[] sign;
      
       /**
        * Construction method.
        */
       public Balance(int number) {
              this.number = number;
             
              init();
             
              if (number > getSumToK(NUM_OF_POISE)) {
                     System.out.println("Error: Out of bound!");
              } else {
                     setBalance();
                     print();
              }
       }
       /**
        * Initialization
        */
       private void init() {
              left = new int[NUM_OF_POISE];
              right = new int[NUM_OF_POISE];
              sign = new int[NUM_OF_POISE];
             
              for (int i = 0; i < NUM_OF_POISE; i ++) {
                     sign[i] = 0;
              }
       }
      
       /**
        * Main algorithm
        */
       private void setBalance() {
              int temp1, temp2, upper;
             
              temp1 = number;
              while (temp1 != 0) {
                     temp2 = Math.abs(temp1);
                     upper = getUpper(temp2);
                     sign[upper] = temp1 / temp2;
                     temp1 -= sign[upper] * power(3, upper);
              }
       }
      
       private void print() {
              int i;
              int lj = 0, rj = 0;
              for (i = 0; i < NUM_OF_POISE; i ++) {
                     if (sign[i] > 0) {
                            left[lj++] = power(3, i);
                     } else if (sign[i] < 0) {
                            right[rj++] = power(3, i);
                     }
              }
             
              System.out.print(this.number + "/t/t");
              for (i = 0; i < lj; i ++) {
                     System.out.print(left[i]);
                     if (i != lj-1) {
                            System.out.print(",");
                     }
              }
              System.out.print("/t/t");
              for (i = 0; i < rj; i ++) {
                     System.out.print(right[i]);
                     if (i != rj-1) {
                            System.out.print(",");
                     }
              }
              System.out.print("/t/t");
              for (i = 0; i < NUM_OF_POISE; i ++) {
                     int t = sign[i] * power(3, i);
                     if (t > 0) {
                            System.out.print("+" + t);
                     } else if (t < 0) {
                            System.out.print(t);
                     }
              }
              System.out.println();
       }
      
       /**
        *
        */
       private int getUpper(int n) {
              for (int i = 1; i <= NUM_OF_POISE; i ++) {
                     if ((n > getSumToK(i)) && (n <= getSumToK(i+1))) {
                            return i;
                     }
              }
              return 0;
       }
      
       /**
        * 返回前k個數的和
        */
       private static int getSumToK(int k) {
              if (k == 1) return 1;
              else return (power(3,k-1) + getSumToK(k-1));
       }
      
       /**
        * Returns the value of the first argument raised to the power of the
     * second argument.
     * @param x the base.
     * @param e the exponent.
     * @return the value <code>x<sup>e</sup></code>.
        */
       private static int power(int x, int e) {
              if (e == 0) return 1;
              else return x * power(x, e-1);
       }
      
    /**
     * Main entry point for invocation.
     * @param args The command line arguments.
     */
       public static void main(String[] args) {
              System.out.println("Number/t/tLeft/t/tRight/t/tFormula");
              for (int i = 1; i <= 40; i ++) {
                     new Balance(i);
              }
       }
}




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章