DFA优化列表内数据组合情况(基于erlang)

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)].
      
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值