重构-打造爱因斯坦谜题最快算法
上一篇里,闡述了解這道題的思路,并在代碼上實現。不過代碼還有很多可改進之處。性能方面,雖然比窮舉法快得多,此外搜索算法還是比較盲目,效率應該能更上一層樓。
首先是在算法實現最后一步的搜索樹遞歸方法中,發現MatchResult枚舉并沒有實際用處
var result = conditions[node.Index].Match(guys, ref attempts);if (result == MatchResult.Fail){if (node.Action != null) node.Action.RollBack();node = node.Parent.Next();}else{node = node.Expand(attempts).Next();}我們只需要知道Condition匹配是否成功即可,如果想知道更詳細情況,可以結合參數中的attempts在執行后的結果。這種無端增加復雜度的東西,當然要砍掉。Condition抽象類定義變成:
abstract class Condition{……public abstract bool Match(IList guys, ref IList attempts);}現在去掉MatchResult枚舉,除了Condition類,還影響Guy類的VerfyProperty方法。
public MatchResult VerifyProperty(Property property, IList attempts);我把它改成了返回bool類型,總是覺得有點別扭。又是在上班路上,想到了,是因為它違反了Shy原則,出現在了不該出現的地方。Guy類不應該直接與Attemp打交道。那VerfyProperty方法應該出現在哪里,Condition類中?也是別扭,這樣要多傳一個Guy參數,主要是它和Condition類中的Properties沒直接聯系。反復斟酌,想到了擴展方法。
static class PuzzleExtension{public static Boolean TryAdd(this IList attempts, Guy guy, Property property);public static Boolean TryAdd(this IList attempts, Guy guy, Property[] properties);public static Boolean TryAdd(this IList attempts, Guy guy1, Guy guy2, Property[] properties);}這樣就比較舒暢了,為貧血模型的進行充血,看來這才是擴展方法的陽關大道。
對于Condition.Match方法在兩個子類實現,尤其是AdjacentCondition,相當的繁瑣,行數近百,一眼望去就感覺有許多重復代碼。嘗試各種優化,費盡九牛之力,精簡了一些代碼,但還是很多。最后,使用Builder模式對Match方法進行拆分,變成這樣:
abstract class Condition{......public Boolean Match(IList guys, ref IList attempts){var availGuys = this.SearchGuy(guys);Guy guy1 = availGuys[0], guy2 = availGuys[1];if (attempts == null) attempts = new List();else attempts.Clear();if (guy1 == null && guy2 == null)return MatchBothNull(guys, attempts);else if (guy1 == guy2)return MatchEqual();else if (guy1 == null ^ guy2 == null)return MatchOneNull(guy1, guy2, attempts);elsereturn MatchNotEqual(guy1, guy2);}protected abstract Boolean MatchOneNull(Guy guy1, Guy guy2, IList attempts);protected abstract Boolean MatchBothNull(IList guys, IList attempts);protected abstract Boolean MatchEqual();protected abstract Boolean MatchNotEqual(Guy guy1, Guy guy2);public abstract Int32 CountPaths(IList guys);......}兩個子類分別要重寫四個方法,這樣每個方法里的代碼量就少了,可以控制在一頁以內。不過AdjacentCondition的MatchOneNull方法還是超過了25行,里面就是兩種情況變換,邏輯上明顯重復,但要強行合并的話,必然要引入新變量,結果得不償失,不知道大家有什么好辦法。
protected override Boolean MatchOneNull(Guy guy1, Guy guy2, IList attempts){ int posKnown, posLeft, posRight; // left/right means match guy1 is on the left/right of guy2Property propToFind; //The property match no guyif (guy1 == null) //1 for null, 2 for not null{posKnown = guy2[PN.Postion];posLeft = posKnown - 1; posRight = posKnown + 1;propToFind = Properties[0];}else{posKnown = guy1[PN.Nation];posLeft = posKnown + 1;posRight = posKnown - 1;propToFind = Properties[1];}if (posLeft > 0 && posLeft < 6){var leftGuy = GuyRuler.Single(posLeft);attempts.TryAdd(leftGuy, propToFind);}if (posRight > 0 && posRight < 6 && Relation == RelativePosition.Both){var rightGuy = GuyRuler.Single(posRight);attempts.TryAdd(rightGuy, propToFind);}return attempts.Count > 0;}強龍難壓地頭蛇,只能暫時睜一只眼閉一只眼了。畢竟還有更重要的事情,優化算法。
再分析一下條件,或者干脆我們自己手工把這道題解一遍,就會發現,前面真的走了很多彎路。比如我們已經知道“挪威人住在第一個房子裏(最左邊)“,下面又有個條件"挪威人和住藍房子的人相鄰”,便能確定第二個人住藍房子,篩選范圍就縮小許多。我們也可以將各個AddCondition語句順序換一下,可以看到遞歸查找的次數差別很懸殊。所以,我們不要運氣決定性能,自己的命運自己作主,主動去找最容易打開突破口的機會。
首先,我們讓Condtion實例具備新的能力 — 即探路能力。
abstract class Condition{......public abstract Int32 CountPaths(IList guys);......}從而可以計算下一步的選擇有多少,我們可以比較每個條件,找出分支最少的選擇,再進行嘗試。
Condition searchCondition(){int minBranchCount = int.MaxValue ;Condition condition = null ;foreach (var item in conditions){var count = item.CountPaths(guys);if (count <= 1) return item;if (count < minBranchCount){minBranchCount = count;condition = item;}}return condition;}讓我們在茫茫黑夜中,找到了指路的明燈,趕緊來試一下吧。
| 輪數(1000次/輪) | 1 | 2 | 3 | 4 |
| 時間(ms) | 80 | 66 | 66 | 66 |
平均每次解題不到0.0001秒!過程中只遞歸了13次。切記是在Release模式下運行,Debug模式慢太多。據我所知,這是目前最快的算法,這就是面向對象算法的潛力!
下一步,還要繼續深入研究這部內容,不知道這種思想能否用在其他智力運算題,如背包問題,二十四點上面。研究人工智能,還真挺有趣。
轉載于:https://www.cnblogs.com/XmNotes/archive/2010/10/26/1861793.html
總結
以上是生活随笔為你收集整理的重构-打造爱因斯坦谜题最快算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: rdlc 报表
- 下一篇: 修复 SyntaxHighlighter