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

黃志斌發表於2013-04-10

2gua 今天上午在第三波 - 程式設計贏取《精益創業實戰》中釋出以下編碼任務:


假設有一個池塘,裡面有無窮多的水。現有2個空水壺,容積分別為5升和6升。如何只用這2個水壺從池塘裡取得3升的水(最後,這三升水,在其中一個壺裡)
用你熟悉的語言編碼實現,程式碼極客、高效為評判依據。


我試著用 C# 語言程式設計解答。

水壺(Kettle.cs)

 1: using System;
 2: 
 3: namespace Skyiv.Ben.Pond
 4: {
 5:   sealed class Kettle
 6:   {
 7:     public string Name { get; private set; }  // 水壺的名稱
 8:     public int Capacity { get; private set; } // 最大容量
 9:     public int Current { get; private set; }  // 當前水量
10:     
11:     public Kettle(string name, int capacity)
12:     { // 建構函式
13:       Name = name;
14:       Capacity = capacity;
15:     }
16:     
17:     public string Clean()
18:     { // 倒空水壺
19:       Current = 0;
20:       return string.Format("[倒空:{0}]", Name);
21:     }
22:     
23:     public string FullUp()
24:     { // 裝滿水壺
25:       Current = Capacity;
26:       return string.Format("[裝滿:{0}]", Name);
27:     }
28:     
29:     public string Move(Kettle other)
30:     { // 將水倒入另一隻壺中,直至本壺空或另一壺滿
31:       var value = Math.Min(Current, other.Capacity - other.Current);
32:       Current -= value;
33:       other.Current += value;
34:       return string.Format("[從{0}倒入{1}:{2}]", Name, other.Name, value);
35:     }
36:     
37:     public override string ToString()
38:     { // 報告水壺自身的狀態
39:       return string.Format("[{0}:{1}/{2}]" , Name, Current, Capacity);
40:     }
41:   }
42: }

這個水壺太簡單,沒什麼需要解釋的。

水(Water.cs)

 1: using System;
 2: using System.IO;
 3: using Skyiv.Extensions;
 4: 
 5: namespace Skyiv.Ben.Pond
 6: {
 7:   sealed class Water
 8:   {
 9:     TextWriter writer; // 往哪寫?
10:     int limit;         // 解決方案的步數限制
11:     int target;        // 要達到的目標
12:     Kettle[] kettles;  // 水壺們: 最少兩個
13:     
14:     public Water(TextWriter writer, int limit, int target, params int[] capacitys)
15:     { // 建構函式
16:       this.writer = writer;
17:       this.limit = limit;
18:       this.target = target;
19:       kettles = new Kettle[capacitys.Length];
20:       for (var i = 0; i < capacitys.Length; i++)
21:         kettles[i] = new Kettle(((char)('A' + i)).ToString(), capacitys[i]);
22:     }
23:     
24:     public void Solve()
25:     { // 尋找解決步驟
26:       writer.Write("步數限制:{0} 目標:{1} 水壺們:", limit, target);
27:       foreach (var kettle in kettles)  writer.Write(kettle);
28:       writer.WriteLine(); // 以上三行輸出題目的條件
29:       for (var i = 0; ; )
30:       { // 主迴圈,演算法1
31:         Solve(++i, kettles[1].FullUp);
32:         Solve(++i, kettles[1].Move, kettles[0]);
33:         Solve(++i, kettles[0].Clean);
34:         Solve(++i, kettles[1].Move, kettles[0]);
35:       }
36:     }
37: 
38:     void Solve(int step, Func<string> func)
39:     { // 適用於無引數動作:倒空水壺、裝滿水壺
40:       Solve(step, func());
41:     }  
42:     
43:     void Solve(int step, Func<Kettle, string> func, Kettle other)
44:     { // 適用於一個引數的動作:將水倒入另一隻壺中
45:       Solve(step, func(other));
46:     }  
47:     
48:     void Solve(int step, string func)
49:     { // 解決步驟的一步
50:       writer.Write("{0,3}: {1}", step, func.ChinesePadRight(14));
51:       foreach (var kettle in kettles) writer.Write(kettle);
52:       writer.WriteLine();
53:       var isFinished = IsFinished();
54:       if (step < limit && !isFinished) return;
55:       if (isFinished) writer.WriteLine("---- 大功告成 ---------------");
56:       else if (step >= limit) writer.WriteLine("**** 出師未捷身先死 ****");
57:       Environment.Exit(0);
58:     }
59:     
60:     bool IsFinished()
61:     { // 是否完成任務?
62:       foreach (var kettle in kettles)
63:         if (kettle.Current == target) return true;
64:       return false;
65:     }
66:   }
67: }

這個也很簡單,我們的演算法體現在第 29~35 行的主迴圈中,就是反覆執行以下步驟:

  • 裝滿B水壺
  • 將水從B水壺倒入A水壺
  • 倒空A水壺
  • 將水從B水壺倒入A水壺

直到完成任務,或者達到步數限制。

控制檯主程式(ConsoleRunner.cs)

 1: using System;
 2: 
 3: namespace Skyiv.Ben.Pond
 4: {
 5:   static class ConsoleRunner
 6:   {
 7:     static void Main(string[] args)
 8:     { // 解決水壺問題的主程式
 9:       int k = 0, limit = 20;
10:       if (args.Length > k && args[k].StartsWith(":"))
11:         limit = int.Parse(args[k++].Substring(1));
12:       if (args.Length - k <= 2) { Usage(); return; }
13:       var target = int.Parse(args[k++]);
14:       var capacitys = new int[args.Length - k];
15:       for (var i = 0; i < capacitys.Length; i++)
16:         capacitys[i] = int.Parse(args[k + i]);
17:       new Water(Console.Out, limit, target, capacitys).Solve();
18:     }
19:     
20:     static void Usage()
21:     { // 報告本程式的使用方法
22:       Console.WriteLine("ConsoleRunner [:limit] target capacity1 capacity2 ...");
23:     }
24:   }
25: }

這個控制檯主程式更簡單了。就是讀取命令列引數,然後呼叫 Water 類的 Solve 方法解決問題。注意以下幾點:

  • 我們的程式叫 ConsoleRunner,說明以後有可能搞一個 GraphicRunner 。
  • 命令列引數用於指定步數限制(可選)目標各水壺的容量
  • 這說明目標水量和各水壺的容量都可以由我們指定,而且水壺可以多於兩個。
  • 但我們目前的演算法只用到前兩個水壺,對多餘的水壺(如果有的話)不理不睬。

擴充套件方法(StringExtensions.cs)

 1: using System.Text;
 2: 
 3: namespace Skyiv.Extensions
 4: {
 5:   public static class StringExtensions
 6:   {
 7:     static readonly Encoding Encode = Encoding.GetEncoding("GB18030");
 8:   
 9:     public static string ChinesePadRight(this string str, int count)
10:     { // 對齊漢字字串,右補空格
11:       return Encode.GetString(Encode.GetBytes(str.PadRight(count)), 0, count);
12:     }
13:   }
14: }

這個不用解釋了吧?就是對齊漢字字串。Microsoft .NET Framework Base Class Library 自帶的string.PadRight只能對齊半形字元,對全形字元無效。

編譯檔案(Makefile)

CSC = dmcs

ConsoleRunner.exe : ConsoleRunner.cs Water.cs Kettle.cs StringExtensions.cs
    $(CSC) -out:$@ ConsoleRunner.cs Water.cs Kettle.cs StringExtensions.cs

編譯和執行

在 Arch Linux 64-bit 作業系統的 Mono 2.10.8 環境下編譯和執行:

Water$ make
dmcs -out:ConsoleRunner.exe ConsoleRunner.cs Water.cs Kettle.cs StringExtensions.cs
Water$ mono ConsoleRunner.exe
ConsoleRunner [:limit] target capacity1 capacity2 ...

上面是使用make編譯,然後執行我們的程式,得知用法是需要在命令列引數指定步數限制(可選)目標各水壺的容量。好吧,我們給出命令列引數 3、5 和 6 來呼叫該程式:

Water$ mono ConsoleRunner.exe 3 5 6
步數限制:20 目標:3 水壺們:[A:0/5][B:0/6]
  1: [裝滿:B]      [A:0/5][B:6/6]
  2: [從B倒入A:5]  [A:5/5][B:1/6]
  3: [倒空:A]      [A:0/5][B:1/6]
  4: [從B倒入A:1]  [A:1/5][B:0/6]
  5: [裝滿:B]      [A:1/5][B:6/6]
  6: [從B倒入A:4]  [A:5/5][B:2/6]
  7: [倒空:A]      [A:0/5][B:2/6]
  8: [從B倒入A:2]  [A:2/5][B:0/6]
  9: [裝滿:B]      [A:2/5][B:6/6]
 10: [從B倒入A:3]  [A:5/5][B:3/6]
---- 大功告成 ---------------

這就成功了。

演算法的有效性

我們的演算法對兩個水壺的情況還是很有效的(水壺B的容量必須大於水壺A的容量)。試看以下執行結果:

目標(0)

Water$ mono ConsoleRunner.exe 0 5 6
步數限制:20 目標:0 水壺們:[A:0/5][B:0/6]
  1: [裝滿:B]      [A:0/5][B:6/6]
---- 大功告成 ---------------

注意,我們的演算法有一個小小的缺陷。因為在目標(0)的情況下,不用做任何事就已經完成了任務。而我們多餘地裝滿了B水壺。

目標(1)

Water$ mono ConsoleRunner.exe 1 5 6
步數限制:20 目標:1 水壺們:[A:0/5][B:0/6]
  1: [裝滿:B]      [A:0/5][B:6/6]
  2: [從B倒入A:5]  [A:5/5][B:1/6]
---- 大功告成 ---------------

目標(2)

Water$ mono ConsoleRunner.exe 2 5 6
步數限制:20 目標:2 水壺們:[A:0/5][B:0/6]
  1: [裝滿:B]      [A:0/5][B:6/6]
  2: [從B倒入A:5]  [A:5/5][B:1/6]
  3: [倒空:A]      [A:0/5][B:1/6]
  4: [從B倒入A:1]  [A:1/5][B:0/6]
  5: [裝滿:B]      [A:1/5][B:6/6]
  6: [從B倒入A:4]  [A:5/5][B:2/6]
---- 大功告成 ---------------

目標(3)

Water$ mono ConsoleRunner.exe 3 5 6
步數限制:20 目標:3 水壺們:[A:0/5][B:0/6]
  1: [裝滿:B]      [A:0/5][B:6/6]
  2: [從B倒入A:5]  [A:5/5][B:1/6]
  3: [倒空:A]      [A:0/5][B:1/6]
  4: [從B倒入A:1]  [A:1/5][B:0/6]
  5: [裝滿:B]      [A:1/5][B:6/6]
  6: [從B倒入A:4]  [A:5/5][B:2/6]
  7: [倒空:A]      [A:0/5][B:2/6]
  8: [從B倒入A:2]  [A:2/5][B:0/6]
  9: [裝滿:B]      [A:2/5][B:6/6]
 10: [從B倒入A:3]  [A:5/5][B:3/6]
---- 大功告成 ---------------

目標(4)

Water$ mono ConsoleRunner.exe 4 5 6
步數限制:20 目標:4 水壺們:[A:0/5][B:0/6]
  1: [裝滿:B]      [A:0/5][B:6/6]
  2: [從B倒入A:5]  [A:5/5][B:1/6]
  3: [倒空:A]      [A:0/5][B:1/6]
  4: [從B倒入A:1]  [A:1/5][B:0/6]
  5: [裝滿:B]      [A:1/5][B:6/6]
  6: [從B倒入A:4]  [A:5/5][B:2/6]
  7: [倒空:A]      [A:0/5][B:2/6]
  8: [從B倒入A:2]  [A:2/5][B:0/6]
  9: [裝滿:B]      [A:2/5][B:6/6]
 10: [從B倒入A:3]  [A:5/5][B:3/6]
 11: [倒空:A]      [A:0/5][B:3/6]
 12: [從B倒入A:3]  [A:3/5][B:0/6]
 13: [裝滿:B]      [A:3/5][B:6/6]
 14: [從B倒入A:2]  [A:5/5][B:4/6]
---- 大功告成 ---------------

目標(5)

Water$ mono ConsoleRunner.exe 5 5 6
步數限制:20 目標:5 水壺們:[A:0/5][B:0/6]
  1: [裝滿:B]      [A:0/5][B:6/6]
  2: [從B倒入A:5]  [A:5/5][B:1/6]
---- 大功告成 ---------------

從這可以看出,我們目前的演算法不是最高效的。實際上目標(5)只需裝滿A水壺這一步就行了,用不著兩步。

目標(6)

Water$ mono ConsoleRunner.exe 6 5 6
步數限制:20 目標:6 水壺們:[A:0/5][B:0/6]
  1: [裝滿:B]      [A:0/5][B:6/6]
---- 大功告成 ---------------

目標(7)

Water$ mono ConsoleRunner.exe 7 5 6 3
步數限制:20 目標:7 水壺們:[A:0/5][B:0/6][C:0/3]
  1: [裝滿:B]      [A:0/5][B:6/6][C:0/3]
  2: [從B倒入A:5]  [A:5/5][B:1/6][C:0/3]
  3: [倒空:A]      [A:0/5][B:1/6][C:0/3]
  4: [從B倒入A:1]  [A:1/5][B:0/6][C:0/3]
  5: [裝滿:B]      [A:1/5][B:6/6][C:0/3]
  6: [從B倒入A:4]  [A:5/5][B:2/6][C:0/3]
  7: [倒空:A]      [A:0/5][B:2/6][C:0/3]
  8: [從B倒入A:2]  [A:2/5][B:0/6][C:0/3]
  9: [裝滿:B]      [A:2/5][B:6/6][C:0/3]
 10: [從B倒入A:3]  [A:5/5][B:3/6][C:0/3]
 11: [倒空:A]      [A:0/5][B:3/6][C:0/3]
 12: [從B倒入A:3]  [A:3/5][B:0/6][C:0/3]
 13: [裝滿:B]      [A:3/5][B:6/6][C:0/3]
 14: [從B倒入A:2]  [A:5/5][B:4/6][C:0/3]
 15: [倒空:A]      [A:0/5][B:4/6][C:0/3]
 16: [從B倒入A:4]  [A:4/5][B:0/6][C:0/3]
 17: [裝滿:B]      [A:4/5][B:6/6][C:0/3]
 18: [從B倒入A:1]  [A:5/5][B:5/6][C:0/3]
 19: [倒空:A]      [A:0/5][B:5/6][C:0/3]
 20: [從B倒入A:5]  [A:5/5][B:0/6][C:0/3]
**** 出師未捷身先死 ****

這下失敗了。但是這不是我們演算法的錯,因為這個任務是無論如何都是無法完成的。注意:我們這次給了三個水壺,雖然實際上我們的演算法沒用到第三個水壺。

演算法的改進

目前的演算法只處理前兩個水壺,對多餘的水壺(如果有的話)不理不睬。請移步下一篇文章:可以處理更多水壺的演算法

相關文章