确定有限状态自动机(DFA)在解算法题中的应用

自动机介绍

        第一次接触自动机是在大学的数电课上,门电路就是最简单的自动机,接收某个状态,输出下一个状态。我们常用的是米利状态机,下一个状态需要根据输入和当前的状态做转移。状态机除了硬件设计在编译方面也非常好用,我们使用状态机处理字符串来进行token识别,自动机本身是模拟程序的运行,可以形成分支判断,循环等基本结构,所以和程序没有本质区别,但是可以同过定义状态来管理繁多的判断条件,用来处理字符串还是很方便的。下面我就通过一道例题来讲一下状态机在解算法题方面的应用。

例:解析布尔表达式

这道题是 leetcode 1106. 解析布尔表达式
在这里插入图片描述

这道题类似于前缀表达式求值,最好的办法是用栈,题目也挺容易理解,后面会拿出一章来理一下栈解决表达式求值。下面我们用自动机处理字符串,然后进行运算来解决,主要是为了讲解自动机原理。
首先我们对表达式的结构进行分析:

<boolExp>::=<fnExp><EOF> //完整表达式串是由函数表达式和结束符组成
<fnExp>::=<symbol>({<fnExp>|<Bool>,})//对于任意函数表达式由函数符号和括号包起来的若干参数组成,参数可能是函数表达式也可能是布尔值
<symbol>::='&'|'|'|'!' //函数符号可能的值
<Bool>::='t'|'f' //布尔变量可能的值

状态机设计

下面我们通过函数来定义不同的状态(当然也可以通过定义变量,但是js的函数太好用了🤣),并在函数内部完成状态转移:

/* 对于函数内部的判断如果走到最底下,说明解析中有不符合语法规范的字符,
 * 就可以抛出错误,处理异常。
 */
 var parseBoolExpr = function(expression) {
    function start(char){//对于单独的表达式,必然以函数符号开始
        if(char==="&"||char==="|"||char==="!") return calcFn;
    }
    function calcFn(char){//函数符号后面接左括号
        if(char==="(") return bracketOpen;
    }
    function bracketOpen(char){//左括号后面是参数,刚才说过参数可能是表达式也可能是布尔值
        if(char==="&"||char==="|"||char==="!") return calcFn;
        if(char==="t"||char==="f") return boolParam;
    }
    function boolParam(char){//布尔值参数后面可能是下一个参数,也可能没有参数,遇到右括号
        if(char===",") return paramSplit;
        if(char===")") return bracketSeal;
    }
    function paramSplit(char){//参数分割符后,是下一个参数,可能是表达式也可能是布尔值
        if(char==="&"||char==="|"||char==="!") return calcFn;
        if(char==="t"||char==="f") return boolParam;
    }
    function bracketSeal(char){//右括号,可能接着下一个参数,可能是下一个右括号,可能是结束,这里结束手动加个$符
        if(char===",") return paramSplit;
        if(char===")") return bracketSeal;
        if(char==="$") return end;
    }
    function end(){//结束,目前没做处理
         
    }
    let state = start;
    for(let char of expression){
        state = state(char);
        console.log(state);
    }
    console.log(state("$"));//原字符串无结束符,手动结束
}

我们用一个稍微复杂的例子测试下:

console.log(parseBoolExpr("!(&(!(&(f)),&(t),|(f,f,t)))"));//false

得到如下的打印:
在这里插入图片描述
那么到这一步就完成了基础的token识别,可以看出能正确识别每个字符的类型。

语法树生成

下面我们来组装语法树,我们通过数组来保存树形结构的语法树,我们需要根据上面的定义的结构式来生成语法树,结构式是递归的形式,我们可以用栈来模拟这一过程,同样是上面的例子。
在这里插入图片描述
代码增加了token收集和统一处理,结构也简单清晰:

var parseBoolExpr = function(expression) {
    let stack = [];
    function emit(flow){//收集处理token流
        //onsole.log(flow);
        if(flow.type==="fnexp") stack.push({type:"expression",symbol:flow.token,param:[]});//遇到函数入栈
        if(flow.type==="boolean") stack[stack.length-1].param.push(flow);//遇到参数,给当前的函数加上
        if(flow.type==="bracketseal"){//遇到结束括号,说明函数结束,函数作为上一个节点的参数,要加入参数列表
            let done = stack.pop();
            if(stack.length===0){//最后弹出的是headNode,就是我们的根节点
                console.log(done);
            }else{
                stack[stack.length-1].param.push(done);
            }
        }
    }
    function start(char){
        if(char==="&"||char==="|"||char==="!"){
            emit({type:'fnexp',token:char});
            return calcFn;
        }
    }
    function calcFn(char){
        if(char==="("){
            emit({type:'bracketopen',token:char});
            return bracketOpen;
        }
    }
    function bracketOpen(char){
        if(char==="&"||char==="|"||char==="!"){
            emit({type:'fnexp',token:char});
            return calcFn;
        }
        if(char==="t"||char==="f"){
            emit({type:'boolean',token:char});
            return boolParam;
        }
    }
    function boolParam(char){
        if(char===","){
            emit({type:'split',token:char});
            return paramSplit;
        }
        if(char===")"){
            emit({type:'bracketseal',token:char});
            return bracketSeal;
        }
    }
    function paramSplit(char){
        if(char==="&"||char==="|"||char==="!"){
            emit({type:'fnexp',token:char});
            return calcFn;
        }
        if(char==="t"||char==="f"){
            emit({type:'boolean',token:char});
            return boolParam;
        }
    }
    function bracketSeal(char){
        if(char===","){
            emit({type:'split',token:char});
            return paramSplit;
        }
        if(char===")"){
            emit({type:'bracketseal',token:char});
            return bracketSeal;
        }
        if(char==="$") return end;
    }
    function end(){}
    let state = start;
    for(let char of expression){
        state = state(char);
    }
    state("$");
}

处理函数也很好懂,到这一步我们就拿到了俗称AST的语法树了,有了语法树就可以做很多事情,可以翻译成其他语言,可以执行计算等。

利用语法树解决该题

var parseBoolExpr = function(expression) {
    let stack = [],tree=null;
    function emit(flow){//收集处理token流
        //onsole.log(flow);
        if(flow.type==="fnexp") stack.push({type:"expression",symbol:flow.token,param:[]});//遇到函数入栈
        if(flow.type==="boolean") stack[stack.length-1].param.push(flow);//遇到参数,给当前的函数加上
        if(flow.type==="bracketseal"){//遇到结束括号,说明函数结束,函数作为上一个节点的参数,要加入参数列表
            let done = stack.pop();
            if(stack.length===0){//最后弹出的是headNode,就是我们的根节点
                tree = done;
            }else{
                stack[stack.length-1].param.push(done);
            }
        }
    }
    function start(char){
        if(char==="&"||char==="|"||char==="!"){
            emit({type:'fnexp',token:char});
            return calcFn;
        }
    }
    function calcFn(char){
        if(char==="("){
            emit({type:'bracketopen',token:char});
            return bracketOpen;
        }
    }
    function bracketOpen(char){
        if(char==="&"||char==="|"||char==="!"){
            emit({type:'fnexp',token:char});
            return calcFn;
        }
        if(char==="t"||char==="f"){
            emit({type:'boolean',token:char});
            return boolParam;
        }
    }
    function boolParam(char){
        if(char===","){
            emit({type:'split',token:char});
            return paramSplit;
        }
        if(char===")"){
            emit({type:'bracketseal',token:char});
            return bracketSeal;
        }
    }
    function paramSplit(char){
        if(char==="&"||char==="|"||char==="!"){
            emit({type:'fnexp',token:char});
            return calcFn;
        }
        if(char==="t"||char==="f"){
            emit({type:'boolean',token:char});
            return boolParam;
        }
    }
    function bracketSeal(char){
        if(char===","){
            emit({type:'split',token:char});
            return paramSplit;
        }
        if(char===")"){
            emit({type:'bracketseal',token:char});
            return bracketSeal;
        }
        if(char==="$") return end;
    }
    function end(){}
    let state = start;
    for(let char of expression){
        state = state(char);
    }
    state("$");
    if(tree){//处理语法树,得到计算结果
        //console.log(tree);
        function adpator(obj){
            if(obj.type==="boolean") return obj.token==="t";
            if(obj.type==="expression") return calcu(obj);
        }
        let fnMap = new Map([
            ["&",(...param)=>param.reduce((p,c)=>p&adpator(c),true)],
            ["|",(...param)=>param.reduce((p,c)=>p|adpator(c),false)],
            ["!",(param)=>!adpator(param)],
        ])
        function calcu(node){
            return fnMap.get(node.symbol)(...node.param);
        }
        return calcu(tree);
    }
}

最后也是成功AC,处理部分加在最后。如果对编译原理感兴趣可以系统学习下哈哈,感觉还是挺有用的。
在这里插入图片描述
此方法是学习自动机的一个简单的栗子,竞赛就不要用了,不然手速没那么快(手动狗头🤣)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值