• 重构打造爱因斯坦谜题最快算法


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

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

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

  • 相关阅读:
    display,visibility,meta知识
    存储过程
    Asp.Net碎知识
    分页
    配置IIS
    SQLAlchemy(三):外键、连表关系
    SQLAlchemy(二):SQLAlchemy对数据的增删改查操作、属性常用数据类型详解
    SQLAlchemy(一):SQLAlchemy去连接数据库、ORM介绍、将ORM模型映射到数据库中
    数据可视化之DAX篇(十)在PowerBI中累计求和的两种方式
    数据可视化之DAX篇(九) 关于DAX中的VAR,你应该避免的一个常见错误
  • 原文地址:https://www.cnblogs.com/XmNotes/p/1861793.html
Copyright © 2020-2023  润新知