程式設計也快樂第三期解答(四)

黃志斌發表於2013-04-17

上篇文章中的演算法3在給定問題有解的情況下一定可以找到解答,但它給出的解答的操作步驟不一定是最少的。 現在讓我們來看看演算法4吧。

水(Water.cs)

  1: using System;
  2: using System.IO;
  3: using System.Linq;
  4: using System.Diagnostics;
  5: using System.Collections.Generic;
  6: using Skyiv.Extensions;
  7: 
  8: namespace Skyiv.Ben.Pond
  9: {
 10:   sealed class Water
 11:   {
 12:     TextWriter writer;    // 往哪寫?
 13:     int target;           // 要達到的目標
 14:     Kettle[] kettles;     // 水壺們: 最少兩個
 15:     List<string> actions; // 記錄解答步驟的列表
 16:     List<int[]> currents; // 記錄各水壺水量的列表
 17:     
 18:     public Water(TextWriter writer, int limit, int target, params int[] capacitys)
 19:     { // 建構函式:本演算法無視步數限制,所以忽略了(limit)引數
 20:       this.writer = writer;
 21:       this.target = target;
 22:       if (target < 0) throw new ArgumentException("目標水量不能小於零");
 23:       kettles = new Kettle[capacitys.Length];
 24:       for (var i = 0; i < capacitys.Length; i++)
 25:       {
 26:         if (capacitys[i] < 0) throw new ArgumentException("水壺的容量不能小於零");
 27:         kettles[i] = new Kettle(((char)('A' + i)).ToString(), capacitys[i]);
 28:       }
 29:       actions = new List<string>();
 30:       currents = new List<int[]>();
 31:     }
 32:     
 33:     public void Solve()
 34:     { // 演算法4:只使用前兩隻水壺,試圖尋找最少操作步驟的解
 35:       Trace.Listeners.Add(new TextWriterTraceListener(writer)); // -d:TRACE
 36:       writer.Write("目標:{0} 水壺們:", target);
 37:       foreach (var kettle in kettles)  writer.Write(kettle);
 38:       writer.WriteLine(); // 以上三行輸出題目的條件
 39:       if (HasSolution())
 40:       {
 41:         var n = GetTargetKettle();
 42:         var count = Solve(n < 0 ? 0 : n);
 43:         if (n < 0 && count < Solve(1)) Solve(0);
 44:         Report();
 45:       }
 46:       else writer.WriteLine("*** 無解(本演算法僅使用前兩個水壺) ***");
 47:     }
 48:     
 49:     int Solve(int n)
 50:     { // 尋找解決方案,返回所需操作步驟的數目
 51:       SetTargetKettle(n);
 52:       while (!kettles[0].IsEmpty || !kettles[1].IsEmpty)
 53:       { // 往回倒推,如果最後得到兩隻空壺就成功了
 54:         Trace.Assert(kettles[0].IsEmpty || kettles[1].IsEmpty);// 剛好一壺為空
 55:         if (kettles[1].IsEmpty && kettles[0].IsFull) { Clean(0); break; }
 56:         if (kettles[0].IsEmpty && kettles[1].IsFull) { Clean(1); break; }
 57:         int i = kettles[0].IsEmpty ? 0 : 1, j = 1 - i;
 58:         FullUp(i); // 挑選一隻空壺,裝滿之 (此時必有一壺為空,另一壺不空也不滿)
 59:         while (!kettles[i].IsEmpty)
 60:         { // 從剛裝滿的壺倒入另一壺,直到本壺為空
 61:           Move(i, j);
 62:           if (kettles[j].IsFull) Clean(j); // 如另一壺滿了,則倒空之
 63:         }
 64:       }
 65:       return StepCount();
 66:     }
 67:     
 68:     bool HasSolution()
 69:     { // 檢查是否有解
 70:       var imax = (kettles[0].Capacity > kettles[1].Capacity) ? 0 : 1;
 71:       var gcd = MathExtensions.Gcd(kettles[0].Capacity, kettles[1].Capacity);
 72:       return (gcd == 0 || target %  gcd == 0) && target <= kettles[imax].Capacity;
 73:     }
 74:     
 75:     int GetTargetKettle()
 76:     { // 目標水量應設定到哪隻壺裡? (-1)表示未定
 77:       var min = (kettles[0].Capacity < kettles[1].Capacity) ? 0 : 1;
 78:       return (target > kettles[min].Capacity) ? (1 - min)
 79:         : (target == 0 || target == kettles[min].Capacity) ? min : -1;
 80:     }
 81:     
 82:     void SetTargetKettle(int n)
 83:     { // 設定(n)壺的水量為目標水量,並做些準備工作
 84:       kettles[n].FullUp();
 85:       kettles[n].Move(new Kettle(null, kettles[n].Capacity - target));
 86:       actions.Clear();
 87:       currents.Clear();
 88:       Mark(null); // 記錄最終完成時的各水壺的水量
 89:     }
 90:     
 91:     int StepCount()
 92:     { // 返回操作步驟的數目
 93:       var i = Math.Max(0, actions.Count - 1);
 94:       while (i >= 0 && currents[i][0] != target && currents[i][1] != target) i--;
 95:       return actions.Count - i;
 96:     }
 97: 
 98:     void FullUp(int n)
 99:     { // 倒推步驟:裝滿(n)壺,解答步驟:倒空(n)壺
100:       kettles[n].FullUp();
101:       Mark("倒空" + kettles[n].Name);
102:     }
103:     
104:     void Clean(int n)
105:     { // 倒推步驟:倒空(n)壺,解答步驟:裝滿(n)壺
106:       kettles[n].Clean();
107:       Mark("裝滿" + kettles[n].Name);
108:     }
109:     
110:     void Move(int n, int m)
111:     { // 倒推步驟:從(n)壺倒入(m)壺,解答步驟:從(m)壺倒入(n)壺
112:       kettles[n].Move(kettles[m]);
113:       Mark("從" + kettles[m].Name + "倒入" + kettles[n].Name);
114:     }
115:     
116:     void Mark(string action)
117:     { // 記錄當前步驟和各水壺的水量
118:       if (action != null) actions.Add(action);
119:       currents.Add(kettles.Select(x => x.Current).ToArray());
120:     }
121:     
122:     void Report()
123:     { // 報告解答步驟
124:       for (var i = actions.Count - 1; i >= 0; i--)
125:       {
126:         writer.Write("{0, 3}: ", actions.Count - i);
127:         writer.Write("{0} (", actions[i].ChinesePadRight(8));
128:         foreach (var current in currents[i]) writer.Write("{0,2} ", current);
129:         writer.WriteLine(")"); // 下一行不是必須的,只是為了縮短解答步驟
130:         if (currents[i][0] == target || currents[i][1] == target) break;
131:       }
132:       writer.WriteLine("---- 大功告成 --------");
133:     }
134:   }
135: }

這個Water類的核心內容其實還是演算法3的那一套,也是往回倒推,大部分內容都是相同的。

  • 第 68~73 行的HasSolution方法檢查是否有解。
  • 第 75~80 行的GetTargetKettle方法決定目標水量應設定到哪隻壺裡。
    • 如果目標水量大於小壺的容量,則只能設定到大壺裡。(第 78 行)
    • 如果目標水量等於零或者等於小壺的容量,則設定到小壺裡。(第 79 行)
    • 否則目標水量就一定大於零而小於小壺的容量,我們無法決定應該設定到哪隻壺裡。(第 79 行)
  • 第 82~89 行的SetTargetKettle方法設定指定的壺的水量為目標水量,然後清除記錄解答步驟和各水壺水量的列表、記錄最終完成時各水壺的水量。
  • 第 91~96 行的StepCount計算已經求解出來的操作步驟的數目。(我懷疑執行到第 95 行時i值總是 1 或者 0(當 actions.Count == 0 時),不過沒有證據)
  • 第 33~47 行的Solve方法是主控程式,根據需要呼叫後面的過載的Solve方法 0~3 次,然後報告執行結果。
    • 如果判斷出給定的問題無解,則直接報告。(第 39 行、第 46 行)
    • 否則,先獲取目標水量應設定到哪隻壺裡。(第 41 行)
    • 如果明確知道目標水量應設定到哪隻壺裡,直接呼叫過載的Solve方法解答之。(第 42 行)
    • 否則,先試著將目標水量設定到到第一隻壺中,呼叫過載的Solve方法得到操作步驟的數目。(第 42 行)
    • 接著試著將目標水量設定到第二隻壺中,再次呼叫過載的Solve方法得到操作步驟的數目。(第 43 行)
    • 如果前者小於後者,只好第三次呼叫過載的Solve方法以得到前者對應的操作步驟。(第 43 行)
    • 否則就沒有必要再幹什麼了,直接報告答案就行了。(第 44 行)
  • 第 49~66 行的過載的Solve方法基本上和演算法3中相同。只不過在往回倒推之前先設定目標水量到指定的壺中,計算完畢後再返回操作步驟的數目給呼叫者。
  • 這個程式的其他方面和演算法3是一樣的。

編譯和執行

Water$ make
dmcs -out:ConsoleRunner.exe -d:TRACE ConsoleRunner.cs Water.cs Kettle.cs StringExtensions.cs MathExtensions.cs
$ mono ConsoleRunner.exe 3 5 6
目標:3 水壺們:[A:0/5][B:0/6]
  1: 裝滿A    ( 5  0 )
  2: 從A倒入B ( 0  5 )
  3: 裝滿A    ( 5  5 )
  4: 從A倒入B ( 4  6 )
  5: 倒空B    ( 4  0 )
  6: 從A倒入B ( 0  4 )
  7: 裝滿A    ( 5  4 )
  8: 從A倒入B ( 3  6 )
---- 大功告成 --------

可以看到,同樣的輸入條件(目標水量 3,水壺容量分別為 5 和 6),本演算法只需要 8 步,而演算法3需要 10 步。再看一個例子:

$ mono ConsoleRunner.exe 3 4 7
目標:3 水壺們:[A:0/4][B:0/7]
  1: 裝滿B    ( 0  7 )
  2: 從B倒入A ( 4  3 )
---- 大功告成 --------

從上面兩個例子中可以看出,在目標水量大於零而小於小壺容量的情況下,為了求得最少操作步驟的解,有時需要將目標水量設定到大壺中,有時又需要將目標水量設定到小壺中。

相關文章