简体   繁体   中英

Prolog - How to sort a list of lists containing numbers by the sum of the numbers in each list

Assume I have a list that looks like this one:

S1 = [ [3, 6], [1, 3], [4, 12], [10] ]

And I want the sorted list to be:

S = [ [1, 3], [3, 6], [10], [4, 12] ] ( 4 < 9 < 10 < 16 )

Update: I think I come up with a solution using predsort/3 and sumlist/2.

mycompare(Comp, [A|C], [B|D]):-
    sumlist(A, X), sumlist(B, Y), 
    (   X < Y -> Comp = '<'
    ;   X > Y -> Comp = '>'
    ;   compare(Comp, [A|C], [B|D])).    

predsort(mycompare, S1, S).

Why use the slow and non-portable predsort/3 ? Use keysort/2 instead—it's win-win!

Every ISO-compliant Prolog system provides keysort/2 —unlike predsort/3 or msort/2 ...

Using SICStus Prolog 4.3.2 we define zss_sumsorted/2 like so:

:- use_module(library(lists), [maplist/3,sumlist/2,keys_and_values/3]).
:- use_module(library(types), [must_be/4]).

zss_sumsorted(L0, L) :-
   must_be(L0, list(list(integer)), zss_sumsorted(L0,L), 1),
   maplist(sumlist, L0, L1),
   keys_and_values(L10, L1, L0),
   keysort(L10, L10_sorted),
   keys_and_values(L10_sorted, _, L).

Sample query as given by the OP:

| ?- zss_sumsorted([[3,6],[1,3], [4,12],[10]], Xss).
Xss = [[1,3],[3,6],[10],[4,12]] ? ;
no

Edit: @tas suggested some more test cases with non-ground instantiation, which should raise the right exceptions—with a little help by must_be/4 .

| ?- zss_sumsorted([[3,6],[1,3],[4,12],[10],[_]], Xss).
! Instantiation error in argument 1 of user:zss_sumsorted/2
! goal:  zss_sumsorted([[3,6],[1,3],[4,12],[10],[_229]],_179)

| ?- zss_sumsorted([[3,6],[1,3],[4,12],[10],x], Xss).
! Type error in argument 1 of user:zss_sumsorted/2
! expected list of an integer, but found x
! goal:  zss_sumsorted([[3,6],[1,3],[4,12],[10],x],_237)

Yet another version, based on the same idea as suggested by @lurker, but without the elegance of maplist/3:

:- use_module(library(clpfd)).
:- use_module(library(pairs)).

lists_sorted(L,S) :-
    lists_withsums(L,SL),
    keysort(SL,SSL),             % instead of msort/2
    pairs_values(SSL,S).

lists_withsums([],[]).
lists_withsums([List|Ls], [Sum-List|SLs]) :-
    sum(List,#=,Sum),
    lists_withsums(Ls,SLs).

The main difference is the use of keysort/2 instead of msort/2 as the latter also incorporates the actual list in the sorting whereas keysort/2 retains the order in which the lists with equal sums occur. Example query with msort/2:

   ?- lists_sorted([[3,6],[3,1],[1,3],[4,12],[10],[2,2],[1,3]], L).
L = [[1,3],[1,3],[2,2],[3,1],[3,6],[10],[4,12]]

Same query with keysort/2:

   ?- lists_sorted([[3,6],[3,1],[1,3],[4,12],[10],[2,2],[1,3]], L).
L = [[3,1],[1,3],[2,2],[1,3],[3,6],[10],[4,12]]

Several problems with the original code. (1) A and B are actually the head of sublists of your original list, so you really wanted sumlist of [A|C] and [B|D] , not of A and B . Your disjunction ( ; ) logic will mean that compare/3 is only called if X = Y is true. (3) The comparator is being used to compare the lists themselves, which will fail.

mycompare(Comp, [A|C], [B|D]):-
    sumlist(A, X), sumlist(B, Y),      % ERROR: A and B are the heads of the list *element*
                                       % you really want sumlist([A|C], X), etc...
    (   X < Y -> Comp = '<'            % Choosing comparator based upon sum
    ;   X > Y -> Comp = '>'            % ERROR: Your disjunction (;) will ONLY do the
                                       % following compare if `X = Y`
    ;   compare(Comp, [A|C], [B|D])).  % ERROR: comparing two lists with comparator


As another alternative, you can approach the problem this way as hinted in my comments:

  1. Map the list to a list of terms SL where S is the sum of L
  2. Use msort to sort SL terms which will occur via the "natural" order in Prolog and put smallest S first
  3. Map the resulting sorted list of SL back to a list of just L

The implementation looks like this:

 sort_by_sum(InputList, SortedList) :- maplist(pre_sum, InputList, SumWithList), msort(SumWithList, SumWithListSorted), maplist(un_sum, SumWithListSorted, SortedList). pre_sum(L, SL) :- sumlist(L, S). un_sum(_-L, L). ?- sort_by_sum([ [3, 6], [1, 3], [4, 12], [10] ], L). L = [[1, 3], [3, 6], [10], [4, 12]]. 

This works because Prolog will use term comparisons (see @< , @> , etc in the documentation) in order to do the sort. A term such as AB is less than XY if A is less than Y . So comparing 9-[3,6] with 10-[10] will find that 9-[3,6] is less. And comparing 16-[4,12] with 10-[10] will find that 16-[4,12] is greater.


As cited in a couple of other answers, keysort/2 would be more canonical and portable here in place of msort/2 .

your solution seems wrong: could be

mycompare(C, A, B) :-
    sumlist(A, Sa), sumlist(B, Sb),
    ( Sa < Sb -> C = < ; C = > ).
lol_sorted(L, S) :-
    predsort(mycompare, L, S).

or, alternatively:

lol_sorted(L, S) :-
    setof(N-E, (member(E,L),sumlist(E,N)), R),
    pairs_values(R, S).

this will loose duplicated, identical lists (not sums). To preserve them, a slightly more complex snippet:

lol_sorted(L, S) :-
    setof(N/P-E, (nth0(P,L,E),sumlist(E,N)), R),
    pairs_values(R, S).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM