這個游戲我是在光榮出的大航海時代4威力加強版(一個蠻古老的單機版PC游戲)上第一次見到的。與其說是個游戲,不如說是個智力題。這個題目是這樣的:
有5個硬幣,三個正面,兩個反面,最初是間隔排列的,如圖1所示。
圖 1 五枚硬幣的原始排列
每次移動只能移動相鄰的2個不同的硬幣,也就是移動的這兩個硬幣一定要一個是正面一個是反面,並且兩個硬幣是相鄰的。可以向左或向右移動,但是移動的那個方向上必須有相鄰的硬幣。移動時還要跨過相鄰的硬幣。舉個例子,比如第1、2兩枚硬幣可以向右移動,但是不可以向左移動,因為左邊沒有相鄰的硬幣。向右移動時要跨過右邊所有相互挨著的硬幣,如圖2所示。
圖 2 移動前兩個硬幣
如果移動方向上硬幣是不連續的,則只能移動到第一個可以放硬幣的地方,比如下面的例子,要將從左邊數第3、第4兩枚硬幣向左邊移動,則只能移動到中間的空位,不能跳過空位移到最左邊,如圖3所示。
圖 3 另一個移動的例子
最終的目標是要移動成正的在一邊,反的在另一邊。
圖 4 最終的目標
這個問題看似蠻簡單的,但真正做起來就發現還是挺難的。我試了好久才找到答案。
但是這個題用計算機來解算卻並不難,並且非常適於用遞歸算法來解算。下面是我編寫的程序,因為這個程序的計算量不大,所以基本沒有考慮計算效率問題,而是使程序盡量的簡單、直白。
程序中用了個字符串來存放這些硬幣的信息,’A’ 表示正面,’B’ 表示反面,空格表示空位。因此,字符串初始化為:" ABABA "。挪動硬幣對應的就是改變字符串中AB的位置。相關的挪動硬幣的操作放到了move 函數中。move 函數中傳進一個整型參數 i,用來標識當前挪動的是哪兩個硬幣,向左挪動還是向右挪動。具體方法為,i的最低的一個bit 表示挪動方向,換句話說當i 為偶數時表示向左挪動,i為奇數時表示向右挪動硬幣。i 的其余bit 表示當前挪動的是哪兩個硬幣。Move函數的返回值表示這次挪動是否成功了。
具體代碼如下:
[cpp]
static bool move(string &coins, int i)
{
int dir = i % 2; // 0 表示左移,1 表示右移
i = i / 2;
if(coins[i] == ' ' || coins[i + 1] == ' ' || coins[i] == coins[i + 1])
{
return false; //無法移動當前硬幣
}
if( dir == 0 ) //向左移
{
if( coins[ i - 1] == ' ') //左邊無硬幣,不能移動
{
return false;
}
else
{
string::size_type j = i - 2;
while( coins[j] != ' ' && j > 1) {j --;}
coins[j - 1] = coins[i];
coins[j] = coins[i + 1];
coins[i] = ' ';
coins[i + 1] = ' ';
}
}
else
{
if( coins[i + 2] == ' ') //右邊無硬幣,不能移動
{
return false;
}
else
{
string::size_type j = i + 3;
while(coins[j] != ' ' && j < coins.size() - 1) {j ++;}
coins[j] = coins[i];
coins[j + 1] = coins[i + 1];
coins[i] = ' ';
coins[i + 1] = ' ';
}
}
return true;
}
isSeperated 函數判斷是否將硬幣分開了,算法很簡單,肯定有更高效的算法,我沒有在這上面多花心思,因為對於這個小程序來說,肯定是寫程序所花的時間遠大於程序運行所話的時間。因此我追求的是如何能快速的寫完這個程序,而不是如何讓這個程序跑的更快。下面是代碼:
[cpp]
static bool isSeperated(string coins)
{
int first = 0, last = coins.size() - 1;
while(coins[first] == ' ') {first ++;} //找到這些硬幣的開始位置
while(coins[last] == ' ') {last --;} //找到這些硬幣的結束位置
if(coins[first] == coins[last] ) return false;// 頭尾的兩個硬幣相同,肯定沒有排列好
int i = first + 1, j = last - 1;
while(coins[i] == coins[first] || coins[i] == ' '){i ++;} //找到第一個與頭硬幣不同的硬幣
while(coins[j] == coins[last] || coins[j] == ' '){j --;} //找到最後一個與尾硬幣不同的硬幣
if( i != j + 1) return false;
return true; // 到這裡說明已經排列好了
}
Solve 函數負責解算,算法很簡單,簡單的說就是窮舉法,把可能的移動方法都試一下,必然就能得到正確的步驟了。唯一需要說明的是對於這個問題,可以移動的步數是無窮的。所以要限定搜索深度,也就是要限定最多移動多少步,這樣窮舉才能有個盡頭。按層數(步數)搜索自然是遞歸算法寫起來最方便。下面是代碼:
[cpp]
bool solve(string coins, int depth)
{
string coins2 = coins;
int first = 0;
int last = coins.size() - 1;
while(coins[first] == ' ') {first ++;}
while(coins[last] == ' ') {last --;}
for( int i = 2 * first; i < 2 * (last - 1); i++ )
{
if( move(coins, i) )
{// 表明可以這樣移動
depth --; // 記錄移動了幾步
if( isSeperated(coins) )
{
// 完成了,輸出結果
cout << coins << endl;
return true;
}
else if( depth != 0 && solve(coins, depth) )// 在當前移動的基礎上繼續移動
{
cout << coins << endl;
return true;
}
else
{
// 到這裡說明當前的移動是不對的
coins = coins2;
depth ++;
continue; // 試探下一種移動
}
}
}
// 到這裡說明所有的移動方式都試過了,全都不行
return false;
}
最後是主程序,就幾行,不用多解釋:
[cpp]
int main()
{
string coins(" ABABA ");
if(solve(coins, 5))
{
cout << coins << endl;
}
return 0;
}
程序輸出的結果如下:
AAABB
ABAA B
A ABAB
AABAB
ABAAB
ABABA
需要說明的是,因為是遞歸,所以輸出的結果要從下往上看。