上一篇里,阐述了解这道题的思路,并在代码上实现。不过代码还有很多可改进之处。性能方面,虽然比穷举法快得多,此外搜索算法还是比较盲目,效率应该能更上一层楼。
首先是在算法实现最后一步的搜索树递归方法中,发现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); 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行,里面就是两种情况变换,逻辑上明显重复,但要强行合并的话,必然要引入新变量,结果得不偿失,不知道大家有什么好办法。
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实例具备新的能力 — 即探路能力。
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模式慢太多。据我所知,这是目前最快的算法,这就是面向对象算法的潜力!
下一步,还要继续深入研究这部内容,不知道这种思想能否用在其他智力运算题,如背包问题,二十四点上面。研究人工智能,还真挺有趣。