“买了票就能进站吗?”

如果告示只写着“进站必须持有效车票”,答案还不能确定。票可能是必需的,但安检尚未通过时,工作人员仍然可以拒绝放行。

这在需求讨论里很常见。“只有登录用户才能下载”,有人会写成“只要登录就允许下载”;“管理员或获授权成员可以编辑”,有人会漏掉后面的“且账号未被冻结”。句子都很短,执行结果却可能相差很大。

上一章中,少算一次换乘就会改变路线选择。这一章先停在闸机前:怎样把一句规则写成可以逐项核对、可以交给程序执行的判断?

先固定一句话在说谁

我们只讨论某个入口的一次进站请求。票是否有效、安检是否通过,都以这次请求的同一份状态快照为准。设:

  • T:这次请求持有有效票
  • E:规则允许这次请求进站

这里的 E 表示“获得许可”。乘客获准后是否真的走过闸机,是另一个问题。

在这个范围内,T、E 各自有“真”或“假”两个取值,这样的陈述叫作命题。问句“票有效吗”本身不作真假判断;“这张票有效”才可以作判断。命题有确定真值,也不代表我们已经知道它的真值。MIT《Mathematics for Computer Science》第一章从这个定义开始建立命题逻辑。

如果要同时检查很多请求,可以写 T(x),表示“请求 x 持有有效票”。这种留下对象位置的判断叫作谓词。代入一个具体请求,便得到一个命题。以后我们把日志中的每条请求交给同一段条件,做的就是这件事。

“必须有”和“有了就够”方向相反

“进站必须持票”写作:

E → T

箭头读作“如果左边为真,那么右边也必须为真”。所以,有效票是获得进站许可的必要条件:缺少它就不能获准。反过来,“持有效票就允许进站”应写成 T → E;这时,有效票是获得许可的充分条件:有它便足够。

一般地,P → Q 表示 P 是 Q 的充分条件,Q 是 P 的必要条件。只记名称容易混,先读一遍“P 成立时,Q 必须成立”更可靠。

图1:有效票 T 与进站许可 E 的四种组合,分别检验两个方向的条件

图1里,“有票但未获准”推翻了 T → E,却完全符合 E → T。只需找到这样一个反例,就能阻止我们把必要条件偷偷升级成充分条件。

这里要注意,经典逻辑中的 P → Q 只有一种失败情况:P 真、Q 假。P 假时,无论 Q 怎样,这个条件式都算真,因为它对“P 不成立时怎样”没有提出要求。检查所有请求是否遵守 E → T,就是寻找“获准但没票”的请求;没获准的请求无法成为这条规则的反例。

这也解释了为什么 E → T 与“没有有效票就不允许进站”,也就是 ¬T → ¬E,说的是同一个限制。符号 ¬ 表示“非”。但“未获准,所以没有票”仍然推不出来,未获准还可能有别的原因。

箭头本身也不声明因果。上面的规则约束哪些真假组合可以出现;它没有证明购票这个动作在现实中必然导致闸机打开。

把多个检查组合起来

现在明确约定一套简化规则:正常运营、设备正常、不设免票通道;持有效票、通过安检、未被暂停通行,这三项同时满足就放行,其他情况全部拒绝。增加两个命题:

  • S:这次请求已通过安检
  • B:这次请求处于暂停通行状态

完整规则成为:

E ↔ (T ∧ S ∧ ¬B)

∧ 表示“且”,各项都真时才真。↔ 读作“当且仅当”,包含左右两个方向。我们能在这里使用双向箭头,是因为刚刚明确约定了全部放行条件,不能仅凭最初那句告示得出它。

另一个常用符号 ∨ 表示“或”,至少一项真就为真,包括两项都真。它不会自动排除“二者都有”。把真记作 1、假记作 0,三个基本操作可以列成表:

PQP ∧ QP ∨ Q¬P
11110
10010
01011
00001

例如 T=1、S=0、B=0 时,¬B=1,但 1∧0∧1=0,仍然拒绝。“未被暂停”只能通过其中一项检查。

同一个条件也能变成集合筛选

假设这次检查共有六条请求,编号是甲、乙、丙、丁、戊、己。把这六条请求组成的集合记为 U,这就是当前讨论的全集。集合只关心元素在不在其中;交换列举顺序、重复写同一编号,都不会改变集合。

用三张名单记录条件为真的请求:

A = {甲,乙,丁,己},持有有效票

C = {甲,丙,丁,己},通过安检

D = {丁},暂停通行

集合 A 与命题 T 不同:A 是一批请求;“甲属于 A”才是一个判断,写作 甲∈A。之所以另用字母命名集合,就是为了把“名单”和“对一条记录的判断”分清。

图2:六条请求按持票、安检、未暂停三个条件筛选,最终保留甲和己

图2的筛选可以逐步算出来。A∩C 叫交集,只留下两张名单共有的元素,因此:

A∩C = {甲,丁,己}

再去掉 D 中的丁,得到允许进站的请求集合:

F = (A∩C)∖D = {甲,己}

∖ 表示集合差,左边有、右边没有的元素才保留。因此,“且”对应交集,“排除”对应集合差。若只要求持票或通过安检,则得到并集 A∪C={甲,乙,丙,丁,己},这显然比原规则宽松。

“非”也有集合版本:U∖D 是 D 在 U 中的补集,包含本次六条请求里所有未暂停的请求。这里必须先说清 U;它不包括另一个入口、另一天尚未检查的人。集合运算的这些定义可对照 Oscar Levin《Discrete Mathematics》第5.1节。

从结果看,F 中每个元素都在 A 中,写作 F⊆A,读作“F 是 A 的子集”。这正是“每个获准请求都有有效票”的集合表达。在这六条数据里,乙属于 A 却不属于 F,也再次给出了“有票便足够”的反例。

两种写法为什么能接上?对 U 中任意一条请求 x,属于 F 就要求它同时属于 A、属于 C、不属于 D;逐项换回判断,恰好就是 T(x)∧S(x)∧¬B(x)。我们既可以拿一条记录问“它满足条件吗”,也可以拿整批记录问“哪些记录满足条件”,使用的是同一套判断。

改写拒绝条件时最容易漏掉什么

程序往往先判断什么时候拒绝。根据完整规则,拒绝条件是:

¬E ↔ ¬(T∧S∧¬B)

有人会把右边直接写成“没票且没通过安检且被暂停”。这要求三件坏事一起发生才拒绝,显然太宽松。

先检查两个条件 P、Q。若它们同时为真,“没有同时满足”就是假;其余三种组合中,总有至少一个为假。因此:

¬(P∧Q) ≡ ¬P∨¬Q

≡ 表示逻辑等价:无论给 P、Q 什么真假组合,两边结果都相同。这个等价式是德摩根律之一,可以用上一张表逐行核对;相应的另一条是 ¬(P∨Q)≡¬P∧¬Q。教材第1.3节给出了它们的真值表与推导。

现在把三项判断分两次展开:

¬(T∧S∧¬B)

≡ ¬(T∧S)∨¬(¬B)

≡ (¬T∨¬S)∨B

≡ ¬T∨¬S∨B

所以,没有效票、没通过安检、被暂停,任何一项成立就拒绝。每一步都保持相同真值,两次否定又回到原来的 B。

图3:放行要三项同时通过,拒绝只需一项失败;左右输出互为取反

拿乙检查:T=1、S=0、B=0,拒绝式得到 0∨1∨0=1。错误的“且”版本得到 0∧1∧0=0,会漏掉它。反例比一句“看起来等价”更能说明问题。

回到权限代码和 AI 工具

如果新增职员通行证 W,约定它只替代车票,安检和暂停检查仍然必须执行,新规则应是:

E ↔ (T∨W)∧S∧¬B

括号标出了“先选择一种有效凭证,再满足共同条件”。写成 T∨(W∧S∧¬B) 就改变了规则:T=1 时整式直接为真,持票请求可以绕过安检。

这是工程上的一个直接用法:审查权限表达式时,为每项条件设计“仅这一项失败”的输入,再检查不同授权分支是否都受共同限制。对 AI 工具调用也是如此,模型提出调用只是一个条件,是否有执行权限还要由明确规则检查;“模型建议了”与“允许执行”不能混为一项。

检查几条日志能发现错误,却未必覆盖所有组合。两个布尔条件共有四种组合,三个有八种,四个有十六种。如果要证明两个关于这四个条件的表达式始终等价,就可以完整列出十六行逐行比较。测试样例的范围和结论的范围,应当对应起来。

实际接入时还有一个边界:本章假定每项检查都有可靠的真假值。若暂停状态查询超时,“还不知道 B”不能随手当成“B 为假”。系统应另外规定未知状态如何处理。逻辑能帮我们检查规则之间是否一致,字段是否可靠、规则是否符合业务目标,仍然需要单独验证。

顺手试一下

题目1:一句话到底保证了什么

已知所有请求都遵守“允许进站的请求必须持有效票”,但没有其他规则。下面哪项一定成立?

  1. 持有效票的请求一定获准
  2. 没有有效票的请求一定不获准
  3. 未获准的请求一定没有有效票

**提示:**只排除 E=1、T=0,看看其余组合是否推翻选项。

**参考答案:**只有第2项。第1、3项都可以被 T=1、E=0 推翻。有效票是许可的必要条件,这条规则没有说明它是否充分。

题目2:增加一种凭证之后

采用 E↔(T∨W)∧S∧¬B。三条请求的 (T,W,S,B) 分别为甲 (0,1,1,0)、乙 (1,0,0,0)、丙 (1,1,1,1)。谁获准?再把拒绝条件展开到每个“非”只作用于一个字母。

**提示:**先算括号内的“或”;否定一个“或”,要让它的两边同时为假。

**参考答案:**仅甲获准。甲的计算为 (0∨1)∧1∧1=1;乙安检失败;丙被暂停。拒绝条件展开为:

¬E ↔ ¬(T∨W)∨¬S∨B

↔ (¬T∧¬W)∨¬S∨B

因此,没有票也没有通行证,或者安检失败,或者被暂停,都会拒绝。双凭证不能抵消暂停状态。

把规则写清楚之后,“为什么没放行”就能定位到具体条件,“这次改动会不会多放进来一类请求”也有了可检查的答案。接下来,我们再把目光从单条请求移开,看看多个对象之间的联系怎样表示。


作者:Jiancheng Wang · 数学之美(独立原创)