Prolog 使用`库(聚合)计算两个序列中匹配元素的复杂性`

Prolog 使用`库(聚合)计算两个序列中匹配元素的复杂性`,prolog,time-complexity,Prolog,Time Complexity,我们想计算两个(可能很长)字符串之间的对应关系,这两个字符串恰好代表DNA序列。序列是字符列表,其中字符取自a,c,t,g,'''',带有''''a“不知道”占位符,该占位符从未与任何内容(甚至其本身)对应。在这种情况下,我们采用(感谢Capelical的创意): 匹配(序号1、序号2、计数):- 合计所有(计数, ( nth1(位置,序号1,X), nth1(位置,序号2,X), 成员chk(X[a,c,g,t]) ), N) 。 这种方法可以与一种“直接”的方法相比较,在这种方法中,我们可

我们想计算两个(可能很长)字符串之间的对应关系,这两个字符串恰好代表DNA序列。序列是字符列表,其中字符取自
a,c,t,g,''''
,带有''''a“不知道”占位符,该占位符从未与任何内容(甚至其本身)对应。在这种情况下,我们采用(感谢Capelical的创意):

匹配(序号1、序号2、计数):-
合计所有(计数,
(
nth1(位置,序号1,X),
nth1(位置,序号2,X),
成员chk(X[a,c,g,t])
),
N) 。
这种方法可以与一种“直接”的方法相比较,在这种方法中,我们可以建立一个(尾部递归)递归,该递归只需依次遍历两个序列,并成对地比较元素,同时计数

由于序列可能非常大,算法复杂度变得令人感兴趣

如果n=长度(序列),且两个序列的长度相同,则可以预期:

  • 直截了当的方法:复杂性是O(n)
  • 聚合方法:复杂性为O(n²)
上述算法的(时间和空间)复杂度是多少?为什么

测试代码 为了补充上述内容,基于SWI Prolog的测试代码块:

:-开始测试(atcg)。
换行匹配(字符串1、字符串2、计数):-
原子字符(字符串1,序号1),
原子字符(String2,Seq2),
拟合(序号1,序号1,0,计数)。
测试(“字符串1为空”,非设置):-
包裹匹配(“atcg”、“计数”),
断言(计数=0)。
测试(“字符串2为空”):-
包裹匹配(“,”atcg“,计数),
断言(计数=0)。
测试(“两个字符串均为空”):-
换行匹配(“,”,计数),
断言(计数=0)。
测试(“两个字符串都匹配,仅1个字符”):-
包裹匹配(“a”、“a”、计数),
断言(计数=1)。
测试(“两个字符串都匹配”):-
包裹匹配(“atcgatcgatcg”,“atcgatcgatcg”,计数),
断言(MatchCount==12)。
测试(“两个字符串都与下划线匹配”):-
换行符(“\u TC\u ATCG\u TCG”、“\u TC\u ATCG\u TCG”,计数),
断言(MatchCount==9)。
测试(“各种不匹配1”):-
包裹匹配(“atcgatcgatcg”,“atcgatcgatcg”,计数),
断言(MatchCount==8)。
测试(“带下划线的各种不匹配”):-
换行比赛(“在加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加加,
断言(计数=8)。
:-结束测试(atcg)。
因此:

?-运行\u测试。
%损益单位:atcg。。。。。。。。完成
%8项测试全部通过
对。
经验信息 在使用下面的代码进行一些手动数据采集(需要自动化)之后,该代码将经过的时间和推断的数量输出到控制台:

给我随机序列(长度,顺序):-
长度(序号,长度),
地图列表(
[E] >>(在(0,3,Ix)和nth0(Ix[a,t,c,g],E]之间的随机_),
顺序)。
多快(长度):-
给我随机序列(长度,序号1),
给我随机序列(长度,序号2),
时间(匹配(Seq1,Seq2,))。
。。。还有LibreOffice Calc中的一些图形错误(我的ggplot技能已经过时),我们有经验数据表明,该算法的成本非常低

O((长度(顺序))²)。

计数、推断、秒、毫秒、兆推断
1000,171179,0.039,39,0.171179
2000,675661,0.097,97,0.675661
3000,1513436,0.186,186,1.513436
4000,2684639,0.327,327,2.684639
5000,4189172,0.502,502,4.189172
6000,6027056,0.722,722,6.027056
7000,8198103,1.002,1002,8.198103
8000,10702603,1.304,1304,10.702603
9000,13540531,1.677,1677,13.540531
10000,16711607,2.062,2062,16.711607
11000,20216119,2.449,2449,20.216119
20000,66756619,8.091,8091,66.756619
30000,150134731,17.907,17907,150.134731
40000,266846773,32.012,32012,266.846773
50000,416891749,52.942,52942,416.891749
60000,600269907,74.103,74103,600.269907

我认为观察到复杂性O(n²)不是由于聚合方法本身,而是因为子目标
nth1(Pos,Seq1,X),nth1(Pos,Seq2,X)
表现为“嵌套循环”(序列大小为n)

因此,只要消除了“嵌套循环”,就应该可以创建另一个算法,即使使用聚合,其复杂性也可以是O(n)

要比较的算法

% Original algorithm: Complexity O(n²)

match1(Seq1, Seq2, Count) :-
   aggregate_all(count,
      (  nth1(Pos, Seq1, X),
         nth1(Pos, Seq2, X),
         memberchk(X, [a,c,g,t]) ),
      Count).

% Proposed algorithm: Complexity O(n)

match2(Seq1, Seq2, Count) :-
   (   maplist([X,Y,X-Y]>>true, Seq1, Seq2, Seq3)
   ->  aggregate_all(count, (member(X-X, Seq3), X\='_'), Count)
   ;   Count = 0 ).

gimme_random_sequence(Length, Seq) :-
   length(Seq, Length),
   maplist([E]>>(random_between(0,3,Ix), nth0(Ix, [a,t,c,g], E)), Seq).

test(N) :-
   gimme_random_sequence(N, S1),
   gimme_random_sequence(N, S2),
   time(match1(S1, S2, Count)),
   time(match2(S1, S2, Count)).
简单的实证结果

?- test(10000).
% 16,714,057 inferences, 1.156 CPU in 1.156 seconds (100% CPU, 14455401 Lips)
% 39,858 inferences, 0.000 CPU in 0.000 seconds (?% CPU, Infinite Lips)
true.

?- test(20000).
% 66,761,535 inferences, 4.594 CPU in 4.593 seconds (100% CPU, 14533123 Lips)
% 79,826 inferences, 0.016 CPU in 0.016 seconds (100% CPU, 5108864 Lips)
true.

?- test(40000).
% 266,856,213 inferences, 19.734 CPU in 19.841 seconds (99% CPU, 13522405 Lips)
% 159,398 inferences, 0.016 CPU in 0.015 seconds (104% CPU, 10201472 Lips)
true.

?- test(80000).
% 1,067,046,835 inferences, 77.203 CPU in 77.493 seconds (100% CPU, 13821291 Lips)
% 320,226 inferences, 0.047 CPU in 0.047 seconds (100% CPU, 6831488 Lips)
true.
编辑2021年4月30日是否
nth1(I,S,X),nth1(I,S,X)
真的像嵌套循环一样工作?

<> p>为了看到这个问题的答案是肯定的,考虑下面的简单实现:<代码> NTH/3 ,使用全局标志:< /P>计算每个解决方案所需的回合数。
nth(Index, List, Item) :-
   (   var(Index)
   ->  nth_nondet(1, Index, List, Item)
   ;   integer(Index)
   ->  nth_det(Index, List, Item)
   ).

nth_det(1, [Item|_], Item) :- !.
nth_det(Index, [_|Rest], Item) :-
   flag(rounds, Rounds, Rounds+1),
   Index1 is Index - 1,
   nth_det(Index1, Rest, Item).

nth_nondet(Index, Index, [Item|_], Item).
nth_nondet(Acc, Index, [_|Rest], Item) :-
   flag(rounds, Rounds, Rounds+1),
   Acc1 is Acc + 1,
   nth_nondet(Acc1, Index, Rest, Item).
要获得轮数,您可以询问:

?- flag(rounds,_,0), nth(5,[a,b,c,d,e],X), flag(rounds,Rounds,Rounds).
X = e,
Rounds = 4.
现在,使用这个谓词,我们可以创建一个谓词来计算不同长度列表的目标
nth(I,L,X),nth(I,L,X)
的轮数:

count_rounds :-
    forall(between(1, 10, N),
           ( Length is 10*N,
             count_rounds(Length, Rounds),
             writeln(rounds(Length) = Rounds)
           )).

count_rounds(Length, _) :-
    numlist(1, Length, List),
    flag(rounds, _, 0),
    nth(Index, List, Item),
    nth(Index, List, Item),
    fail.
count_rounds(_, Rounds) :-
    flag(rounds, Rounds, Rounds).
实证结果:

?- count_rounds.
rounds(10) = 55
rounds(20) = 210
rounds(30) = 465
rounds(40) = 820
rounds(50) = 1275
rounds(60) = 1830
rounds(70) = 2485
rounds(80) = 3240
rounds(90) = 4095
rounds(100) = 5050
如我们所见,目标
nth(I,L,X),nth(I,L,X)
计算n阶方阵的一半(包括其对角线)。因此,长度n的列表的轮数为轮数(n)=(n²+n)/2。因此,该目标的时间复杂度为O(n²)


备注库谓词
nth1/3
的实现比本实验考虑的谓词
nth/3
的实现效率略高。然而,goal
nth1(I,S,X),nth1(I,S,X)
的时间复杂度仍然是O(n²)。

永远不要在Prolog中使用避免回溯的函数式编程习惯用法,如
maplist/4
。这里的
pair_member/4
match3/3
应该快一点

match2(Seq1, Seq2, Count) :-
   (   maplist([X,Y,X-Y]>>true, Seq1, Seq2, Seq3)
   ->  aggregate_all(count, (member(X-X, Seq3), X\='_'), Count)
   ;   Count = 0 ).

pair_member(X, Y, [X|_], [Y|_]).
pair_member(X, Y, [_|L], [_|R]) :-  
    pair_member(X, Y, L, R).

match3(Seq1, Seq2, Count) :-
   aggregate_all(count,
         (pair_member(X, X, Seq1, Seq2), X \= '_'), Count).

gimme_random_sequence(Length, Seq) :-
   length(Seq, Length),
   maplist([E]>>(random_between(0,3,Ix), nth0(Ix, [a,t,c,g], E)), Seq).

test(N) :-
   gimme_random_sequence(N, S1),
   gimme_random_sequence(N, S2),
   time(match2(S1, S2, Count)),
   time(match3(S1, S2, Count)).
哇!速度快了10倍!多亏了SWI Prolog的天才,它是如何实现的
编译
配对成员/4中的尾部递归:

/* SWI-Prolog 8.3.21, MacBook Air 2019 */
?- set_prolog_flag(double_quotes, chars).
true.

?- X = "abc".
X = [a, b, c].

?- match2("_TC_ATCG_TCG","_TC_ATCG_TCG",Count).
Count = 9.

?- match3("_TC_ATCG_TCG","_TC_ATCG_TCG",Count).
Count = 9.

?- test(100000).
% 1,575,520 inferences, 0.186 CPU in 0.190 seconds (98% CPU, 8465031 Lips)
% 175,519 inferences, 0.018 CPU in 0.019 seconds (98% CPU, 9577595 Lips)
true.
编辑2021年4月29日:
讽刺的是,分叉回溯仍然具有挑战性。
在修复误用
库(应用宏)
后,我得到:

?- test(100000).
% 374,146 inferences, 0.019 CPU in 0.019 seconds (99% CPU, 19379778 Lips)
% 174,145 inferences, 0.014 CPU in 0.014 seconds (99% CPU, 12400840 Lips)
true.
本地成员/2是否对g有贡献
% 1,751,758 inferences, 0.835 CPU in 0.835 seconds (100% CPU, 2098841 Lips)
% 1,751,757 inferences, 0.637 CPU in 0.637 seconds (100% CPU, 2751198 Lips)