前段時間抽空看了《說謊者悖論和漢諾塔遊戲》([加拿大]馬塞爾·丹尼斯著,程雲琦譯)一書,作者在第一個謎題“斯芬克斯之謎”中介紹了法國耶稣會詩人 Claude Gaspard Bachet de Méziriac(1581-1638)的一個經典謎題:
若天平兩端可以任意放置砝碼,要稱量從1磅到40磅的整磅重的糖,天平所需要的砝碼個數最少是多少?
換句話說,我們需要确定若幹個讀數互不相同的砝碼,用它們在天平上組合出1至40的數。當然,前提是這是台特殊的天平——你不能開玩笑說天平上還有遊碼,也不能說砝碼的規格是有限定的。我們假定砝碼值可以是任意正整數。傳統上我們可能認為1,"Times New Roman";" >2,4,8,16和32磅共7個砝碼,這樣最高可以稱出63磅重的糖。不過這個謎題有意思的地方就在于它使用的是天平quot;Times New Roman";" >——兩邊都可以放砝碼。于是我們可以這樣來稱出2磅重的糖:把3磅重的砝碼放在天平的左邊,1磅重的砝碼放在天平的右邊,在天平右邊放上糖直到天平平衡。這樣,隻需要1,3,9和27磅的砝碼就可以稱出1磅到40磅的糖。
注意到這些砝碼的重量都是3的幂:1=30,3=31,9=32,27=33。依次類推,我們可以使用1,3,9,27和81磅的砝碼稱出1磅到121磅的糖。
那麼針對具體數量的糖,怎樣放置各個砝碼呢?作者給出了如下圖所示的稱法。
圖1砝碼放置
作者似乎想表達的是,具體的砝碼放置法的獲得是來源于所謂的“頓悟思維(insight thinking)”。不過寫個具體的算法來實作針對具體數值的砝碼放置方案并不難。
我們同樣假定稱量時将糖放置在天平右邊。那麼算法就是要實作,在輸入砝碼個數和需要稱量的最大數值後,輸出如圖1所示的清單。若稱量的數值超過砝碼所能稱量的最大數值,則提示出錯(比如四個砝碼不能稱出41磅糖)。
我們假設要稱量的數值為n。算法的基本思想是,通過判斷n所在的“區間”,疊代地把砝碼加到天平左右兩側。這裡的“區間”指的是,将砝碼的值按從小到大累積相加,形成一個新的數列,其中n所在的前開後閉區間。如,對于砝碼1,3,9和27,其累積和形成的數列為1,4,13,40。如3所在的區間為(1,4],而13所在的區間為(4,13]。
仍假定n=5。算法的運作過程如下:
1.判斷得n∈(4,13]。13是1、3和9的累積和,将最大的砝碼9放入左邊天平。砝碼9的符号為正。
2.令n = n – 9 = 5 – 9 = - 4,判斷得|n|∈(1,4]。4是1和3的累積和,且由于n的符号為負,故将最大的砝碼3放入右邊天平。砝碼3的符号為負。
3.繼續令n = n – (-3) = (-4) – (-3) = - 1,判斷得|n|∈(1,1]。1是1的累積和,且由于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);
}
}
}