自动机介绍
第一次接触自动机是在大学的数电课上,门电路就是最简单的自动机,接收某个状态,输出下一个状态。我们常用的是米利状态机,下一个状态需要根据输入和当前的状态做转移。状态机除了硬件设计在编译方面也非常好用,我们使用状态机处理字符串来进行token识别,自动机本身是模拟程序的运行,可以形成分支判断,循环等基本结构,所以和程序没有本质区别,但是可以同过定义状态来管理繁多的判断条件,用来处理字符串还是很方便的。下面我就通过一道例题来讲一下状态机在解算法题方面的应用。
例:解析布尔表达式
这道题类似于前缀表达式求值,最好的办法是用栈,题目也挺容易理解,后面会拿出一章来理一下栈解决表达式求值。下面我们用自动机处理字符串,然后进行运算来解决,主要是为了讲解自动机原理。
首先我们对表达式的结构进行分析:
<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,处理部分加在最后。如果对编译原理感兴趣可以系统学习下哈哈,感觉还是挺有用的。
此方法是学习自动机的一个简单的栗子,竞赛就不要用了,不然手速没那么快(手动狗头🤣)