程式設計也快樂第三期解答(一)
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]
**** 出師未捷身先死 ****
這下失敗了。但是這不是我們演算法的錯,因為這個任務是無論如何都是無法完成的。注意:我們這次給了三個水壺,雖然實際上我們的演算法沒用到第三個水壺。
演算法的改進
目前的演算法只處理前兩個水壺,對多餘的水壺(如果有的話)不理不睬。請移步下一篇文章:可以處理更多水壺的演算法。
相關文章
- 程式設計也快樂第三期解答(四)程式設計
- 程式設計也快樂第三期解答(三)程式設計
- 程式設計也快樂第三期解答(二)程式設計
- 程式設計也快樂第3期SQL程式碼程式設計SQL
- 程式設計也快樂: 兩隻水壺 C程式碼 搜尋版程式設計C程式
- 程式設計師節快樂程式設計師
- 使用Google Guava快樂程式設計GoGuava程式設計
- 程式設計師的快樂生活程式設計師
- 快樂指南:程式設計師版程式設計師
- 華為大佬:做一個快樂的程式設計師程式設計師
- 1024!程式設計師節快樂!程式設計師
- 女程式設計師們!節日快樂!程式設計師
- 傳播正能量——做一個快樂的程式設計師程式設計師
- 程式設計師如何祝自己生日快樂程式設計師
- 程式設計學習之路:痛並快樂著程式設計
- 程式設計師的快樂:那些小細節程式設計師
- 讓程式設計快樂起來的過程程式設計
- 程式設計師快樂器之JAVA程式碼生成工具程式設計師Java
- 編碼也快樂:兩隻水壺F#程式
- 編碼也快樂:兩隻水壺Scheme程式Scheme
- 編碼也快樂:兩隻水壺C#程式C#
- 調查:是什麼讓程式設計師快樂?程式設計師
- 快樂Node程式設計師的10個習慣程式設計師
- 《Ruby基礎教程(第4版)》:快樂程式設計程式設計
- 在程式設計中體驗純粹的快樂程式設計
- 編碼也快樂:兩水壺的故事之JS程式JS
- 五線譜入門,程式設計師也可以玩音樂程式設計師
- 快樂的星期天:Scratch少兒趣味程式設計程式設計
- 程式設計師保持天天快樂的6個習慣程式設計師
- 程式設計師的燈下黑:沒學會快樂程式設計師
- 交響樂:程式設計師的生活(第一樂章)程式設計師
- 《快樂碼農》第5期 大話程式設計師面試程式設計師面試
- 如何做一個快樂的程式設計師?謹記六個好習慣程式設計師
- 快樂的暑假線上程式設計競賽的第一個問題:分割點遊戲程式設計遊戲
- 編碼也快樂活動:撲克牌排序排序
- 編碼也快樂!撲克牌排序JAVA排序Java
- Java程式設計師面試題及解答Java程式設計師面試題
- 也談程式設計改革程式設計