DFA处理激活条件判断
最近处理了一个功能,需要判断某个列表中数据是否能够满足配置的某些组合情况,类似于:
id | 组合 | 效果 |
---|---|---|
1 | [101,202,303] | 效果1 |
2 | [102,201,301] | 效果2 |
3 | [102,202,302] | 效果3 |
4 | [101,203,303] | 效果4 |
5 | [101,202, 303, 404] | 效果5 |
6 | [101,203, 302, 401] | 效果6 |
7 | [101,202,301,401,501] | 效果7 |
8 | [102,201,301,402,501] | 效果8 |
9 | [101,203,201,402,501] | 效果9 |
如果按照简单处理,用某个列表(例如[102,103,201,202,301,303])去遍历整个表的所有组合,那效率是十分低下的,这个时候需要用一些巧思去处理,于是就想到之前敏感词过滤用到的DFA算法
DFA算法
DFA(Deterministic Finite Automaton,确定有穷自动机)算法是通过一个预编译的一个Map来处理状态的转换。而状态机的处理在erlang中是非常方便的,并且这套算法中数据运算部分是很少的,多的只是状态的转换,所以效率上非常高。
在这个例子中,我们以id=1为例,我们需要一个状态转换为 101 -> 202 -> 303 -> final
的链式结构就可以确定我们id=1是满足条件的,并获取效果1的数值
那么我们只需要生成一个能够满足链式结构查找的Map就可以了。
Map生成
要使用DFA,我们需要先构建一个用于转换状态的Map或者一个状态机,这里简单构建一个map演示,也可以自行将配置存于状态机或者写成一个静态文件以便读取
atom替换:Table为表名,Table:keys()获取所有主键列表,Table:get(Key)获取配置数据,#record{}为表结构
-
v1(确定方法):
-
构建Map,首先我们先依次遍历所有配置数据
gen() -> Keys = Table:keys(), gen_1(Keys, #{}).
-
对于每个数据,我们需要提取组合的链式结构,并按照链式顺序加入到列表中,并为结尾的id写入结束标记,结构记为
#{101 => [202], 202 => [303], 303 => [{final, 1}]}
gen_1([], AccMap) -> AccMap; gen_1([Key | T], AccMap) -> % 为每个key生成结构 #record{group = GroupList} = Table:get(Key), gen_1(T, gen_2(GroupList, Key, AccMap)). gen_2([], _, AccMap) -> AccMap; gen_2([Id], Key, AccMap) -> NextList = maps:get(Id, AccMap, []), AccMap#{Id => [{final, Key} | NextList]}; % 写入结束标记 gen_2([Id, Next | T], Key, AccMap) -> NextList = maps:get(Id, AccMap, []), gen_2([Next | T], Key, AccMap#{Id => [Next | NextList]}). % 将下一个节点id保存在当前的结构里
-
-
v2(问题修复):
-
在写v1的时候发现特殊情况下出现了意料之外的结果,当组合为
[101,202,302]
时,意外得出了效果3的结果,正确情况下应该是无匹配效果 -
所以我们优化生成规则,加入一个前置判断Pre,需要在使用时判断前置是特定情况下才可以作为链式结构的下一项,此时结构变为
#{101 => [{0,202}], 202 => [{101,303}], 303 => [{final,202,1}]}
gen_1([], AccMap) -> AccMap; gen_1([Key | T], AccMap) -> % 为每个key生成结构 #record{group = GroupList} = Table:get(Key), gen_1(T, gen_2(GroupList, 0, Key, AccMap)). % 0作为开头项 gen_2([], _Pre, _, AccMap) -> AccMap; gen_2([Id], Pre, Key, AccMap) -> NextList = maps:get(Id, AccMap, []), AccMap#{Id => [{final, Pre, Key} | NextList]}; % 写入结束标记 gen_2([Id, Next | T], Pre, Key, AccMap) -> NextList = maps:get(Id, AccMap, []), gen_2([Next | T], Key, AccMap#{Id => [{Pre, Next} | NextList]}). % 将下一个节点id保存在当前的结构里
-
使用Map
map在调用gen()
生成之后可以存在ets或者进程字典中,不需要多次生成。
在传入一个列表检查满足的效果时,我们以[101,201,202,303,401,404]
为例:
-
v2:
-
第一步,我们需要取其中一个作为初始值,以列表首项101为开始
check([]) -> []; check(List) -> check(hd(List), 0, 1, List, gen()).
-
之后我们只需要按照链式节点的顺序依次查找即可,返回的数据结构为
[{final, 前置id,目标组合id,深度}]
:%% Id:当前节点id; Pre:前置节点id; Step:查找深度; %% List:输入的列表,判断节点是否满足; Map:生成的DFA表 check(Id, Pre, Step, List, Map) -> LinkList = maps:get(Id, Map), % 取出关联的下一个节点 {NextList, FinishList} = % 区分出需要继续的节点和已经结束的分支 lists:partition( fun(Acc) -> case Acc of {Pre, Next} -> % 还需要继续的格式 lists:member(Next, List); _ -> % 已经结束的格式 :: {final, xx, xx}或前置节点不同的{OtherPre, OtherNext} false end end, LinkList), %% 需要继续的节点,递归判断,并将当前节点作为下一步的前置节点 L = [check(NextId, Id, Step + 1, List, Map) || {_, NextId} <- NextList], %% 从继续的节点中找出匹配成功结束的节点(final开头的节点为正常结束,因为正常匹配最后必定匹配到一个final开头的结构) NextL = [Data || {final, _, _, _} = Data <- lists:flatten(L)], %% 从本轮结束的节点中找出结束的节点(同上,但是要检测一下前置节点是否是正确的) FinishL = [{final, Id, TId, Step} || {final, Check, TId} <- FinishList, Check =:= Pre], %% 最后返回两种结果的总和 NextL ++ FinishL.
-
-
v3(优化内容):
v2版本我们输出的是所有满足条件的情况,v3我们可以针对输出结果做一些优化,例如:
-
1.需要取出路径最长的结果:
我们可以针对判断返回的结果,比较Step的大小,例如从递归开始修改为:
%% 需要继续的节点,递归判断,并将当前节点作为下一步的前置节点 L = [check(NextId, Id, Step + 1, List, Map) || {_, NextId} <- NextList], %% 从继续的节点中找出匹配成功结束的节点 NextL = [Data || {final, _, _, _} = Data <- lists:flatten(L)], case NextL of [] -> % 没有更长的节点了,用当前长度的节点 %% 从本轮结束的节点中找出结束的节点(同上,但是要检测一下前置节点是否是正确的) case FinishL of [] -> % 当前也没有,往回找 []; [Data | _] -> % 直接随便取一个 [Data] end; Data -> % 继续的节点有返回更长的结果 Data end.
-
2.输入数据杂乱情况,首个id不一定满足满足时:
我们需要优化第一步取值的情况,并对列表内所有的值尝试找一遍:
check([]) -> []; check(List) -> GenMap = gen(), [check(Hd, 0, 1, List, GenMap) || Hd <- lists:usort(List)].
-