DFA的最小化 也称为 确定的有穷状态机的化简。
DFA的最小化 = 消除无用状态 + 合并等价状态
-
消除无用状态这里是指删掉那些达到不了的状态。这不是我们的重点,DFS+HashSet不难实现。
-
其实关键在于合并等价状态。那么,怎样的两个状态是等价的呢?
状态的等价需要满足两个条件:
- 一致性条件:它们都是可接受状态或不可接受状态(即都是终态或非终态)
- 蔓延性条件:我们用所有的输入符号进行转化,它们都可以转化到等价的状态里
DFA最小化核心算法:分割法
通俗的说:
- 先根据终态/非终态分为两组
- 然后对同一组中的状态,用所有输入符号去试着转化,如果转化不到一组,则再分
- 不断进行上述操作,直到每一组各自的状态集通过任何一个字母都不会转移到其他组
首先定义DFA类
// DFA类(属性与命名与五元式完全一致)(不熟悉五元式就去查课本啊喂)
public class DFA {
private List<Integer> K; // 状态集
private char[] letters; // 字母表
private String[][] f; // 转换函数
private int S; // 唯一初态
private List<Integer> Z; // 终态集
// setter和getter这里省略
}
下面是分割法的核心逻辑 ⬇️
但其实分割法的实现称不上简单,特别是“分组”这一动作,由人来做很容易很直观,但计算机实现起来会遇到困难——需要借助一个Group类。
public class Group {
public int groupID; // 该组的唯一ID
public Set<Integer> stateSet; // 该组所包含的状态集
public Group(int groupID, Set<Integer> stateSet) {
this.groupID = groupID;
this.stateSet = stateSet;
}
}
/**
*【DFA的最小化】
* 核心思路:
* 1. 定义一个Group类,作为「分组」。
* Group有两个属性:groupID作为唯一标识;StateSet为该分组包含的状态集
* 2. separate()方法的作用是,根据某个字母(letter)对分组集合(groupSet)进行彻底分裂
* 3. 对于字母表中的每个字母,进行separate分裂
*
* 下面的论述有利于理解这个算法:
* 1. HashMap做映射,是该算法的一个关键点。
* 对于某个字母,一个分组(group)的所有状态(state)根据这个字母,用HashMap记录它们分别会被映射到哪个分组里,据此分裂。
* 举个例子:{group1,[0, 1]}, {group2,[2, 3]} (key是组,value是转化后指向该组的所有状态)
* ⬆ 0,1状态会转化到1组,2,3状态会转化到2组,据此,旧组分裂为了两个新组,然后删掉旧组,新组入队BFS
* 如果这个哈希表的size==1,说明所有状态只能转化到一个组中,那么它们是等价的,不用删掉旧组,该组直接进入finalGroupSet最终分组
* 2. groupID的作用是什么?为什么还要专门维护它?
* 唯一标识。从1中看出,过程中不断进行着"删掉旧组,生成新组"的行为。维护这个ID主要是为了HashMap做映射
*
*/
public class minDFA {
private int cnt = 0; // 维护Group的唯一ID
public void minDFA(DFA dfa){
List<Integer> K = dfa.getK();
List<Integer> Z = dfa.getZ();
String[][] f = dfa.getF();
char[] letters = dfa.getLetters();
K.removeAll(Z); // 全部状态集K - 终态集Z = 非终态集
Group groupx = new Group(cnt++, new HashSet<>(K));
Group groupy = new Group(cnt++, new HashSet<>(Z));
Set<Group> finalGroupSet = new HashSet<>(); // 最终分组
Set<Group> curGroupSet = new HashSet<>(); // 此时的分组
finalGroupSet.add(groupx);
finalGroupSet.add(groupy);
for(char letter : letters){ // 对于每个字母
curGroupSet = finalGroupSet; // 【最终分组】不断沦为【此时分组】
finalGroupSet = separate(curGroupSet, letter, f); // 【此时分组】又分裂成新的【最终分组】
} // 所有字母都用了一次后,成为名副其实的【最终分组】
// 打印最终分组(每个组中的状态等价)
for(Group group : finalGroupSet){
System.out.print(group.groupID);
System.out.print(group.stateSet);
System.out.println();
}
}
private Set<Group> separate(Set<Group> groupSet, char letter, String[][] f){
Set<Group> finalGroupSet = new HashSet<>();
Set<Group> curGroupSet = groupSet;
Queue<Group> queue = new LinkedList<>();
for(Group group : groupSet){
queue.add(group);
}
while (!queue.isEmpty()){
Group oldGroup = queue.poll();
Map<Group, List<Integer>> map = new HashMap<>(); //根据指向的组,对状态Integer进行分类
for(Integer state : oldGroup.stateSet){
Group stateNextBelong = beLong(state, letter, f, curGroupSet);
if(!map.containsKey(stateNextBelong)){
map.put(stateNextBelong, new ArrayList<>());
}
map.get(stateNextBelong).add(state);
}
if (map.size() == 1){ // 如果这些状态映射到了一个状态集(Group)中,则为最终分组
finalGroupSet.add(oldGroup);
}else{ // 如果这些状态映射到了多个状态集(Group)中,则删除原先分组,创建多个新分组,并将新分组入队
curGroupSet.remove(oldGroup);
for(List<Integer> list : map.values()){
Group newGroup = new Group(cnt++, new HashSet<>(list));
curGroupSet.add(newGroup);
queue.add(newGroup);
}
}
}
return finalGroupSet;
}
/**
* move方法: 返回唯一后继状态(-1表示没有后继状态)
*/
private int move(int state, char letter, String[][] f){
for(int nextState = 0; nextState < f.length; nextState++){
for(char c : f[state][nextState].toCharArray()){
if(c == letter){
return nextState;
}
}
}
return -1;
}
/**
* beLong方法: 某状态(state)经过字母(letter)一次转化(move)后,所属于的当前分组(group)
*/
private Group beLong(int state, char letter, String[][] f, Set<Group> groupSet){
int newState = move(state, letter, f);
for(Group group : groupSet){
if(group.stateSet.contains(newState)){
return group;
}
}
return null;
}
}