逻辑式编程语言极简实现(使用C#) - 2. 一道逻辑题:谁是凶手

本系列前面的文章:

这是一道Prolog经典的练习题,中文翻译版来自阮一峰的文章《Prolog 语言入门教程》

问题

Boddy 先生死于谋杀,现有六个嫌疑犯,每个人在不同的房间,每间房间各有一件可能的凶器,但不知道嫌疑犯、房间、凶器的对应关系。请根据下面的条件和线索,找出谁是凶手。

六个嫌疑犯是三男(George、John、Robert)三女(Barbara、Christine、Yolanda)。

六个嫌疑犯分别待在六个房间:浴室(Bathroom)、饭厅(Dining Room)、厨房(Kitchen)、起居室(Living Room)、 储藏室(Pantry)、书房(Study)。每间房间都有一件可疑的物品,可以当作凶器:包(Bag)、火枪(Firearm)、煤气(Gas)、刀(Knife)、毒药(Poison)、绳索(Rope)。

所有线索如下:

线索一:厨房里面是一个男人,那里的凶器不是绳索、刀子、包和火枪。

线索二:Barbara 和 Yolanda 在浴室和书房。

线索三:带包的那个人不是 Barbara 和 George,也不在浴室和饭厅。

线索四:书房里面是一个带绳子的女人。

线索五:起居室里面那件凶器,与 John 或 George 在一起。

线索六:刀子不在饭厅。

线索七:书房和食品储藏室里面的凶器,没跟 Yolanda 在一起。

线索八:George 所在的那间屋子有火枪。

线索九:Boddy 先生死在食品储藏室里,那里的凶器是煤气。

NMiniKanren解题

直接上代码:

var George = "George";
var John = "John";
var Rebert = "Rebert";
var Barbara = "Barbara";
var Christine = "Christine";
var Yolanda = "Yolanda";
var res = KRunner.Run(10, (k, q) =>
{
// 男人集合
var manNames = new string[] { George, John, Rebert };
var man = k.List(manNames);
// 女人集合
var womanNames = new string[] { Barbara, Christine, Yolanda };
var woman = k.List(womanNames);
// 所有人集合
var person = k.List(manNames.Concat(womanNames).ToArray());
// 每个场所所在的人
var bathroom = k.Fresh();
var dining = k.Fresh();
var kitchen = k.Fresh();
var livingroom = k.Fresh();
var pantry = k.Fresh();
var study = k.Fresh();
// 物品持有者
var bag = k.Fresh();
var firearm = k.Fresh();
var gas = k.Fresh();
var knife = k.Fresh();
var poison = k.Fresh();
var rope = k.Fresh();
// 不同的人在不同的房间
var locationConst = k.Distincto(bathroom, dining, kitchen, livingroom, pantry, study);
// 不同的人持有的物品不同
var weaponConst = k.Distincto(bag, firearm, gas, knife, poison, rope);
// 变量X表示凶手
var X = k.Fresh();
// 线索
// 厨房里面是一个男人,那里的凶器不是绳索、刀子、包和火枪。
var clue1 = k.All(
k.Is(kitchen, man),
k.Noto(k.Eq(kitchen, rope), k.Eq(kitchen, knife), k.Eq(kitchen, bag), k.Eq(kitchen, firearm)));
// Barbara 和 Yolanda 在浴室和书房。
var clue2 = k.Any(
k.All(k.Eq(bathroom, Barbara), k.Eq(study, Yolanda)),
k.All(k.Eq(bathroom, Yolanda), k.Eq(study, Barbara)));
// 带包的那个人不是 Barbara 和 George,也不在浴室和饭厅。
var clue3 = k.Noto(
k.Eq(bag, Barbara), k.Eq(bag, George),
k.Eq(bag, bathroom), k.Eq(bag, dining));
// 书房里面是一个带绳子的女人。
var clue4 = k.All(k.Is(rope, woman), k.Eq(rope, study));
// 起居室里面那件凶器,与 John 或 George 在一起。
var clue5 = k.Any(k.Eq(livingroom, John), k.Eq(livingroom, George));
// 刀子不在饭厅。
var clue6 = k.Noto(k.Eq(knife, dining));
// 书房和食品储藏室里面的凶器,没跟 Yolanda 在一起。
var clue7 = k.Noto(k.Eq(study, Yolanda), k.Eq(pantry, Yolanda));
// George 所在的那间屋子有火枪。
var clue8 = k.Eq(firearm, George);
// Boddy 先生死在食品储藏室里,那里的凶器是煤气。
var clue9 = k.All(k.Eq(X, pantry), k.Eq(X, gas));
// 集合所有条件
return k.All(
k.Is(X, person),
k.Is(bathroom, person),
k.Is(dining, person),
k.Is(kitchen, person),
clue5,
k.Is(livingroom, person),
k.Is(pantry, person),
k.Is(study, person),
clue2,
locationConst,
k.Is(bag, person),
k.Is(firearm, person),
clue8,
k.Is(gas, person),
k.Is(knife, person),
k.Is(poison, person),
k.Is(rope, person),
weaponConst,
clue1,
clue3,
clue4,
clue6,
clue7,
clue9,
k.Eq(q, k.List(
bathroom, dining, kitchen, livingroom, pantry, study,
bag, firearm, gas, knife, poison, rope,
X)));
});
Console.WriteLine("(bathroom dining kitchen livingroom pantry study bag firearm gas knife poison rope X)");
KRunner.PrintResult(res);

其中一些辅助函数:

k.Is(a, s): a是集合s的成员。

k.Noto(g1, g2, ...): g1g2……都不成立。NMiniKanren并没有支持“非”运算,这里用If方法模拟的,仅在一定场合下成立。

k.Distincto(a, b, c, ...): abc……两两不相等。

完整代码在https://github.com/sKabYY/NMiniKanren/blob/master/NMiniKaren.Tests/Crime.cs

另外,最后使用k.All整合所有条件时,并不是按照顺序写的。了解NMiniKanren运行原理后会知道,这是因为不同顺序会影响运行速度。大体上来说,应该尽可能让分支较少的放前面。

点击运行,等待几十秒,输出结果:

(bathroom dining kitchen livingroom pantry study bag firearm gas knife poison rope X)
[(Yolanda George Rebert John Christine Barbara John George Christine Yolanda Rebert Barbara Christine)]

凶手是Christine。

上一篇:【NOIP2013】华容道


下一篇:python语言中的数据类型之集合