Search code examples
listprologprolog-difprolog-cutlogical-purity

Longest common prefix (LCP) of a list of strings


lcs([ H|L1],[ H|L2],[H|Lcs]) :-
    !,
    lcs(L1,L2,Lcs).
lcs([H1|L1],[H2|L2],Lcs):-
    lcs(    L1 ,[H2|L2],Lcs1),
    lcs([H1|L1],    L2 ,Lcs2),
    longest(Lcs1,Lcs2,Lcs),
    !.
lcs(_,_,[]).

longest(L1,L2,Longest) :-
    length(L1,Length1),
    length(L2,Length2),
    (  Length1 > Length2
    -> Longest = L1
    ;  Longest = L2
    ).

This is my code so far. How could I optimize it so that it prints the prefix, e.g.:

["interview", "interrupt", "integrate", "intermediate"]

should return "inte"

A bit rusty with Prolog, haven't done it in a while :)


Solution

  • First, let's start with something related, but much simpler.

    :- set_prolog_flag(double_quotes, chars).  % "abc" = [a,b,c]
    
    prefix_of(Prefix, List) :-
       append(Prefix, _, List).
    
    commonprefix(Prefix, Lists) :-
       maplist(prefix_of(Prefix), Lists).
    
    ?- commonprefix(Prefix, ["interview", "integrate", "intermediate"]).
       Prefix = []
    ;  Prefix = "i"
    ;  Prefix = "in"
    ;  Prefix = "int"
    ;  Prefix = "inte"
    ;  false.
    

    (See this answer, how printing character lists with double quotes is done.)

    This is the part that is fairly easy in Prolog. The only drawback is that it doesn't give us the maximum, but rather all possible solutions including the maximum. Note that all strings do not need to be known, like:

    ?- commonprefix(Prefix, ["interview", "integrate", Xs]).
       Prefix = []
    ;  Prefix = "i", Xs = [i|_A]
    ;  Prefix = "in", Xs = [i, n|_A]
    ;  Prefix = "int", Xs = [i, n, t|_A]
    ;  Prefix = "inte", Xs = [i, n, t, e|_A]
    ;  false.
    

    So we get as response a partial description of the last unknown word. Now imagine, later on we realize that Xs = "induce". No problem for Prolog:

    ?- commonprefix(Prefix, ["interview", "integrate", Xs]), Xs = "induce".
       Prefix = [], Xs = "induce"
    ;  Prefix = "i", Xs = "induce"
    ;  Prefix = "in", Xs = "induce"
    ;  false.
    

    In fact, it does not make a difference whether we state this in hindsight or just before the actual query:

    ?- Xs = "induce", commonprefix(Prefix, ["interview", "integrate", Xs]).
       Xs = "induce", Prefix = []
    ;  Xs = "induce", Prefix = "i"
    ;  Xs = "induce", Prefix = "in"
    ;  false.
    

    Can we now based on this formulate the maximum? Note that this effectively necessitates some form of extra quantor for which we do not have any direct provisions in Prolog. For this reason we have to limit us to certain cases we know will be safe. The easiest way out would be to insist that the list of words does not contain any variables. I will use iwhen/2 for this purpose.

    maxprefix(Prefix, Lists) :-
       iwhen(ground(Lists), maxprefix_g(Prefix, Lists)).
    
    maxprefix_g(Prefix, Lists_g) :-
       setof(N-IPrefix, ( commonprefix(IPrefix, Lists_g), length(IPrefix, N ) ), Ns),
       append(_,[N-Prefix], Ns).   % the longest one
    

    The downside of this approach is that we get instantiation errors should the list of words not be known.

    Note that we made quite some assumptions (which I hope really hold). In particular we assumed that there is exactly one maximum. In this case this holds, but in general it could be that there are several independent values for Prefix. Also, we assumed that IPrefix will always be ground. We could check that too, just to be sure. Alternatively:

    maxprefix_g(Prefix, Lists_g) :-
       setof(N, IPrefix^ ( commonprefix(IPrefix, Lists_g), length(IPrefix, N ) ), Ns),
       append(_,[N], Ns),
       length(Prefix, N),
       commonprefix(Prefix, Lists_g).
    

    Here, the prefix does not have to be one single prefix (which it is in our situation).

    The best, however, would be a purer version that does not need to resort to instantiation errors at all.