上一篇里,阐述了解这道题的思路,并在代码上实现。不过代码还有很多可改进之处。性能方面,虽然比穷举法快得多,此外搜索算法还是比较盲目,效率应该能更上一层楼。
首先是在算法实现最后一步的搜索树递归方法中,发现MatchResult枚举并没有实际用处
1 2 3 4 5 6 7 8 9 10 | 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抽象类定义变成:
1 2 3 4 5 6 7 | abstract class Condition { …… public abstract bool Match(IList guys, ref IList attempts); } |
现在去掉MatchResult枚举,除了Condition类,还影响Guy类的VerfyProperty方法。
1 | public MatchResult VerifyProperty(Property property, IList attempts); |
我把它改成了返回bool类型,总是觉得有点别扭。又是在上班路上,想到了,是因为它违反了Shy原则,出现在了不该出现的地方。Guy类不应该直接与Attemp打交道。那VerfyProperty方法应该出现在哪里,Condition类中?也是别扭,这样要多传一个Guy参数,主要是它和Condition类中的Properties没直接联系。反复斟酌,想到了扩展方法。
1 2 3 4 5 6 7 8 | 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方法进行拆分,变成这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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); else return 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行,里面就是两种情况变换,逻辑上明显重复,但要强行合并的话,必然要引入新变量,结果得不偿失,不知道大家有什么好办法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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 guy2 Property propToFind; //The property match no guy if (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实例具备新的能力 — 即探路能力。
1 2 3 4 5 6 7 8 | abstract class Condition { ...... public abstract Int32 CountPaths(IList guys); ...... } |
从而可以计算下一步的选择有多少,我们可以比较每个条件,找出分支最少的选择,再进行尝试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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模式慢太多。据我所知,这是目前最快的算法,这就是面向对象算法的潜力!
下一步,还要继续深入研究这部内容,不知道这种思想能否用在其他智力运算题,如背包问题,二十四点上面。研究人工智能,还真挺有趣。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步