One method, after a bit of performance improvement:
go(Sol) :-
People = [alex, bob, john, deo, sam],
Topics = [lang, law, it, math, phys],
TopicsP = [LangP, LawP, ItP, MathP, PhysP],
Placements = [1, 2, 3, 4, 5],
CombLsts = [People, Topics, Placements],
% Break symmetry by specifying the people order
Sol = [
[alex, _, AlexP],
[bob, _, BobP],
[john, _, JohnP],
[deo, _, _],
[sam, _, SamP]
],
% Rule 1
when((nonvar(LangP), nonvar(BobP)), (
% Caution with "placed higher" = lower
LangPDiff is BobP - LangP,
LangPDiff >= 1,
when(nonvar(LawP), (
BobPDiff is LawP - BobP,
LangPDiff =:= BobPDiff
))
)),
% Rule 2
% Caution with "placed higher" = lower
when_both(ItP, JohnP, ItP is JohnP - 3),
% Rule 3
freeze(AlexP, 0 is AlexP mod 2),
freeze(MathP, 1 is MathP mod 2),
% Rule 4
% "places below" = higher number
when_both(SamP, PhysP, SamP is PhysP + 2),
% Find a solution
find_combs(CombLsts, Sol, Topics, TopicsP).
find_combs(CombLsts, [Comb|Sol], Topics, TopicsP) :-
% Select a combination, with remainder
maplist(select, Comb, CombLsts, Rem),
% Populate TopicsP
Comb = [_, Topic, Placement],
update_topic_topicp(Topics, Topic, Placement, TopicsP),
% Continue down this combination path
find_combs(Rem, Sol, Topics, TopicsP).
find_combs(CombLsts, _, _, _) :-
% Nothing left to try
maplist(=([]), CombLsts).
update_topic_topicp([HT|TT], Topic, Placement, [HTP|TTP]) :-
( HT = Topic -> HTP = Placement
; update_topic_topicp(TT, Topic, Placement, TTP)
).
when_both(V1, V2, Cond) :-
when((nonvar(V1), nonvar(V2)), Cond).
Result:
?- time(findall(S, go(S), Ss)).
% 23,998 inferences, 0.011 CPU in 0.011 seconds (101% CPU, 2111377 Lips)
Ss = [[[alex,lang,2],[bob,phys,3],[john,law,4],[deo,it,1],[sam,math,5]]].