不知道访问这个Blog的人里有多少写过程序,只要是写过的,想必一定知道,想要让计算机给你解题,首先你必须自己知道这题怎么解,这似乎是一件理所当然的事情。学校里的老师想必也一定也说过“程序=算法+数据结构”这个经典的等式。不过今天我们从以前的窠臼中跳出来,来质问一下那件理所当然的事情:凭什么我让计算机解题前我还非得要自己会才行?理想状况应该是我告诉电脑我需要解的问题,之后就让计算机去解好了。这种事情可不可能呢?事实上,虽然还有着各种各样的限制,但是逻辑程序设计已经为我们在一定程度上实现了这个理想,这次我会介绍逻辑程序设计里其中的一个分支Answer Set Programming(ASP)。这里我不准备涉及关于理论方面的知识,一切以实际应用为主,因为我觉得大部分时候应用时牵扯到的语法和语义都是很好理解的。当然如果你能够有一些关于命题逻辑和一阶逻辑的知识的话那就再好不过了。
在正式的介绍ASP的各种规则之前,我们先来看一个简单的例子:现在有乌鲁木齐,北京,合肥,秦皇岛4个城市,我们现在要坐火车在这4个城市之间穿梭,已知除了秦皇岛以外,其他三个城市之间都有直达的火车,而秦皇岛只有到北京的直达火车,那么从合肥出发,我都能到达哪些城市呢?一个典型的ASP程序会像下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
road(urumqi, beijing).
road(hefei, beijing).
road(urumqi, hefei).
road(beijing, qinhuangdao).
road(X, Y) :- road(Y, X).
route(X, Y) :- road(X, Y).
route(X, Y) :- route(X, Z), route(Z, Y).
arrive(X) :- route(hefei, X).
#hide.
#show arrive/1.
|
在这里,符号':-'代表如果,符号','代表并且。现在,如果我什么也不说,你能猜出这个程序里每一行的意思么?这并不困难,显然前4行代表了一些事实,它告诉我们哪些城市之间有道路相连,6-10行则是一些规则,其中第六行告诉我们,如果城市Y和X间有道路,则X和Y之间也有道路。第七行则告诉我们,如果X和Y之间有道路,那么X和Y就有一条通路,同理第八行告诉我们,如果X和Z有通路,Z和Y有通路,那么X和Y之间同样也有通路。第十行则告诉我们,如果合肥与X有通路,那么X就是从合肥可达的。最后两行我们可以先忽略,那是用于格式化输出结果的,和程序本身没有太大的关联了。
最后,如果你有兴趣,可以去下载gringo和clasp两样东西,将以上文件保存为road之后我们就可以在命令行下执行
1
|
gringo road | clasp 0
|
来得到如下的结果了:
1
2
3
|
Answer: 1
arrive(hefei) arrive(qinhuangdao)
arrive(beijing) arrive(urumqi)
|
那么现在让我们稍微扩展一下,假设北京和秦皇岛之间有列车发生了轻度追尾事故导致无法再通行(当然是没有人员伤亡了,至于你信不信,反正我是信了),那么我们有必要对上面那个程序做出一个修改,首先我们应该添加一个事实规则:
1
|
block(beijing,qinhuangdao).
|
同时也需要修改第7行的规则为:
1
|
route(X,Y) :- road(X,Y), not block(X,Y).
|
这个规则表示X和Y之间有一个通路如果X和Y之间有个道路并且这个道路没有被阻断[注]。之后再次执行这个修改过的程序,我们就会发现秦皇岛不再是可达的了。
从上面的例子我们可以看出,一个ASP程序包括两样重要的部分:事实,用于描述现实世界的状态。还有规则,用于进行推理,并且他们都由英文句号'.'结尾。而规则又分为在符号':-'左边的结论和右边的条件。结论成立,如果右边的条件被满足。另外事实也可以看做是省略了符号':-'和右边所有条件的规则。更细分一点,每一个ASP程序里还包括了一些常量(在这里就是城市名),谓词(这里的road,arrive什么的),每个谓词有若干个参数,并且为了简化起见,我们把带有n个参数的谓词p写作p/n。最后当然还有变量(在这里就是大写字母X和Y),需要注意的是本质上ASP程序是不支持变量的,这里的变量仅仅是为了方便书写,在实际的求解中这些变量会被替换为程序中出现的所有常量,这也是我们为什么需要先运行gringo程序的原因,它的作用就是将程序里所有变量挨个替换成程序里出现的所有常量,你可以用:
1
|
gringo --text road
|
来看看程序被替换后的样子。
现在让我们来定义一下ASP程序里各种规则的样子,有了这些,我们就可以用它来解决问题了。
1. 约束规则
所有在符号':-'左边什么都没有的规则就是约束规则,它表示右边的条件不能同时被全部满足。
2. 选择规则
所有在符号':-'左边形如 {a1,a2,...,am} 的规则就是选择规则,其含义是如果右边条件成立,那么左边的任意一个子集也成立。
下面我们来考虑一下对于选择规则的扩展,首先我们不限制 {a1,a2,...,am} 这样的元素只能出现在规则左边,而是也可以出现在规则右边。其次,任意子集的说法也未免太简单了,我们给它加上个数限制,像 l{a1,a2,...,am}u ,这个元素表明花括号中的m个元素最少有l个最多有u个成立。所以形如
1
|
a :- 2 {b, c, d, e} 3.
|
这样的式子就表明如果b, c, d, e中最少有两个最多有三个元素成立,那么a也成立。当然,这里的l或者u并非必须同时出现,只写一个也是可以的,含义也很好理解。
3. 条件元素
数学里大家想必遇见过诸如 {a|a∈A} 这样的东西,它表示了所有满足 a 属于 A 的元素 a 的集合,这里我们也有类似的东西。比如说顶点着色问题,假设我们有诸如 vertex_color(X, Y) 这样的元素,我们想让它代表顶点X被染了颜色Y,并以此得到某一顶点v1所有的染色可能性。但是我们之前也说了,gringo会将程序里的所有变量拿所有常量替换一遍,这样Y不仅会被拿颜色替换,同时也会被拿顶点替换,这显然不是我们想要的,这时我们就需要条件元素了:
1
|
vertex_color(v1, Y):color(Y) :- vertex(v1).
|
看到了么?只需要用冒号':'就可以将变量Y限定在颜色的范围内,这样我们得到的就是顶点v1所有的着色可能了。需要说明的是条件并非只能加一个,如果你有多个变量需要做限制的话,可以将冒号一直连下去。
那么冗长无聊的语法介绍就先到此为止吧,我们来用一个例子来介绍如何来撰写一个ASP程序。我们所用的例子就是著名的八皇后问题,如何在8*8的国际象棋棋盘上摆放8个皇后而使他们无法相互攻击。
首先是事实,我们的棋盘有8行8列:
1
2
|
row(1..8).
col(1..8).
|
这里我们用1..8来代表从1到8的简写,这样的式子会被展开为row(1). row(2).等等直到row(8).
之后就是各个规则了,首先我们需要在 I 行 J 列上放置皇后:
1
|
queen(I, J) : row(I) : col(J).
|
这还不够,因为我们放且只能放8个皇后,所以我们把这个规则改写一下:
1
|
8 {queen(I, J) : row(I) : col(J) } 8
|
好了,到此为止如果你好奇的话可以像上面的例子那样先执行一下看看结果是什么,不过千万要记得把clasp后面的0换成5来表示只计算5种满足条件的结果,毕竟我们现在可以说什么限制都没有加,所有结果的数量多的可怕。另外,因为我们只关心queen的情况,而你会发现程序把row和col的结果都打印出来了,所以我们可以用
1
2
|
#hide.
#show queen/2.
|
来将结果显示限定在谓词queen上而隐藏其他的谓词。好了,到现在你会发现得到的结果只是随便把8个皇后放到棋盘上,根本没有考虑他们之间有没有攻击的问题,那么之后我们就来添加这些限制,首先是每行每列上不能同时有两个皇后:
1
2
|
:- queen(I, J1), queen(I, J2), J1 != J2.
:- queen(I1, J), queen(I2, J), I1 != I2.
|
最后,对角线上也不能同时有两个皇后:
1
2
3
4
|
:- queen(I1, J1), queen(I2, J2), I1 != I2,
J1 != J2, I1 - J1 == I2 - J2.
:- queen(I1, J1), queen(I2, J2), I1 != I2,
J1 != J2, I1 + J1 == I2 + J2.
|
到现在为止我们的程序就完成了,运行来看看结果吧。很有趣不是么,我们没有教计算机如何计算,只是描述了整个问题,计算机就把结果告诉了我们。当然这里介绍的只是冰山一角,限于篇幅和我的水平也没法更加详细的介绍了,如果你有兴趣的话,这里有不少文档可以供参考,如果能把你拉下水,那么我的目的也达到了。
注:也许很多人在这里的第一反应是用否定的 ¬block(X,Y). 而不是什么奇怪的not,这其实牵扯到非单调推理的问题,不过限于篇幅这个就不扯了,大家可以自己去找找资料。
PS:原文出处http://blog.tuidao.me/2012/04/answer-set-programming/