正则表达式转化为自动机

2024年3月24日
2021年10月15日
Rust Regex

正则文法,3-型文法,可用正则表达式描述,可被有限状态自动机接受。

一般,正则表达式到 DFA 转化分为三步:

  • 正则表达式转化为 NFA
  • NFA 转化 DFA
  • DFA 最小化

正则表达式转化为 NFA

正则表达式的定义

本文只实现了最基础的正则表达式,仅支持连接 ab,选择 a|b 和 Kleene 星号 a* 运算。此外,因为没有做字符转义,所以 ε 将被特殊符号处理。程序未考虑运算优先级,没有使用括号指明的都当作 UB 考虑。

预处理正则表达式

根据 Regular Expression Matching Can Be Simple And Fast 这篇文章。预先将正则表达式预处理为后缀表达式再转化到 NFA 更方便。对于省略的连接运算符,相邻两个字符,字符与括号,星号和字符之间都需要额外补上。例如 aa(((bc)|(de))*)f 补上省略的连接符(用加号表示)a+a+(((b+c)|(d+e))*)+f,转为后缀 aabc+de+|*f+++

pub fn regex2post(r: &str) -> Vec<RegexToken> {
    use RegexToken::{Alter, Cat, Bracket, Closure, Char};
    let mut stack = Vec::new();
    let mut post = Vec::new();
    let mut add_cat = false; // 表示是否需要一个连接符
    for c in r.chars() {
        match c {
            '|' => {
                add_cat = false; // 新的双目运算符直接取代了连接符
                stack.push(Alter);
            }
            '(' => {
                if add_cat {
                    stack.push(Cat);
                    add_cat = false; // 一个左括号可以连接前面的,但不能连接后面的
                }
                stack.push(Bracket);
            }
            ')' => {
                if !add_cat { unreachable!(); }
                while !matches!(stack.last(), Some(Bracket) | None) {
                    post.push(stack.pop().unwrap());
                }
                stack.pop();
            }
            '*' => { // 单目运算符不影响状态
                post.push(Closure);
            }
            'ε' => {
                if add_cat {
                    stack.push(Cat);
                }
                add_cat = true;
                post.push(Epsilon);
            }
            _ => {
                if add_cat {
                    stack.push(Cat);
                }
                add_cat = true; // 字符即可连接前面也可连接后面
                post.push(Char(c));
            }
        }
    }
    if !add_cat { unreachable!(); }
    while let Some(r) = stack.pop() {
        post.push(r);
    }
    post
}

后缀表达式转 NFA

rust 用指针不是很方便,所以这里用数组来存放 NFA 的图结构。

pub struct NFA {
    pub nodes: Vec<Option<char>>,
    pub edges: Vec<(usize, usize)>,
    pub start: usize,
    pub out: usize,
}

用一个栈来存放后缀表达式转化到 NFA 中临时的图,使用边来表示(即想象将这个图的入口和出口用一条有向边直接连起来),暂且称之为块。

对于字符,直接向图中添加新节点,压入下标。

RegexToken::Char(c) => {
    let idx = nfa.add_node(NFANode::new(Some(*c)));
    stack.push((idx, idx));
}

RegexToken::Epsilon => {
    let idx = nfa.add_node(NFANode::new(None));
    stack.push((idx, idx));
}

对于连接符,弹出两个块,块甲和块乙(注意先后)。连接乙的出和甲的入(这条边是可以确定下来存入 NFA 中的),再压入乙的入和甲的出。

      +---+    +---+
in -> | B | -> | A | -> out 
      +---+    +---+
RegexToken::Cat => {
    let (e2_start, e2_out) = stack.pop().unwrap();
    let (e1_start, e1_out) = stack.pop().unwrap();
    nfa.add_edge(e1_out, e2_start);
    stack.push((e1_start, e2_out));
}

对于选择符,弹出两个块,需要两个新节点做辅助,并可确定四条边。

              +---+
        +---> | A | --->+
      +---+   +---+   +---+
in -> |   |           |   | -> out
      +---+   +---+   +---+
        +---> | B | --->+
              +---+
RegexToken::Alter => {
    let (e2_start, e2_out) = stack.pop().unwrap();
    let (e1_start, e1_out) = stack.pop().unwrap();
    let a_start = nfa.add_node(NFANode::new(None));
    let a_out = nfa.add_node(NFANode::new(None));
    nfa.add_edge(a_start, e1_start);
    nfa.add_edge(a_start, e2_start);
    nfa.add_edge(e1_out, a_out);
    nfa.add_edge(e2_out, a_out);
    stack.push((a_start, a_out));
}

对于星号,弹出一个块,需要一个新节点做辅助,并可确定两条边。

              +---+
        +---> | A |
      +---+   +---+
in -> |   | <---+  
      +---+
        +---> out
RegexToken::Closure => {
    let idx = nfa.add_node(NFANode::new(None));
    let (e_start, e_out) = stack.pop().unwrap();
    nfa.add_edge(idx, e_start);
    nfa.add_edge(e_out, idx);
    stack.push((idx, idx));
}

最后栈里剩下的一个块就是完整的图了,两个下标分别就是 NFA 的初始状态和接收状态,后缀表达式到 NFA 的转化就做完了。

NFA 转 DFA

NFA 转 DFA 使用子集构造法,这需要为 NFA 增加一个 get_reach 函数计算一个状态消耗一个字符可到达的状态的集合。另外 DFA 没有采用 NFA 一样的数据结构,而是直接使用了一个二维数组表示。

pub struct DFA {
    pub accepts: Vec<char>,
    pub table: Vec<Vec<Option<usize>>>,
    pub start: usize,
    pub out: Vec<usize>,
}

nfa.get_reach(nfa.start, None) 作为初始状态,求新的状态,直到没有新的状态产生为止。所有包含了原终结状态的新状态都作为 DFA 的终结状态。

pub fn determinize(nfa: &NFA) -> DFA {
    let start = nfa.get_reach(nfa.start, None);
    let mut table = Vec::new();
    let mut states = vec![start.clone()];
    let accepts: Vec<_> = nfa
        .get_accepts()
        .into_iter()
        .filter(|x| matches!(x, Some(_)))
        .map(|x| x.unwrap())
        .collect(); // 输入符号集合
    while !states.is_empty() {
        let state = states.pop().unwrap();
        let new_states: Vec<_> = accepts
            .iter()
            .map(|a| {
                let mut new_state: Vec<_> = state
                    .iter()
                    .map(|s| nfa.get_reach(*s, Some(*a)))
                    .flatten()
                    .collect();
                new_state.sort_unstable();
                new_state.dedup();
                new_state
            })
            .collect();
        table.push((state, new_states.clone()));
        for new_state in new_states {
            if !new_state.is_empty()
                && !table.iter().any(|s| s.0 == new_state)
                && !states.contains(&new_state)
            {
                states.push(new_state) // 只有新的状态才加入计算队列
            }
        }
    }

    let rename: HashMap<Vec<usize>, usize> = table
        .iter()
        .enumerate()
        .map(|(i, s)| (s.0.clone(), i))
        .collect();

    DFA {
        accepts,
        table: table
            .into_iter()
            .map(|(_, ns)| {
                ns.iter()
                    .map(|n| rename.get(n).copied())
                    .collect::<Vec<_>>()
            })
            .collect(),
        start: *rename.get(&start).unwrap(),
        out: rename
            .into_iter()
            .filter(|(s, _)| s.contains(&nfa.out))
            .map(|(_, i)| i)
            .collect(),
    }
}

最小化 DFA

最小化 DFA 使用的是 Hopcroft 算法,但是不同于网上算法,这里首先求出了状态的划分,再重新建立跳转表。划分状态首先要判断两个状态是否可区分(distinguish),即能否有串能被甲状态接受而不被乙状态接受。如果甲、乙接受一个字符跳转到的状态丙、丁是可区分的,那么甲、乙是可区分的。根据定义,一个终结状态和一个非终结状态一定是可区分的(终结状态可接受 ε 而非终结状态不能)。由此可以递归地求出任意两个状态是否是可区分的。

例如:

   | a | b |
 0 | 2 | 1 |
 1 | 2 | 1 |
 2 | 2 | 1 |

其中 0 是初始状态,1 是终结状态。这里的 0 和 2 就是不可区分的两个状态,因为没有一个串可以被 0 接受而不被 2 接受。所以状态就被划分为 {0, 2} 和 1。

pub fn distinguishable(&self, p: usize, q: usize) -> bool {
        use TransRes::{Accept, Next, Unaccept};
        let p_out = self.out.contains(&p);
        let q_out = self.out.contains(&q);
        if p_out ^ q_out {
            return true;
        }
        if p == q {
            return false;
        }
        let mut accepts: Vec<_> = self.accepts
            .iter()
            .map(|a| Some(*a))
            .collect();
        accepts.push(None);
        for a in accepts {
            let r = self.get_trans(p, a);
            let s = self.get_trans(q, a);
            match (r, s) {
                (Accept, Accept) | (Unaccept, Unaccept) => {}
                (Accept, _) | (_, Accept) | (Unaccept, _) | (_, Unaccept) => {
                    return true;
                }
                (Next(rc), Next(sc)) => {
                    if self.distinguishable(rc, sc) {
                        return true;
                    }
                }
            }
        }
        false
    }

得到了新的一组状态后,需要重建跳转表。因为多个原状态被合并为了一个新状态,所有只要求任意一个原状态的跳转就可得出新状态的跳转了。

pub fn minimize(&self) -> DFA {
        let states = self.get_nondistinguishable_states();
        let rename: HashMap<usize, usize> = states
            .iter()
            .enumerate()
            .map(|(i, x)| x.iter().map(move |tx| (*tx, i)))
            .flatten()
            .collect();
        let mut table = Vec::new();
        for s in states {
            let state_trans = self
                .accepts
                .iter()
                .map(|a| match self.get_trans(*s.first().unwrap(), Some(*a)) {
                    TransRes::Accept => unreachable!(),
                    TransRes::Next(ns) => Some(*rename.get(&ns).unwrap()),
                    TransRes::Unaccept => None,
                })
                .collect::<Vec<_>>();
            table.push(state_trans);
        }
        DFA {
            accepts: self.accepts.clone(),
            table,
            start: *rename.get(&self.start).unwrap(),
            out: {
                let mut o: Vec<_> = self.out
                    .iter()
                    .map(|x| *rename.get(x).unwrap())
                    .collect();
                o.sort_unstable();
                o.dedup();
                o
            },
        }
    }