百姓网那道题
赖勇浩(http://laiyonghao.com )
昨天 @smallfish 传了个网址(http://home.wangjianshuo.com/cn/20100505_ccceeieeaece.htm )给我,打开一看,是王建硕先生的博客,正好我前段时间经由阮一峰的博客看到过他的简介,就好奇他又写了啥。仔细一看,原来是出了一道公开的笔试题,蛮有意思的,就决心做做看。
一开始看到要支持 AND OR 等逻辑操作符,想来也要支持括号标明优先级了,就想到拿前几天 python-cn 社区介绍的 PLY 模块做个 parser。虽然是第一次用这个模块,但真的很好用,写 parser 用的时间很短,调试了几下,就能够支持比较复杂的表达式了,比如:
(age < 15 and sex == 0) or age > 30
整出来的语法树如下:
or
and
('<', 'age', 15)
('==', 'sex', 0)
('>', 'age', 30)
接下来在比较两棵树是否有子集关系的时候,遇到了问题。今天早上看到 qmigh(http://qmigh.blogspot.com/ )的想法,启发了我。不过他提出的方案是把查询表达式“规范化”,这一步是比较难做的,需要用到离散数学的一些知识,实现起来比较烦。我思考之后,发现“规范化”只是为了将语法树转成两层的或与树,想到这一点之后,就可以不进行“规范化”了,只要将与或树的特性应用到语法树中即可。具体的判断方案我在代码里以注释的形式写了出来。
本题的解决方案后面的知识,SeasonLee 有一篇文章写得清晰,可供参考:从离散数学到编译原理--百姓网编程题后序(http://www.cnblogs.com/SeasonLee/archive/2010/05/11/1731414.html )
现在这个方案可以实现比较复杂的表达式判断,不过不支持 NOT,也不支持 != 号,只支持 AND、OR、>、< 和 ==,可以使用 () 指定优先级。其实代码不长,约有 100 行是后面测试用的一些代码。
经王建硕先生同意,我把自己的方案公布到博客上,欢迎大家讨论和指正。
1
# encoding:utf-8
2
3
try
:
4
from
ply import
yacc
5
from
ply import
lex
6
except
ImportError:
7
import
sys
8
print
"Please install PLY first.
"
9
print
r"You can download it from here: http://www.dabeaz.com/ply/ply-3.3.tar.gz
"
10
sys.exit(1)
11
12
#############################################
13
# lex
14
#############################################
15
16
# 定义条件表达式的词法
17
tokens = ['FIELD
', 'NUMBER
', 'GT
', 'EQ
', 'LT
', 'AND
', 'OR
', 'LPAREN
', 'RPAREN
']
18
19
t_ignore = '
/t
'
20
t_FIELD = r'(?!and)(?!or)[a-zA-Z_][a-zA-Z0-9_]*
'
21
t_GT = r'>
'
22
t_EQ = r'==
'
23
t_LT = r'<
'
24
t_AND = r'and
'
25
t_OR = r'or
'
26
t_LPAREN = r'/(
'
27
t_RPAREN = r'/)
'
28
29
def
t_NUMBER
(t):
30
r'/d+
'
31
t.value = int(t.value)
32
return
t
33
34
def
t_error
(t):
35
raise
TypeError("Unknow text '%s'
"%t.value)
36
37
lex.lex()
38
39
#############################################
40
# yacc
41
#############################################
42
43
# 用 BNF 定义条件表达式的语法,支持 AND OR,以及 () 的嵌套
44
def
p_expr4
(p):
45
'''
46
expr :
47
'''
48
49
def
p_expr3
(p):
50
'''
51
expr : LPAREN expr RPAREN
52
'''
53
p[0] = p[2]
54
55
def
p_expr2
(p):
56
'''
57
expr : condi
58
'''
59
p[0] = p[1]
60
61
def
p_expr
(p):
62
'''
63
expr : expr logic_op expr
64
'''
65
p[0] = (p[2], p[1], p[3])
66
67
def
p_logic_op
(p):
68
'''
69
logic_op : AND
70
logic_op : OR
71
'''
72
p[0] = p[1]
73
74
def
p_condi
(p):
75
'''condi : FIELD cmp_op NUMBER
'''
76
p[0] = (p[2], p[1], p[3])
77
78
# for i in p:print i
79
80
def
p_cmp_op
(p):
81
'''
82
cmp_op : GT
83
cmp_op : EQ
84
cmp_op : LT
85
'''
86
p[0] = p[1]
87
#for i in p:print i
88
89
def
p_error
(p):
90
raise
TypeError("unknow text at %s
"%p.value)
91
92
yacc.yacc()
93
94
#############################################
95
# util
96
#############################################
97
98
# 以比较漂亮的形式打印语法树
99
INDENT = '
'
100
def
print_tree
(t, indent):
101
if
not
t:
102
print
indent + str(t)
103
return
104
op, l, r = t
105
if
op in
('and
', 'or
'):
106
print
indent + op
107
print_tree(l, indent + INDENT)
108
print_tree(r, indent + INDENT)
109
else
:
110
print
indent + str(t)
111
112
#############################################
113
# judge
114
#############################################
115
OP = ('>
', '<
', '==
')
116
AND = 'and
'
117
OR = 'or
'
118
119
# 对 age < 18 和 age < 10 这种原子条件表达式对行判断
120
# 首先要求字段名要相同,否则返回 false
121
# 然后是根据表达式的比较运算符进行分情况比较
122
def
is_subset_atom_atom
(left, right):
123
assert
left and
right
124
lop, ll, lr = left
125
rop, rl, rr = right
126
assert
lop in
OP and
rop in
OP
127
if
ll != rl:
128
return
False
129
if
lop == rop == '>
':
130
return
lr >= rr
131
elif
lop == rop == '<
':
132
return
lr <= rr
133
elif
lop == rop == '==
':
134
return
lr == rr
135
elif
lop == '>
':
136
return
False
137
elif
lop == '<
':
138
return
False
139
elif
lop == '==
':
140
if
rop == '>
':
141
return
lr > rr
142
else
: # '<'
143
return
lr < rr
144
else
:
145
raise
RuntimeError('Error
')
146
147
# 比较 age < 10 和 age < 10 and sex == 0 这种形式的复合表达式
148
# 这种情况下要求左式必须是右式的两个子式的子集才返回为 true
149
def
is_subset_atom_and
(left, right):
150
assert
left and
right
151
rop, rl, rr = right
152
global
is_subset
153
return
is_subset(left, rl) and
is_subset(left, rr)
154
155
# 比较 age < 10 和 age < 10 or sex == 0 这种形式的复杂表达式
156
# 这种情况下只需要左式是右式中的任一子式的子集即返回 true
157
def
is_subset_atom_or
(left, right):
158
assert
left and
right
159
rop, rl, rr = right
160
global
is_subset
161
return
is_subset(left, rl) or
is_subset(left, rr)
162
163
# 比较 age < 10 and sex == 0 和 age < 10 这种复合表达式
164
# 要求左式的任一子式为右式的子集即返回 true
165
def
is_subset_and_atom
(left, right):
166
assert
left and
right
167
lop, ll, lr = left
168
global
is_subset
169
return
is_subset(ll, right) or
is_subset(lr, right)
170
171
# 比较 age < 10 and sex == 0 和 age < 18 and sex == 1 这种复合表达式
172
# 要求左式的任一子式必须都是右式的*某一*子式的子集才返回 true
173
def
is_subset_and_and
(left, right):
174
assert
left and
right
175
lop, ll, lr = left
176
rop, rl, rr = right
177
global
is_subset
178
lresult = is_subset(ll, rl) or
is_subset(ll, rr)
179
rresult = is_subset(lr, rl) or
is_subset(lr, rr)
180
return
lresult and
rresult
181
182
# 比较 age < 10 and sex == 0 和 age < 10 or sex == 1 这种复合表达式
183
# 要求整个左式必须是右式的某一子式为真才返回 true
184
def
is_subset_and_or
(left, right):
185
assert
left and
right
186
lop, ll, lr = left
187
rop, rl, rr = right
188
global
is_subset
189
return
is_subset(left, rl) or
is_subset(left, rr)
190
191
# 比较 age < 10 or sex == 0 和 age < 10 这种复合表达式
192
# 要求左式的任一子式都必须是右式的子集才返回 true
193
def
is_subset_or_atom
(left, right):
194
assert
left and
right
195
lop, ll, lr = left
196
global
is_subset
197
return
is_subset(ll, right) and
is_subset(lr, right)
198
199
# 比较 age < 10 or sex == 0 和 age < 10 and sex == 0 这种复合表达式
200
# 与 is_subset_or_atom 是一样的。
201
def
is_subset_or_and
(left, right):
202
return
is_subset_or_atom(left, right)
203
204
# 比较 age < 10 or sex == 0 和 age < 10 or sex == 0 这种复合表达式
205
# 与 is_subset_or_atom 是一样的。
206
def
is_subset_or_or
(left, right):
207
return
is_subset_or_atom(left, right)
208
209
# 根据左右式的操作符分派给上述的判断函数
210
def
is_subset
(left, right):
211
assert
left and
right
212
print
'left:
',left
213
print
'right:
',right
214
lop, ll, lr = left
215
rop, rl, rr = right
216
if
lop in
OP:
217
if
rop in
OP:
218
return
is_subset_atom_atom(left, right)
219
elif
rop == AND:
220
return
is_subset_atom_and(left, right)
221
elif
rop == OR:
222
return
is_subset_atom_or(left, right)
223
raise
RuntimeError('Error
')
224
elif
lop == 'and
':
225
if
rop in
OP:
226
return
is_subset_and_atom(left, right)
227
elif
rop == AND:
228
return
is_subset_and_and(left, right)
229
elif
rop == OR:
230
return
is_subset_and_or(left, right)
231
else
:
232
raise
RuntimeError('Error
')
233
elif
lop == 'or
':
234
if
rop in
OP:
235
return
is_subset_or_atom(left, right)
236
elif
rop == AND:
237
return
is_subset_or_and(left, right)
238
elif
rop == OR:
239
return
is_subset_or_or(left, right)
240
else
:
241
raise
RuntimeError('Error
')
242
else
:
243
raise
RuntimeError('Error
')
244
245
#############################################
246
# query
247
#############################################
248
class
Query
(object):
249
def
__init__
(self, q):
250
self._q = q
251
self._tree = yacc.parse(q)
252
assert
self._tree
253
254
def
is_subset
(self, other):
255
if
not
self._tree:
256
return
False
257
if
not
other._tree:
258
return
True
259
return
is_subset(self._tree, other._tree)
260
261
#############################################
262
# test
263
#############################################
264
if
__name__ == '__main__
':
265
t0 = Query('age > 40
') # 中年人
266
t1 = Query('age > 18
') # 成年人
267
print
t0.is_subset(t0)
268
print
t0.is_subset(t1)
269
print
t1.is_subset(t0)
270
print
t1.is_subset(t1)
271
print
'-
'*30
272
273
t2 = Query('age > 18 and weight < 100
') # 成年瘦子
274
t3 = Query('age > 18 or weight < 100
') # 成年人,或体重小于 100
275
print
t0.is_subset(t2)
276
print
t0.is_subset(t3)
277
278
print
t2.is_subset(t0)
279
print
t2.is_subset(t3)
280
281
print
t3.is_subset(t2)
282
283
r0 = Query('age > 30 and sex == 0
')
284
r1 = Query('age > 40 and sex == 0
')
285
286
print
r0.is_subset(r1)
287
print
r1.is_subset(r0)
288
print
'=
'*30
289
290
t0 = Query('(age < 15 and sex == 0) or age > 30
')
291
t1 = Query('age < 7
')
292
t2 = Query('age < 18
')
293
print_tree(t0._tree, '')
294
print
'*
'*30
295
assert
't0 is subset of t0:
' and
t0.is_subset(t0) == True
296
print
'-
'*30
297
assert
't0 is subset of t1:
' and
t0.is_subset(t1) == False
298
print
'-
'*30
299
assert
't1 is subset of t0:
' and
t1.is_subset(t0) == False
300
print
'-
'*30
301
assert
't2 is subset of t0:
' and
t2.is_subset(t0) == False
302
print
'-
'*30
303
304
q0 = Query('age < 15
')
305
q1 = Query('age > 30
')
306
q2 = Query('age > 18
')
307
q3 = Query('age > 40
')
308
q4 = Query('age > 30 and sex == 0
')
309
310
311
assert
'q0 is subset of q0:
' and
q0.is_subset(q0) == True
312
print
'-
'*30
313
assert
'q0 is subset of q1:
' and
q0.is_subset(q1) == False
314
print
'-
'*30
315
assert
'q0 is subset of q2:
' and
q0.is_subset(q2) == False
316
print
'-
'*30
317
assert
'q0 is subset of q3:
' and
q0.is_subset(q3) == False
318
print
'-
'*30
319
assert
'q0 is subset of q4:
' and
q0.is_subset(q4) == False
320
print
'-
'*30
321
print
322
323
assert
'q1 is subset of q0:
' and
q1.is_subset(q0) == False
324
print
'-
'*30
325
assert
'q1 is subset of q1:
' and
q1.is_subset(q1) == True
326
print
'-
'*30
327
assert
'q1 is subset of q2:
' and
q1.is_subset(q2) == True
328
print
'-
'*30
329
assert
'q1 is subset of q3:
' and
q1.is_subset(q3) == False
330
print
'-
'*30
331
assert
'q1 is subset of q4:
' and
q1.is_subset(q4) == False
332
print
'-
'*30
333
print
334
335
assert
'q2 is subset of q0:
' and
q2.is_subset(q0) == False
336
print
'-
'*30
337
assert
'q2 is subset of q1:
' and
q2.is_subset(q1) == False
338
print
'-
'*30
339
assert
'q2 is subset of q2:
' and
q2.is_subset(q2) == True
340
print
'-
'*30
341
assert
'q2 is subset of q3:
' and
q2.is_subset(q3) == False
342
print
'-
'*30
343
assert
'q2 is subset of q4:
' and
q2.is_subset(q4) == False
344
print
'-
'*30
345
print
346
347
assert
'q3 is subset of q0:
' and
q3.is_subset(q0) == False
348
print
'-
'*30
349
assert
'q3 is subset of q1:
' and
q3.is_subset(q1) == True
350
print
'-
'*30
351
assert
'q3 is subset of q2:
' and
q3.is_subset(q2) == True
352
print
'-
'*30
353
assert
'q3 is subset of q3:
' and
q3.is_subset(q3) == True
354
print
'-
'*30
355
assert
'q3 is subset of q4:
' and
q3.is_subset(q4) == False
356
print
'-
'*30
357
print
358
359
assert
'q4 is subset of q0:
' and
q4.is_subset(q0) == False
360
print
'-
'*30
361
assert
'q4 is subset of q1:
' and
q4.is_subset(q1) == True
362
print
'-
'*30
363
assert
'q4 is subset of q2:
' and
q4.is_subset(q2) == True
364
print
'-
'*30
365
assert
'q4 is subset of q3:
' and
q4.is_subset(q3) == False
366
print
'-
'*30
367
assert
'q4 is subset of q4:
' and
q4.is_subset(q4) == True
368
print
'-
'*30
369
370