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

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

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

posted on   小城故事  阅读(1865)  评论(5编辑  收藏  举报
努力加载评论中...
点击右上角即可分享
微信分享提示