Grouping list elements based on values in list

Viewed 203

i have a list of elements(tuples):

tuples(
        [
            (a,b,[1,3,5,7]),
            (a,b,[9,11,13,15]),
            (a,b,[17,19,21,23]),
            (c,d,[0,2,4,6]),
            (c,d,[8,10,12,14]),
            (c,d,[16,18,20,22]),
            (e,f,[100,200,300,400]),
            (e,f,[500,600,700,800]),
            (e,f,[900,1000,1100,1200])
        ]
    ).

How to group them so it becomes:

[
        (a,b,[1,3,5,7,9,11,13,15,17,19,21,23]),
        (c,d,[0,2,4,6,8,10,12,14,16,18,20,22]),
        (e,f,[100,200,300,400,500,600,700,800,900,1000,1100,1200])
]

As we can see, we grouped the (a,b) (c,d) (e,f) and concatenated their respective lists.

thank you for the help.

4 Answers

How about this somewhat SQL-ish way:

Define a predicate to access data in the original deep and nasty datastructure (which uses conjunction (_,_) to create what looks like n-tuples but are actually "nearly" lists, except at the final position of the backbone). On backtracking, it will pull out the individual records one by one and present the information therein in the head variables:

some_tuple(V,W,Values) :- 
   tuples(Tuples), 
   member((V,W,Values), Tuples).

And then collect using a 2-level bagof/3 call.

solution(Bag) :-
   bagof((V,W,FlatBagForVW),                       % will backtrack over V,W
      SubBag^(bagof(L,some_tuple(V,W,L),BagForVW), % collect lists for a given V,W 
              flatten(BagForVW,FlatBagForVW)),     % flatten it
      Bag).                                        % what we want

Done!

If you are in SWI-Prolog, first tell the toplevel printer to not elide long lists so much:

?-
set_prolog_flag(answer_write_options,[max_depth(100)]).

Then:

?-
solution(Bag).

Bag = [(a,b,[1,3,5,7,9,11,13,15,17,19,21,23]),
       (c,d,[0,2,4,6,8,10,12,14,16,18,20,22]),
       (e,f,[100,200,300,400,500,600,700,800,900,1000,1100,1200])].

A variation using foldl/4 and then library(yall):

'Grouping list elements based on values in list'(Gs) :-
    tuples(Ts),
    foldl([(X,Y,L),V0,V1]>>(
              (  append(N,[(X,Y,L0)|M],V0)
              -> append(L0,L,L1)
              ;  N=V0, L1=L
              ),
              append(N,[(X,Y,L1)|M],V1)
          )
          ,Ts,[],Gs).

a day after...

There is a bug going unnoticed, namely M remains unbound where a pair of keys (that is, X,Y in a tuple) is not present in V0 (have attempted to keep the variables naming coherent to the docs for foldl/4). A possible correction, that illustrates a single unification call (=/2) to perform multiple 'assignments' at once:

    ...
    foldl([(X,Y,L),V0,V1]>>(
              (  append(N,[(X,Y,L0)|M],V0)
              -> append(L0,L,L1)
              ;  (N,M,L1)=(V0,[],L1)
              ),
              append(N,[(X,Y,L1)|M],V1)
    ...

Still, it's not clear to me why the bug didn't materialized... for instance, here it's clearly visible...

?- append([1,2,3],[4,5,6|_],R).
R = [1, 2, 3, 4, 5, 6|_19156].

another way...

library(solution_sequences) is a recent addition to SWI-Prolog arsenal, providing more 'SQL like' constructs, for instance group_by/4:

by_group_by(Gs) :-
    tuples(Ts),
    findall((X,Y,All_xy), (
      group_by([X,Y],L,member((X,Y,L),Ts),Lt),
      append(Lt,All_xy)
    ),Gs).

The documentation is too much terse, but overall, the library is worth a try. There are some examples posted in SWI-Prolog discourse group, but I find the data used there boring and difficult to understand.

Note: I used the syntax [X,Y] for the free variables, to make clear the 'shape' of this important specification is unrelated to the pattern.

group([],L,L).
group([(A,B,L)|T],Acc,Out):-
    (   append(L1,[(A,B,LL)|L2], Acc)
    ->  append(LL,L,LLL),
        append(L1,[(A,B,LLL)|L2], NewAcc),
        group(T,NewAcc,Out)
    ;   append(Acc,[(A,B,L)],NewAcc),
        group(T,NewAcc,Out)
    ).

?- tuples(L), group(L,[],G).
G = [
     (a,b,[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23]),
     (c,d,[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]), 
     (e,f,[100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200])
    ],
L = [
     (a,b,[1, 3, 5, 7]), 
     (a,b,[9, 11, 13, 15]),  
     (a,b,[17, 19, 21, 23]), 
     (c,d,[0, 2, 4, 6]),  
     (c,d,[8, 10, 12, 14]),  
     (c,d,[16, 18, 20, 22]),   
     (e,f,[100, 200, 300, 400]),  
     (e,f,[500, 600, 700, 800]),  
     (e,f,[900, 1000, 1100, 1200])
    ].

Basically I'm stripping an element from the (tuple) list ([(A,B,L)|T]) and look if I got a similar entry within my bag-list (2nd argument, append(L1,[(A,B,LL)|L2], Acc)). I use append/3 to do so because it can be used to find an element and at the same time dividing the original list into the list before and after the found element.
If I found an entry in the baglist, I append the two number lists (L and LL) to a new one (LLL) and exchange the values within the bag-list. Then I call the predicate again with the rest list and the altered bag-list.
If no element fits, I just add the element to the bag-list (as last element, append(Acc,[(A,B,L)],Acc2)) and call the predicate again. So each time I call the predicate group/3 it's first argument loses an element. Until there is no element left (group([],L,L).), in this case I state my bag-list is my ouput.

After playing a bit around with group_by/4 and/or bagof/3 as suggested by Guy coder, and using a variant of Davids "extraction" predicate here is another solution:

one_tuple(V,W,Value) :- 
   tuples(Tuples), 
   member((V,W,Values), Tuples),
   member(Value, Values).

?- findall((A,B,Cs), bagof(C, one_tuple(A, B, C), Cs), O).
O = [(a,b,[1,3,5,7,9,11,13,15,17,19,21,23]),
     (c,d,[0,2,4,6,8,10,12,14,16,18,20,22]),
     (e,f,[100,200,300,400,500,600,700,800,900,1000,1100,1200])].

First we need to be able to find the list of unique A,B pairings in the list of tuples:

foo(X,A,B) :-
    setof( p(A,B), C^member( (A,B,C), X ), T), 
    member( p(A,B), T).

/* 48 ?- tuples(_X), foo(_X,A,B).
A = a,
B = b ;
A = c,
B = d ;
A = e,
B = f. */

Next we collect the tuples for each pair of the A and B values:

bar(X,A,B,G) :- 
    foo(X,A,B),      % for each unique (A,B,_) in X
    findall( C,      % find all Cs such that (A,B,C) is in X
               member( (A,B,C), X),
             CS),
    append( CS,G).   % and append them together
    
/* 66 ?- tuples(_X), bar(_X,A,B,G).
A = a,
B = b,
G = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23] ;
A = c,
B = d,
G = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22] ;
A = e,
B = f,
G = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200].*/

So that the grouping is done with

groups(X,GS):-
    findall( (A,B,G),  bar(X,A,B,G),  GS).

/* 68 ?- tuples(_X), groups(_X, GS).
GS = [(a, b, [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23]), 
      (c, d, [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]), 
      (e, f, [100, 200, 300, 400, 500, 600, 700, 800, 
              900, 1000, 1100, 1200])]. 
*/

update: from David Tonhofer's answer, we see that it can be done with just one nested bagof call,

solution( Sol ) :-
   tuples(TS),
   bagof( (A,B,FLS),     % (* collect (A,B,FLS) for each (A,B) *)
          LS^(           % (* such that (A,B,L) is in TS       *)
                bagof( L,  member((A,B,L), TS),  LS ),
                flatten( LS, FLS)
             ),          % (* with the lists LS flattened      *)
          Sol ).

The outer bagof's goal predicate (i.e. the inner bagof call) is automatically backtracking to produce its LS result for each distinct pair of the A,B values, automagically achieving the same effect as we had "manually", in this answer.

Related