Search code examples
prologcombinations

Unique sets that add up to target value n


The given database:

age(ann, 20).
age(joe, 44).
age(bob, 40).
age(min, 27).
age(cai, 20).
age(ned, 27).
age(deb, 36).
age(pat, 36).
age(edo, 24).
age(tod, 56).

What I have so far:

hundred(L) :-
    findall(X, age(X,_), L1),
    sum(L1,100,L), 
    length(L,Len),
    Len > 1.

sum(_, 0, []).
sum(L, S, [H|T]) :-
    select(H, L, L1),
    age(H, A),
    S1 is S-A,
    S1 >= 0,
    sum(L1, S1, T).

I'd like to get the unique sets of persons (of length > 1) in the database whose ages add up to 100. My solution yields all possible permutations whereas I'd like to get the unique sets of persons as below (the listing order in the output example is irrelevant):

L=[joe,tod];
L=[ann,joe,deb];
L=[ann,joe,pat];
L=[ann, edo, tod];
L=[ann, cai, deb, edo];
L=[ann, cai, edo, pat];
L=[cai, edo, tod];
L=[joe, cai, deb];
L=[joe, cai, pat];
L=[bob, deb, edo];
L=[bob, pat, edo];

For clarity: people may have the same age, and that's fine. So the number combinations themselves may include duplicates to get to 100, as long as those ages belong to different persons. In the end, each solution should be a combination of different persons but should not be a mere permutation of a previous solution. My solution always takes combinations of different persons, but lists every change in the order as a new solution. I've tried to get rid of the permutations by putting all solutions in a list of lists and then sorting it to remove duplicates, but it didn't work.


Solution

  • For the numbers, can use generic predicates:

    sub_list_sum(Sum, Lst, Sub) :-
        compare(C, Sum, 0),
        sub_list_sum_(C, Sum, Lst, Sub).
    
    sub_list_sum_(=, 0, _, []).
    sub_list_sum_(>, Sum, Lst, [E|Sub]) :-
        select_forward(E, Lst, Lst0),
        Sum0 is Sum - E,
        compare(C, Sum0, 0),
        sub_list_sum_(C, Sum0, Lst0, Sub).
    
    % A subsequence variant of select/3
    select_forward(E, [H|T], F) :-
        select_forward_(T, H, E, F).
    
    select_forward_(T, H, H, T).
    select_forward_([H|T], _, E, F) :-
        select_forward_(T, H, E, F).
    

    Result in swi-prolog:

    ?- Dupes = [20, 44, 40, 27, 20, 27, 36, 36, 24, 56],
    sort(Dupes, Sorted), sub_list_sum(100, Sorted, S).
    S = [20, 24, 56] ;
    S = [20, 36, 44] ;
    S = [24, 36, 40] ;
    S = [44, 56] ;
    false.
    

    Customising this for the ages:

    age(ann, 20).
    age(joe, 44).
    age(bob, 40).
    age(min, 27).
    age(cai, 20).
    age(ned, 27).
    age(deb, 36).
    age(pat, 36).
    age(edo, 24).
    age(tod, 56).
    
    sub_list_age_sum(Sum, Sub) :-
        % Want at least length 2
        Sub = [_,_|_],
        bagof(age(P, A), age(P, A), Ages),
        compare(C, Sum, 0),
        sub_list_age_sum_(C, Sum, Ages, Sub).
    
    sub_list_age_sum_(=, 0, _, []).
    sub_list_age_sum_(>, Sum, Lst, [P|Sub]) :-
        select_forward(age(P, A), Lst, Lst0),
        Sum0 is Sum - A,
        compare(C, Sum0, 0),
        sub_list_age_sum_(C, Sum0, Lst0, Sub).
    

    ... and add select_forward code as above - result:

    ?- sub_list_age_sum(100, L).
    L = [ann, joe, deb] ;
    L = [ann, joe, pat] ;
    L = [ann, cai, deb, edo] ;
    L = [ann, cai, pat, edo] ;
    L = [ann, edo, tod] ;
    L = [joe, cai, deb] ;
    L = [joe, cai, pat] ;
    L = [joe, tod] ;
    L = [bob, deb, edo] ;
    L = [bob, pat, edo] ;
    L = [cai, edo, tod] ;
    false.