How to use dynamic databases in Prolog?

1.7k Views Asked by At

I have written the following program, which calculates the longest non-decreasing sub-sequence of input array.

The sub-program to find the longest list from the list of lists is taken from stackoverflow (How do I find the longest list in a list of lists) itself.

:- dynamic lns/2.
:- retractall(lns(_, _)).

lns([], []).
lns([X|_], [X]).
lns([X|Xs], [X, Y|Ls]) :-
    lns(Xs, [Y|Ls]),
    X < Y,
    asserta(lns([X|Xs], [X, Y|Ls])).
lns([_|Xs], [Y|Ls]) :-
    lns(Xs, [Y|Ls]).

% Find the longest list from the list of lists.
lengths([], []).
lengths([H|T], [LH|LengthsT]) :-
    length(H, LH),
    lengths(T, LengthsT).

lengthLongest(ListOfLists, Max) :-
    lengths(ListOfLists, Lengths),
    max_list(Lengths, Max).

longestList(ListOfLists, Longest) :-
    lengthLongest(ListOfLists, Len),
    member(Longest, ListOfLists),
    length(Longest, Len).

optimum_solution(List, Ans) :-
    setof(A, lns(List, A), P),
    longestList(P, Ans), 
    !.

I have used the Prolog dynamic database for memoization purpose. Though the program with database runs slower than the program without database. Below are the comparative times between two runs.

?- time(optimum_solution([0, 8, 4, 12, 2, 10, 6, 14, 1, 9], Ans)).
% 53,397 inferences, 0.088 CPU in 0.088 seconds (100% CPU, 609577 Lips)
Ans = [0, 2, 6, 9]. %% With database

?- time(optimum_solution([0, 8, 4, 12, 2, 10, 6, 14, 1, 9], Ans)).
% 4,097 inferences, 0.002 CPU in 0.002 seconds (100% CPU, 2322004 Lips)
Ans = [0, 2, 6, 9]. %% Without database. commented out the database usage.

I would like to know if I am using the dynamic database correctly. Thanks!

4

There are 4 best solutions below

2
On BEST ANSWER

The issue is that as you are traversing the list building subsequences, you need to consider only prior subsequences whose last value is less than the value you have in-hand. The problem is that Prolog's first-argument indexing is doing an equality check, not a less-than check. So Prolog will have to traverse the entire store of lns/2, unifying the first parameter with a value so you can check to see if it's less and then backtracking to get the next.

0
On

Keeps getting better! In this answer we present list_long_nondecreasing_subseq__NEW/2, a drop-in replacement of list_long_nondecreasing_subseq/2—presented in this earlier answer.

Let's cut to the chase and define list_long_nondecreasing_subseq__NEW/2!

:- use_module([library(clpfd), library(lists), library(random), library(between)]).

list_long_nondecreasing_subseq__NEW(Zs, Xs) :-
   minimum(Min, Zs),
   append(Prefix, Suffix, Zs),
   same_length(Suffix, Xs),
   zs_skipped_subseq_taken0(Zs, Prefix, Xs, Min).

zs_skipped_subseq_taken0([], _, [], _).
zs_skipped_subseq_taken0([E|Es], Ps, [E|Xs], E0) :-
   E0 #=< E,
   zs_skipped_subseq_taken0(Es, Ps, Xs, E).
zs_skipped_subseq_taken0([E|Es], [_|Ps], Xs, E0) :-
   zs_skipped_subseq_taken0_min0_max0(Es, Ps, Xs, E0, E, E).

zs_skipped_subseq_taken0_min0_max0([], _, [], E0, _, Max) :-
   Max #< E0.
zs_skipped_subseq_taken0_min0_max0([E|Es], Ps, [E|Xs], E0, Min, Max) :-
   E0 #=< E,
   E0 #> Min #\/ Min #> E,
   E0 #> Max #\/ Max #> E,
   zs_skipped_subseq_taken0(Es, Ps, Xs, E).
zs_skipped_subseq_taken0_min0_max0([E|Es], [_|Ps], Xs, E0, Min0, Max0) :-
   Min #= min(Min0,E),
   Max #= max(Max0,E),
   zs_skipped_subseq_taken0_min0_max0(Es, Ps, Xs, E0, Min, Max).

So ... does it still work as before? Let's run some tests and compare the answer sequences:

| ?- setrand(random(29251,13760,3736,425005073)),
     between(7, 23, N), 
     nl,
     write(n=N),
     write(' '),
     length(Zs, N),
     between(1, 10, _),
     maplist(random(1,N), Zs),
     findall(Xs1, list_long_nondecreasing_subseq(     Zs,Xs1), Xss1),
     findall(Xs2, list_long_nondecreasing_subseq__NEW(Zs,Xs2), Xss2),
     ( Xss1 == Xss2 -> true ; throw(up) ),
     length(Xss2,L),
     write({L}),
     false.

n=7 {3}{8}{3}{7}{2}{5}{4}{4}{8}{4}
n=8 {9}{9}{9}{8}{4}{4}{7}{5}{6}{9}
n=9 {9}{8}{5}{7}{10}{7}{9}{4}{5}{4}
n=10 {7}{12}{7}{14}{13}{19}{13}{17}{10}{7}
n=11 {14}{17}{7}{9}{17}{21}{14}{10}{10}{21}
n=12 {25}{18}{20}{10}{32}{35}{7}{30}{15}{11}
n=13 {37}{19}{18}{22}{20}{14}{10}{11}{8}{14}
n=14 {27}{9}{18}{10}{20}{29}{69}{28}{10}{33}
n=15 {17}{24}{13}{26}{32}{14}{22}{28}{32}{41}
n=16 {41}{55}{35}{73}{44}{22}{46}{47}{26}{23}
n=17 {54}{43}{38}{110}{50}{33}{48}{64}{33}{56}
n=18 {172}{29}{79}{36}{32}{99}{55}{48}{83}{37}
n=19 {225}{83}{119}{61}{27}{67}{48}{65}{90}{96}
n=20 {58}{121}{206}{169}{111}{66}{233}{57}{110}{146}
n=21 {44}{108}{89}{99}{149}{148}{92}{76}{53}{47}
n=22 {107}{137}{221}{79}{172}{156}{184}{78}{162}{112}
n=23 {163}{62}{76}{192}{133}{372}{101}{290}{84}{378}
no

All answer sequences were exactly identical! ... So, how about runtimes?

Let's run some more queries using SICStus Prolog 4.3.2 and pretty-print the answers!

?- member(N, [15,20,25,30,35,40,45,50]),
   length(Zs, N),
   _NN #= N*N,
   maplist(random(1,_NN), Zs),
   call_time(once(list_long_nondecreasing_subseq(     Zs, Xs )), T1),
   call_time(once(list_long_nondecreasing_subseq__NEW(Zs,_Xs2)), T2),
   Xs == _Xs2,
   length(Xs,L).
N = 15, L =  4, T1 =    20, T2 =    0, Zs = [224,150,161,104,134,43,9,111,76,125,50,68,202,178,148], Xs = [104,111,125,202] ;
N = 20, L =  6, T1 =    60, T2 =   10, Zs = [71,203,332,366,350,19,241,88,370,100,288,199,235,343,181,90,63,149,215,285], Xs = [71,88,100,199,235,343] ;
N = 25, L =  7, T1 =   210, T2 =   20, Zs = [62,411,250,222,141,292,276,94,548,322,13,317,68,488,137,33,80,167,101,475,475,429,217,25,477], Xs = [62,250,292,322,475,475,477] ;
N = 30, L = 10, T1 =   870, T2 =   30, Zs = [67,175,818,741,669,312,99,23,478,696,63,793,280,364,677,254,530,216,291,660,218,664,476,556,678,626,75,834,578,850], Xs = [67,175,312,478,530,660,664,678,834,850] ;
N = 35, L =  7, T1 =   960, T2 =  120, Zs = [675,763,1141,1070,299,650,1061,1184,512,905,139,719,844,8,1186,1006,400,690,29,791,308,1180,819,331,482,982,81,574,1220,431,416,357,1139,636,591], Xs = [299,650,719,844,1006,1180,1220] ;
N = 40, L =  9, T1 =  5400, T2 =  470, Zs = [958,1047,132,1381,22,991,701,1548,470,1281,358,32,605,1270,692,1020,350,794,1451,11,985,1196,504,1367,618,1064,961,463,736,907,1103,719,1385,1026,935,489,1053,380,637,51], Xs = [132,470,605,692,794,985,1196,1367,1385] ;
N = 45, L = 10, T1 = 16570, T2 = 1580, Zs = [1452,171,442,1751,160,1046,470,450,1245,971,1574,901,1613,1214,1849,1805,341,34,1923,698,156,1696,717,1708,1814,1548,463,421,1584,190,1195,1563,1772,1639,712,693,1848,1531,250,783,1654,1732,1333,717,1322], Xs = [171,442,1046,1245,1574,1613,1696,1708,1814,1848] ;
N = 50, L = 11, T1 = 17800, T2 = 1360, Zs = [2478,2011,2411,1127,1719,1286,1081,2042,1166,86,355,894,190,7,1973,1912,753,1411,1082,70,2142,417,1609,1649,2329,2477,1324,37,1781,1897,2415,1018,183,2422,1619,1446,1461,271,56,2399,1681,267,977,826,2145,2318,2391,137,55,1995], Xs = [86,355,894,1411,1609,1649,1781,1897,2145,2318,2391] ;
false.

Of course, the baroque approach shown in this answer simply cannot compete with "serious" suitable algorithms for determining the —still, getting 10X speedup always feels good:)

4
On

TL;DR: In this answer we implement a very general approach based on .

:- use_module(library(clpfd)).

list_nondecreasing_subseq(Zs, Xs) :-
   append(_, Suffix, Zs),
   same_length(Suffix, Xs),
   chain(Xs, #=<),
   list_subseq(Zs, Xs).                % a.k.a. subset/2 by @gusbro

Sample query using SWI-Prolog 7.3.16:

?- list_nondecreasing_subseq([0,8,4,12,2,10,6,14,1,9], Zs).
   Zs = [0,8,12,14]
;  Zs = [0,8,10,14]
;  Zs = [0,4,12,14]
;  Zs = [0,4,10,14]
;  Zs = [0,4,6,14]
;  Zs = [0,4,6,9]
;  Zs = [0,2,10,14]
;  Zs = [0,2,6,14]
;  Zs = [0,2,6,9]
;  Zs = [0,8,12]
...
;  Zs = [9]
;  Zs = []
;  false.

Note the particular order of the answer sequence! The longest lists come first, followed by the lists a little smaller ... all the way down to singleton lists, and the empty list.

0
On

Earlier, we presented a concise solution based on . Now we aim at generality and efficiency!

:- use_module([library(clpfd), library(lists)]).

list_long_nondecreasing_subseq(Zs, Xs) :-
   minimum(Min, Zs),
   append(_, Suffix, Zs),
   same_length(Suffix, Xs),
   zs_subseq_taken0(Zs, Xs, Min).

zs_subseq_taken0([], [], _).
zs_subseq_taken0([E|Es], [E|Xs], E0) :-
   E0 #=< E,
   zs_subseq_taken0(Es, Xs, E).
zs_subseq_taken0([E|Es], Xs, E0) :-
   zs_subseq_taken0_min0_max0(Es, Xs, E0, E, E).

zs_subseq_taken0_min0_max0([], [], E0, _, Max) :-
   Max #< E0.
zs_subseq_taken0_min0_max0([E|Es], [E|Xs], E0, Min, Max) :-
   E0 #=< E,
   E0 #> Min #\/ Min #> E,
   E0 #> Max #\/ Max #> E,
   zs_subseq_taken0(Es, Xs, E).
zs_subseq_taken0_min0_max0([E|Es], Xs, E0, Min0, Max0) :-
   Min #= min(Min0,E),
   Max #= max(Max0,E),
   zs_subseq_taken0_min0_max0(Es, Xs, E0, Min, Max).

Sample query using SICStus Prolog 4.3.2 (with pretty-printed answer sequence):

?- list_long_nondecreasing_subseq([0,8,4,12,2,10,6,14,1,9], Xs).
   Xs = [0,8,12,14]
;  Xs = [0,8,10,14]
;  Xs = [0,4,12,14]
;  Xs = [0,4,10,14]
;  Xs = [0,4, 6,14]
;  Xs = [0,4, 6, 9]
;  Xs = [0,2,10,14]
;  Xs = [0,2, 6,14]
;  Xs = [0,2, 6, 9]
;  Xs = [0,8,9]
;  Xs = [0,4,9]
;  Xs = [0,2,9]
;  Xs = [0,1,9]
;  false.

Note that the answer sequence of list_long_nondecreasing_subseq/2 may be a lot smaller than the one given by list_nondecreasing_subseq/2.

Above list [0,8,4,12,2,10,6,14,1,9] has 9 non-descending subsequences of length 4—all are "returned" by both list_nondecreasing_subseq/2 and list_long_nondecreasing_subseq/2.

The respective answer sequence sizes, however, differ considerably: (65+9=74) vs (4+9=13).