wfSet,良构不变量,引理调引理

3 minute read Published: 2026-08-27

uniq-set.dfy

Dafny 4.11.0 全绿的完整答案版。抄写顺序 = 文件顺序 = 难度顺序。带 ★ 的是考点,考前一天只复习 ★

本文件要证的五条引理:

member(x,insert(y,A)) <==> x == y || member(x,A)
member(x,delete(y,A)) <==> x != y && member(x,A)
member(x,union(A,B)) <==> member(x,A) || member(x,B)
member(x,inter(A,B)) <==> member(x,A) && member(x,B)
member(x,A) && subset(A,B) ==> member(x,B)

member 与 wfSet:良构不变量

wf 是 well-formed(良构)的缩写:头不在尾里,且尾自身良构——整条列表无重复。

include "../core-list.dfy"

predicate member(x : int, A : lset)
{
    match A
    case Nil => false
    case Cons(y,ys) => x == y || member(x,ys)
}

predicate wfSet( A : lset)
{
    match A
    case Nil => true
    case Cons(x,xs) => !member(x,xs) && wfSet(xs)
}

insert:让 member 把门

设计:让 member 把门,不在集合里才 Cons,insert 天生不制造重复。代价换来的好处:insert 不递归 => 下面两条引理不需要归纳,Dafny 展开一层定义就能自己看出来,空体 {} 即可通过

requires 不能删:A 自己带病(如 [1,1]),insert 救不回来。

function insert(x: int, A: lset): lset
{
    if member(x, A) then A else Cons(x, A)
}

lemma member_insert(x: int, y: int, A: lset)
  ensures member(x, insert(y, A)) <==> x == y || member(x, A)
{
}

lemma wfSet_insert(x: int, A: lset)
  requires wfSet(A)
  ensures wfSet(insert(x, A))
{
}

delete:真归纳的起点

规格 member_delete 没带 wf 前提 => 实现必须"删干净":命中头之后尾巴还要继续删(x==y 分支里仍递归),不能只删第一个。

归纳的标准形状:match 拆结构,对尾巴调用引理自己 = 归纳假设delete 是递归函数,所以这里开始必须真归纳,空体不算你的功

function delete(x: int, A: lset): lset
{
    match A
    case Nil => Nil
    case Cons(y, ys) => if x == y then delete(x, ys) else Cons(y, delete(x, ys))
}

lemma member_delete(x: int, y: int, A: lset)
  ensures member(x, delete(y, A)) <==> x != y && member(x, A)
{
    match A
    case Nil =>
    case Cons(z, zs) =>
        member_delete(x, y, zs);
}

wfSet_delete:第一次"引理调引理"

★★ Cons 分支缺的事实是 !member(y, delete(x, ys)),由 member_delete(y, x, ys) 提供——注意实参顺序:问的是"头 y 在不在删完 x 的尾巴里",所以 y 在前 x 在后。喂反必红

lemma wfSet_delete(x: int, A: lset)
  requires wfSet(A)
  ensures wfSet(delete(x, A))
{
    match A
    case Nil =>
    case Cons(y, ys) =>
        wfSet_delete(x, ys);      // 归纳假设:尾巴删完仍良构
        member_delete(y, x, ys);  // 弹药:头 y 不会出现在删完的尾巴里
}

inter:只需要 A 干净

以 A 为骨架过滤:头在 B 里才留下。

只需要 wfSet(A):结果的骨架全部来自 A,B 脏不脏无所谓

function inter(A: lset, B: lset): lset
{
    match A
    case Nil => Nil
    case Cons(x, xs) => if member(x, B) then Cons(x, inter(xs, B)) else inter(xs, B)
}

lemma member_inter(x: int, A: lset, B: lset)
  ensures member(x, inter(A, B)) <==> member(x, A) && member(x, B)
{
    match A
    case Nil =>
    case Cons(y, ys) =>
        member_inter(x, ys, B);
}

lemma wfSet_inter(A: lset, B: lset)
  requires wfSet(A)
  ensures wfSet(inter(A, B))
{
    match A
    case Nil =>
    case Cons(x, xs) =>
        wfSet_inter(xs, B);
        member_inter(x, xs, B);  // 弹药:x 不在 xs 里 => x 不在 inter(xs,B) 里
}

subset:单向 ==>

A 的每个头都得在 B 里。

这条是单向 ==>,不是 <==>:x 在 B 里推不回"x 在 A 里"。

predicate subset(A: lset, B: lset)
{
    match A
    case Nil => true
    case Cons(x, xs) => member(x, B) && subset(xs, B)
}

lemma member_subset(x: int, A: lset, B: lset)
  ensures member(x, A) && subset(A, B) ==> member(x, B)
{
    match A
    case Nil =>
    case Cons(y, ys) =>
        member_subset(x, ys, B);
}

union:这次 A、B 都要干净

逐个把 A 的元素垒到 B 前面:已在 B 里就跳过(和 insert 同一个把门思路)。

这次 A、B 都要干净:Nil 分支直接返回 B,B 的病没人治。(对比 inter 只要 A 干净——差别全在 Nil 分支返回什么。)

function union(A: lset, B: lset): lset
{
    match A
    case Nil => B
    case Cons(x, xs) => if member(x, B) then union(xs, B) else Cons(x, union(xs, B))
}

lemma member_union(x: int, A: lset, B: lset)
  ensures member(x, union(A, B)) <==> member(x, A) || member(x, B)
{
    match A
    case Nil =>
    case Cons(y, ys) =>
        member_union(x, ys, B);
}

lemma wfSet_union(A: lset, B: lset)
  requires wfSet(A) && wfSet(B)
  ensures wfSet(union(A, B))
{
    match A
    case Nil =>
    case Cons(x, xs) =>
        wfSet_union(xs, B);
        member_union(x, xs, B);  // 弹药:x 既不在 xs 也不在 B => 不在 union(xs,B)
}

card:势只在良构时有意义

就是数长度。注意它对 [1,1] 会数出 2:"势"只在 wfSet 成立时才有集合意义——这正是良构不变量存在的理由

function card(A: lset): nat
{
    match A
    case Nil => 0
    case Cons(_, xs) => 1 + card(xs)
}