如果集合 A 的每个元素也都是集合 B 的元素,就说 A 是 B 的子集,写作 A ⊆ B。例如 {1, 2} ⊆ {1, 2, 3}。每个集合都是它自己的子集,∅ 是每个集合的子集。不要混淆 ∈ 和 ⊆。1 ∈ {1, 2} 说的是 1 是一个元素;{1} ⊆ {1, 2} 说的是只含 1 的那个集合是一个子集。集合也可以是另一个集合的元素。集合 {∅} 恰好有一个元素,这个元素就是空集。所以 |{∅}| 是 1,而 |∅| 是 0。里面装着一个空盒子的盒子,并不是空盒子。
两个集合有三种组合方式。并集 A ∪ B 包含属于 A 或属于 B 或两者都属于的所有元素。交集 A ∩ B 包含同时属于两者的元素。差集 A − B 包含属于 A 但不属于 B 的元素。取 A = {1, 2, 3},B = {3, 4}:A ∪ B = {1, 2, 3, 4},A ∩ B = {3},A − B = {1, 2}。
A={1,2,3}B={3,4}print("2 in A:",2inA)print("5 in A:",5inA)print("size of {3, 1, 2, 2}:",len({3,1,2,2}))print("A union B:",sorted(A|B))print("A intersect B:",sorted(A&B))print("A minus B:",sorted(A-B))print("{1, 2} subset of A:",{1,2}<=A)print("A subset of B:",A<=B)print("size of the empty set:",len(set()))print("size rule holds:",len(A|B)==len(A)+len(B)-len(A&B))
先预测输出,再看结果
预期输出
2 in A: True
5 in A: False
size of {3, 1, 2, 2}: 3
A union B: [1, 2, 3, 4]
A intersect B: [3]
A minus B: [1, 2]
{1, 2} subset of A: True
A subset of B: False
size of the empty set: 0
size rule holds: True
逐步讲解
A = {1, 2, 3} 建立一个集合并取名 A。Python 里的花括号和纸上一样,用来写集合。
单词 in 提出属于关系的问题,答案是 True 或 False。2 in A 就是 Python 写法的 2 ∈ A。
read 1 -> state odd
read 1 -> state even
read 0 -> state even
read 1 -> state odd
'' even True
'1' odd True
'11' even True
'0110' even True
'10101' odd True
反证法是这样的。要说明一个命题为真,先假设它为假。顺着推下去,直到得出某个不可能的结果,比如一个数既是偶数又是奇数。正确的推理不可能导出不可能的事,所以假设一定是错的,命题就为真。如果命题的形式是“如果 P 那么 Q”,要假设的是 P 为真而 Q 为假。
看一个例子。命题:如果 n × n 是偶数,那么 n 是偶数。用反证法,假设 n × n 是偶数而 n 不是偶数。不是偶数的整数就是奇数,所以对某个整数 m,n = 2m + 1。于是 n × n = 4m² + 4m + 1 = 2(2m² + 2m) + 1,这是奇数。但我们假设了 n × n 是偶数,而没有数既是偶数又是奇数。这就是矛盾。所以假设是错的,命题成立。
归纳法分两部分证明对每个 k = 0, 1, 2, ... 成立的命题。基本情形证明命题对 k = 0 成立。归纳步骤假设命题对某个 k 成立,再证明它对 k + 1 成立。这样命题对 0 成立,于是对 1 成立,于是对 2 成立,依此类推,就像一排多米诺骨牌。例子命题:{a, b} 上长度为 k 的字符串恰好有 2^k 个。基本情形,k = 0:只有空串的长度是 0,而 2^0 是 1。归纳步骤:长度为 k + 1 的每个字符串,都是一个长度为 k 的字符串后面加一个符号 a 或 b。由假设,长度为 k 的字符串有 2^k 个,每个得到 2 个长度为 k + 1 的字符串。所以共有 2 × 2^k = 2^(k+1) 个。
# Claim 1: if n * n is even, then n is even.counterexamples=[]forninrange(1000):if(n*n)%2==0andn%2==1:counterexamples.append(n)print("counterexamples to claim 1:",counterexamples)# A false claim: if n * n is even, then n is a multiple of 4.forninrange(1000):if(n*n)%2==0andn%4!=0:print("counterexample to the false claim:",n)break# Claim 2: there are 2 ** k strings of length k over {a, b}.strings=[""]forkinrange(7):print(k,len(strings),2**k)assertlen(strings)==2**klonger=[]forsinstrings:longer.append(s+"a")longer.append(s+"b")strings=longer
这证明的是相反的方向:如果 n 是偶数,则 n * n 是偶数。而命题是:如果 n * n 是偶数,则 n 是偶数。要用反证法证明它,应当从“n * n 是偶数而 n 不是偶数”出发,推出不可能的结果。
正确写法
Assume n * n is even and n is odd. Then n * n is odd. Contradiction.
错误写法
# Claim: a string of length k has k + 1 symbols.
# Step: if length k has k + 1 symbols, adding one symbol gives k + 2 symbols for length k + 1.
# So it is true for every k.
Python 里 and、or、not 是普通的词,True 和 False 是它的两个真值。它没有表示蕴含的词。我们写 (not p) or q,它的真值表和 p → q 相同。两层循环,一个套在另一个里面,就能走遍每一行。在这里,检查每一行就是完整的证明,因为只有四行,而检查覆盖了全部。暴力检查恰好在它覆盖每一个情形时才是证明。
新词
命题
要么真要么假的陈述。
¬ ∧ ∨ →
非、且、或、蕴含。Python 里:not、and、or,以及 (not p) or q。
真值表
每一行对应字母真假的一种组合,并给出陈述在该行的值的表。
等价
两个陈述的真值表在每一行都一致,它们就等价。
空真
前半部分为假的蕴含是真的,因为没有承诺任何东西。
完整例子
Python 3
defimplies(p,q):return(notp)orqprint("p q | p and q | p or q | not p | p implies q")forpin[True,False]:forqin[True,False]:print(p,q,"|",pandq,"|",porq,"|",notp,"|",implies(p,q))same=Trueforpin[True,False]:forqin[True,False]:if(not(pandq))!=((notp)or(notq)):same=Falseprint("not (p and q) equals (not p) or (not q):",same)
先预测输出,再看结果
预期输出
p q | p and q | p or q | not p | p implies q
True True | True | True | False | True
True False | False | True | False | False
False True | False | True | True | True
False False | False | False | True | True
not (p and q) equals (not p) or (not q): True
每个打印出的行显示 p、q,然后是 p and q、p or q、not p 和 implies(p, q)。读第二行:p 是 True,q 是 False,所以“p and q”是 False,“p or q”是 True,“not p”是 False,“p implies q”是 False。这是 implies 为 False 的唯一一行。
第三、四行 p 是 False,implies 是 True。这又是那个带伞的承诺:什么都没有被检验,所以什么也没有失败。
集合构造记号(描述法)。{x ∈ S : P(x)} 读作“S 中所有使 P(x) 成立的 x 组成的集合”。冒号前面的部分说明 x 从哪里来,冒号后面的部分说明保留哪些 x。有些书用竖线 | 代替冒号。例如 {w ∈ {0, 1}^2 : w 以 1 结尾} 是 {01, 11}。在 Python 里,这是用带筛选条件的循环构造的集合:{w for w in S if P(w)}。
δ(q, a)。符号 δ(德尔塔)是机器的转移函数。δ(q, a) 读作“在状态 q 读到符号 a 时到达的状态”。它只接收一个符号。像 {("q0", "1"): "q1", ...} 这样的 Python 字典就是同一个函数,δ(q, a) 就是 delta[(q, a)]。对整个字符串,书上用第二个函数,写作 δ*(q, w),它对 w 的每个符号各用一次 δ。
量词。符号 ∀ 表示“对所有”。“∀w ∈ S : P(w)”说的是 P 对 S 中每个 w 都成立。符号 ∃ 表示“存在”。“∃w ∈ S : P(w)”说的是 S 中至少有一个 w 满足 P。Python 的 all(...) 和 any(...) 在有限集合上做同样的事。课程中的其他符号:ε 是空串,∅ 是空集,∪ 和 ∩ 是并集和交集,|w| 是字符串的长度,|S| 是集合的大小,⊆ 表示子集。
新词
Σ^k
Σ 上长度恰好为 k 的字符串的集合。
集合构造记号(描述法)
{x ∈ S : P(x)} 是 S 中所有使 P(x) 成立的 x 组成的集合。冒号的意思是“使得”。
δ(q, a)
在状态 q 读到符号 a 时到达的状态。
∀ 和 ∃
“对所有”和“存在”。在有限集合上,Python 里是 all(...) 和 any(...)。
下标
写在符号下方的小数字或小字母,是名字的一部分,如 q₀。
完整例子
Python 3
sigma={"0","1"}# Σ, the alphabet# Σ^2: all strings of length 2 over Σsigma_2=set()forainsigma:forbinsigma:sigma_2.add(a+b)print("sigma^2 =",sorted(sigma_2))# set-builder: {w in Σ^2 : w ends in 1}ends_in_1={wforwinsigma_2ifw.endswith("1")}print("ends in 1:",sorted(ends_in_1))# δ(q, a): a transition function stored as a tabledelta={("q0","0"):"q0",("q0","1"):"q1",("q1","0"):"q0",("q1","1"):"q1"}print("delta(q0, 1) =",delta[("q0","1")])print("delta(q1, 0) =",delta[("q1","0")])# for all, there existsprint("all have length 2:",all(len(w)==2forwinsigma_2))print("some w equals 11:",any(w=="11"forwinsigma_2))
先预测输出,再看结果
预期输出
sigma^2 = ['00', '01', '10', '11']
ends in 1: ['01', '11']
delta(q0, 1) = q1
delta(q1, 0) = q0
all have length 2: True
some w equals 11: True