上一篇里,阐述了解这道题的思路,并在代码上实现。不过代码还有很多可改进之处。性能方面,虽然比穷举法快得多,此外搜索算法还是比较盲目,效率应该能更上一层楼。

  首先是在算法实现最后一步的搜索树递归方法中,发现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模式慢太多。据我所知,这是目前最快的算法,这就是面向对象算法的潜力!

  下一步,还要继续深入研究这部内容,不知道这种思想能否用在其他智力运算题,如背包问题,二十四点上面。研究人工智能,还真挺有趣。

posted on 2010-10-26 19:32  小城故事  阅读(1836)  评论(5编辑  收藏  举报