记得第一次接触这个所谓的“爱因斯坦谜题”的时候,也花了很多时间做了一个比较难看的解法,大体上就是将不可能的条件筛掉一部分,然后再穷举。这样的解法可以说是相当难看的,因为只要题目的条件稍微变变,程序又要改一大堆。也是受限于当时的技术条件,的确很难实现一个比较通用的解题器。
不过在时过境迁的今天,利用新思想和新技术,我们能不能交出一份漂亮的答卷呢?
答案是肯定的。
我设计的第一个版本的思路是这样的,穷举所有的情况,然后将已知线索作为筛选器来检查每一种情况,如果有一种情况符合所有的线索,则就是最终答案。
类似于排列组合这种运算,老实说还真没有什么现成的方案,还是老老实实的笛卡尔积再筛选吧。
利用多重from表达式,我们能很方便的得到N个集合的笛卡尔积。
房子有五种宠物,五种颜色,五种国籍,五种香烟和五种饮料,将将它们互相得到一个每个房子的可能结果集,再对这个结果集进行五次自身的笛卡尔积,最终的结果筛去不满足前提条件的结果(有相同的颜色或宠物)。
第一个程序很快做出来了,结果跑了几十分钟还没有结果,猜想是结果集太大了。
其实用脚趾头想想都知道这个结果集的大小几乎是天文数字,我的笔记本是不可能算出来的,看来不能有侥幸心理。。。。
改,这一次使用一种先进的算法,计算五种宠物、五种颜色等的全排列,然后计算这些全排列的结果的笛卡尔积,这样得出来的结果不存在有相同颜色的房子这样的问题,结果集可以缩小一个很大的数量级。
计算全排列我使用了一种非常简单不通用的算法,难看就难看点,先解决问题吧:
不过在时过境迁的今天,利用新思想和新技术,我们能不能交出一份漂亮的答卷呢?
答案是肯定的。
我设计的第一个版本的思路是这样的,穷举所有的情况,然后将已知线索作为筛选器来检查每一种情况,如果有一种情况符合所有的线索,则就是最终答案。
类似于排列组合这种运算,老实说还真没有什么现成的方案,还是老老实实的笛卡尔积再筛选吧。
利用多重from表达式,我们能很方便的得到N个集合的笛卡尔积。
房子有五种宠物,五种颜色,五种国籍,五种香烟和五种饮料,将将它们互相得到一个每个房子的可能结果集,再对这个结果集进行五次自身的笛卡尔积,最终的结果筛去不满足前提条件的结果(有相同的颜色或宠物)。
第一个程序很快做出来了,结果跑了几十分钟还没有结果,猜想是结果集太大了。
其实用脚趾头想想都知道这个结果集的大小几乎是天文数字,我的笔记本是不可能算出来的,看来不能有侥幸心理。。。。
改,这一次使用一种先进的算法,计算五种宠物、五种颜色等的全排列,然后计算这些全排列的结果的笛卡尔积,这样得出来的结果不存在有相同颜色的房子这样的问题,结果集可以缩小一个很大的数量级。
计算全排列我使用了一种非常简单不通用的算法,难看就难看点,先解决问题吧:
public static IEnumerable<T[]> GetCustomPermutation<T>( this T[] array ) { return from item1 in array from item2 in array.Where( item => !item.Equals( item1 ) ) from item3 in array.Where( item => !( item.Equals( item1 ) || item.Equals( item2 ) ) ) from item4 in array.Where( item => !( item.Equals( item1 ) || item.Equals( item2 ) || item.Equals( item3 ) ) ) from item5 in array.Where( item => !( item.Equals( item1 ) || item.Equals( item2 ) || item.Equals( item3 ) || item.Equals( item4 ) ) ) select new T[] { item1, item2, item3, item4, item5 }; }
第二个程序就这样诞生了,运行。。。。。还是没有结果。。。。。
优化,避免延迟执行。。。。
还是没戏。。。。
看来还是结果集太大了。
我算算,五个东西的全排列,嗯,是5!=120,五个这样全排列的笛卡尔积的结果集大小是120^5,心算一下就知道这至少是个百亿级别的数了。
所以暴力的穷举就这样可耻的失败了。。。。。。
好吧,看来不给程序加上智能是不可能做到的了。
要给程序加上智能,就要将原来纯粹的判断改为智能的判断,原来的判断是这样的:
优化,避免延迟执行。。。。
还是没戏。。。。
看来还是结果集太大了。
我算算,五个东西的全排列,嗯,是5!=120,五个这样全排列的笛卡尔积的结果集大小是120^5,心算一下就知道这至少是个百亿级别的数了。
所以暴力的穷举就这样可耻的失败了。。。。。。
好吧,看来不给程序加上智能是不可能做到的了。
要给程序加上智能,就要将原来纯粹的判断改为智能的判断,原来的判断是这样的:
public static IEnumerable<Predicate<Conjecture>> GetKnownConditions() { //1、The Brit lives in the red house yield return conjecture => conjecture.Any( house => house.Nationality == "Brit" && house.Color == "red" ); //2、The Swede keeps dogs as pets yield return conjecture => conjecture.Any( house => house.Nationality == "Swede" && house.Pet == "dogs" ); //3、The Dane drinks tea yield return conjecture => conjecture.Any( house => house.Nationality == "Dane" && house.Drinks == "tea" ); //4、The green house is on the immediate left of the white house as you stare at the front of the 5 houses yield return conjecture => conjecture.Any( house => house.Color == "green" && house.IsImmediateLeftOf( _house => _house.Color == "white" ) ); //5、The green house owner drinks coffee yield return conjecture => conjecture.Any( house => house.Color == "green" && house.Drinks == "coffee" ); //6、The person who smokes Pall Mall raises birds yield return conjecture => conjecture.Any( house => house.Smokes == "Pall Mall" && house.Pet == "birds" ); //7、The owner of the yellow house smokes Dunhill yield return conjecture => conjecture.Any( house => house.Color == "yellow" && house.Smokes == "Dunhill" ); //8、The man living in the house right in the center drinks milk yield return conjecture => conjecture.Any( house => house.IsCenter && house.Drinks == "milk" ); //9、The Norwegian lives in the first house yield return conjecture => conjecture.Any( house => house.Nationality == "Norwegian" && house.Index == 0 ); //10、The man who smokes Blends lives next to the one who keeps cats yield return conjecture => conjecture.Any( house => house.Smokes == "Blends" && house.IsImmediateOf( _house => _house.Pet == "cats" ) ); //11、The man who keeps horses lives next to the one who smokes Dunhill yield return conjecture => conjecture.Any( house => house.Pet == "horses" && house.IsImmediateOf( _house => _house.Smokes == "Dunhill" ) ); //12、The owner who smokes Bluemaster drinks juice yield return conjecture => conjecture.Any( house => house.Smokes == "Bluemaster" && house.Drinks == "juice" ); //13、The German smokes Prince yield return conjecture => conjecture.Any( house => house.Nationality == "German" && house.Smokes == "Prince" ); //14、The Norwegian lives next to the blue house yield return conjecture => conjecture.Any( house => house.Nationality == "Norwegian" && house.IsImmediateOf( _house => _house.Color == "blue" ) ); //15、The man who smokes Blend has a neighbor who drinks water. yield return conjecture => conjecture.Any( house => house.Smokes == "Blends" && house.IsImmediateOf( _house => _house.Drinks == "water" ) ); }
现在不能再这样判断了,要能够智能的提出断言,例如,如果发现一个房子是红色的,就断言里面住着英国人。好吧,这真是一个大工程,我只摘取部分代码:
public class Question { private class Clue0 : ClueBase { protected override bool Apply( Conjecture conjecture ) { if ( conjecture.ExistColors.Distinct().Count() != conjecture.ExistColors.Count() ) throw new Exception(); if ( conjecture.ExistNationalities.Distinct().Count() != conjecture.ExistNationalities.Count() ) throw new Exception(); if ( conjecture.ExistDrinks.Distinct().Count() != conjecture.ExistDrinks.Count() ) throw new Exception(); if ( conjecture.ExistSmokes.Distinct().Count() != conjecture.ExistSmokes.Count() ) throw new Exception(); if ( conjecture.ExistPets.Distinct().Count() != conjecture.ExistPets.Count() ) throw new Exception(); if ( conjecture.ExistColors.Count() == 4 ) conjecture.First( house => house.Color == null ).Color = Conjecture.allColors.Except( conjecture.ExistColors ).First(); if ( conjecture.ExistNationalities.Count() == 4 ) conjecture.First( house => house.Nationality == null ).Nationality = Conjecture.allNationalities.Except( conjecture.ExistNationalities ).First(); if ( conjecture.ExistDrinks.Count() == 4 ) conjecture.First( house => house.Drinks == null ).Drinks = Conjecture.allDrinks.Except( conjecture.ExistDrinks ).First(); if ( conjecture.ExistSmokes.Count() == 4 ) conjecture.First( house => house.Smokes == null ).Smokes = Conjecture.allSmokes.Except( conjecture.ExistSmokes ).First(); if ( conjecture.ExistPets.Count() == 4 ) conjecture.First( house => house.Pet == null ).Pet = Conjecture.allPets.Except( conjecture.ExistPets ).First(); return false; } } //1、The Brit lives in the red house private class Clue1 : ClueBase { protected override bool Apply( Conjecture conjecture ) { var house = conjecture.FirstOrDefault( item => item.Color == "red" ); if ( house != null ) { house.Nationality = "Brit"; return true; } house = conjecture.FirstOrDefault( item => item.Nationality == "Brit" ); if ( house != null ) { house.Color = "red"; return true; } return false; } } //2、The Swede keeps dogs as pets private class Clue2 : ClueBase { protected override bool Apply( Conjecture conjecture ) { var house = conjecture.FirstOrDefault( item => item.Nationality == "Swede" ); if ( house != null ) { house.Pet = "dogs"; return true; } house = conjecture.FirstOrDefault( item => item.Pet == "dogs" ); if ( house != null ) { house.Nationality = "Swede"; return true; } return false; } } //... //15、The man who smokes Blend has a neighbor who drinks water. private class Clue15 : ClueBase { protected override bool Apply( Conjecture conjecture ) { var house = conjecture.FirstOrDefault( item => item.Smokes == "Blends" ); if ( house != null ) { Func<HouseConditon, HouseConditon, bool> check = ( left, right ) => { if ( left == null ) { right.Drinks = "water"; return true; } if ( left.Drinks == "water" ) return true; if ( left.Drinks != null ) { right.Drinks = "water"; return true; } return false; }; if ( check( house.GetImmediateLeft(), house.GetImmediateRight() ) ) return true; if ( check( house.GetImmediateRight(), house.GetImmediateLeft() ) ) return true; } house = conjecture.FirstOrDefault( item => item.Drinks == "water" ); if ( house != null ) { Func<HouseConditon, HouseConditon, bool> check = ( left, right ) => { if ( left == null ) { right.Smokes = "Blends"; return true; } if ( left.Smokes == "Blends" ) return true; if ( left.Smokes != null ) { right.Smokes = "Blends"; return true; } return false; }; if ( check( house.GetImmediateLeft(), house.GetImmediateRight() ) ) return true; if ( check( house.GetImmediateRight(), house.GetImmediateLeft() ) ) return true; } return false; } } public static IEnumerable<IClue> GetClues() { yield return new Clue1(); yield return new Clue2(); yield return new Clue3(); yield return new Clue4(); yield return new Clue5(); yield return new Clue6(); yield return new Clue7(); yield return new Clue8(); yield return new Clue9(); yield return new Clue10(); yield return new Clue11(); yield return new Clue12(); yield return new Clue13(); yield return new Clue14(); yield return new Clue15(); yield return new Clue0(); } }
请注意Clue0,这个其实是写在题目开头的已知条件,即所有房子颜色、宠物等都不相同。
Clue的基类型ClueBase负责捕获异常,并实现接口。
public interface IClue { bool IsApplied { get; } bool TryApply( Conjecture conjecture ); } public abstract class ClueBase : IClue { public bool IsApplied { get; set; } public virtual bool TryApply( Conjecture conjecture ) { try { IsApplied = Apply( conjecture ); return true; } catch { return false; } } protected abstract bool Apply( Conjecture conjecture ); public override string ToString() { if ( IsApplied ) return "Applied"; else return "NotApply"; } }
好了,断言的冲突的问题采用异常来解决,即如果一个断言说A房子里住的是挪威人,而另一个断言已经宣布这个里面住的是英国人了,这样就造成了冲突,让它抛异常吧。这个逻辑写在了房子的属性里:
/// <summary> /// 代表一个房子的所有情况 /// </summary> public class HouseConditon { private string _color = null; public string Color { get { return _color; } set { if ( _color != null && _color != value ) throw new Exception(); _color = value; } } private string _nationality = null; public string Nationality { get { return _nationality; } set { if ( _nationality != null && _nationality != value ) throw new Exception(); _nationality = value; } } private string _drinks = null; public string Drinks { get { return _drinks; } set { if ( _drinks != null && _drinks != value ) throw new Exception(); _drinks = value; } } private string _smokes = null; public string Smokes { get { return _smokes; } set { if ( _smokes != null && _smokes != value ) throw new Exception(); _smokes = value; } } private string _pet = null; public string Pet { get { return _pet; } set { if ( _pet != null && _pet != value ) throw new Exception(); _pet = value; } } internal HouseConditon( Conjecture conjectur, int index ) { Conjecture = conjectur; Index = index; } public Conjecture Conjecture { get; private set; } public int Index { get; private set; } /// <summary> /// 获取紧挨在左边的房子 /// </summary> /// <returns></returns> public HouseConditon GetImmediateLeft() { if ( Index <= 0 ) return null; else return Conjecture[Index - 1]; } /// <summary> /// 获取紧挨在右边的房子 /// </summary> /// <returns></returns> public HouseConditon GetImmediateRight() { if ( Index >= 4 ) return null; else return Conjecture[Index + 1]; } public override string ToString() { return string.Format( "{0,10}; {1,7}; {2,7}; {3,10}; {4,7}", Nationality, Color, Drinks, Smokes, Pet ); } }
那个Conjecture其实就是房子状态的集合,具体如下:
/// <summary> /// 代表一种假设 /// </summary> public class Conjecture : IEnumerable<HouseConditon> { private HouseConditon[] _houses; public Conjecture() { _houses = new HouseConditon[5].Initialize( i => new HouseConditon( this, i ) ); } public HouseConditon this[int index] { get { return _houses[index]; } } #region IEnumerable<House> 成员 public IEnumerator<HouseConditon> GetEnumerator() { return ( (IEnumerable<HouseConditon>) _houses ).GetEnumerator(); } #endregion #region IEnumerable 成员 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion public override string ToString() { return string.Format( "First: {0}\nSecond:{1}\nThird: {2}\nForth: {3}\nFifth: {4}\n", this[0], this[1], this[2], this[3], this[4] ); } public Conjecture Clone() { var instance = new Conjecture(); for ( int i = 0; i < 5; i++ ) { instance[i].Nationality = this[i].Nationality; instance[i].Color = this[i].Color; instance[i].Drinks = this[i].Drinks; instance[i].Pet = this[i].Pet; instance[i].Smokes = this[i].Smokes; } return instance; } public static readonly string[] allColors = new string[] { "red", "green", "white", "yellow", "blue" }; public static readonly string[] allDrinks = new string[] { "tea", "coffee", "milk", "juice", "water" }; public static readonly string[] allSmokes = new string[] { "Pall Mall", "Blends", "Bluemaster", "Dunhill", "Prince" }; public static readonly string[] allNationalities = new string[] { "Brit", "Swede", "Dane", "Norwegian", "German" }; public static readonly string[] allPets = new string[] { "dogs", "cats", "birds", "horses", "fish" }; public IEnumerable<string> ExistColors { get { return from house in this where house.Color != null select house.Color; } } public IEnumerable<string> ExistNationalities { get { return from house in this where house.Nationality != null select house.Nationality; } } public IEnumerable<string> ExistDrinks { get { return from house in this where house.Drinks != null select house.Drinks; } } public IEnumerable<string> ExistSmokes { get { return from house in this where house.Smokes != null select house.Smokes; } } public IEnumerable<string> ExistPets { get { return from house in this where house.Pet != null select house.Pet; } } }
好了,万事俱备,写一个程序跑跑。
但是发现在一片空白之下,通过这样的推理,程序只能找出三个房屋属性的确定位置,比如说喝牛奶的房子在中间。
解决的办法很简单,当电脑发现不能通过规则得到进一步的结果的时候,就猜一个。
猜测之后再去匹配规则,如果还不能得到进一步的结果,那么就再猜。
猜测的函数很简单:
private static void Guess( Conjecture conjecture ) { Console.WriteLine( "Hmmmmmm...Guess!..." ); begin: var house = conjecture[random.Next( 5 )]; switch ( random.Next( 4 ) ) { case 0: if ( house.Nationality == null ) { var nationalities = Conjecture.allNationalities.Except( conjecture.ExistNationalities ).ToArray(); house.Nationality = nationalities[random.Next( nationalities.Length )]; return; } else goto begin; case 1: if ( house.Color == null ) { var colors = Conjecture.allColors.Except( conjecture.ExistColors ).ToArray(); house.Color = colors[random.Next( colors.Length )]; return; } else goto begin; case 2: if ( house.Drinks == null ) { var drinks = Conjecture.allDrinks.Except( conjecture.ExistDrinks ).ToArray(); house.Drinks = drinks[random.Next( drinks.Length )]; return; } else goto begin; case 3: if ( house.Smokes == null ) { var smokes = Conjecture.allSmokes.Except( conjecture.ExistSmokes ).ToArray(); house.Smokes = smokes[random.Next( smokes.Length )]; return; } else goto begin; } }
这里没有写猜测宠物并不是出于什么特别的原因,而是因为懒的缘故,电脑猜这么几个东西已经足够得到结论了。
最终的程序样子是这样的:
Conjecture conjecture = new Conjecture(); var clues = Question.GetClues().ToArray(); int applies = 0; Stack<Conjecture> stack = new Stack<Conjecture>(); while ( applies < 15 ) { var statusString = conjecture.ToString() + applies; if ( !clues.All( item => item.TryApply( conjecture ) ) || ( applies = clues.Count( item => item.IsApplied ) ) == 0 ) { conjecture = new Conjecture(); clues = Question.GetClues().ToArray(); Console.WriteLine( "ooooooooops......my god....." ); continue; } applies = clues.Count( item => item.IsApplied ); if ( statusString == conjecture.ToString() + applies ) { Guess( conjecture ); clues = Question.GetClues().ToArray(); } Console.WriteLine( conjecture ); } Console.WriteLine( "Found a result!!!" ); Console.WriteLine( conjecture ); Console.WriteLine(); Console.ReadLine();
哈哈,执行一把,在我的破本本上平均十几秒钟就能找到结果。很不错了,最关键的是,这是用人脑子的方式思考问题的程序。
性能不是这个程序的主要诉求,这个程序的革命性意义在于,15条线索(规则),都是独立于主逻辑之外的,我们可以随时新增和修改线索,这个程序同样能找到合理的解答。
当然这个程序还狠不完美,断言的代码冗余很严重。而这个问题,在表达式树出现后,呵呵,,,,,显然用表达式树来描述线索,再分析表达式树来创建断言函数。将使得这个程序更上一层楼。但我很懒,有兴趣的同学可以自己试试看哦。。。。