Quick Tip: Interval Trees in Prolog
Today I had a need to be able to efficiently look up mappings from a range to a value in Prolog. The initial naïve version looked essentially like this:
find_thing_in_range(Mapping, Key, Value) :- member(start_end_value(Start, End, Value), Mapping), between(Start, End, Key), !.
Simple, nice and declarative: I build up a list of terms like start_end_value(Start, End, Value)
to represent the range and associated value, nice and easy.
Unfortunately, while the runtime wasn’t too bad for the simple testcases I was using, profiling the overall predicate for which this was a small component, this accounted for 80% of the runtime.
That wasn’t too surprsing, as using member/2
meant a linear traversal through the list each time and the predicate was getting called quite a bit.
An obvious solution then would be to use a more efficient data structure for searching.
Red-black trees (via library(rbtrees)
) are usually the go-to, but the normal rb_lookup/3
wouldn’t work for me here.
The normal use of the trees assume that you’re searching for a specific key, but here I want the keys to be a range that I will search within.
Fortunately, it was pretty easy to modify rb_lookup/3
to instead search inside a range:
rb_lookup_range(Key, KeyRange, Value, t(_, Tree)) => rb_lookup_range_(Key, KeyRange, Value, Tree). rb_lookup_range_(_Key, _KeyRange, _Value, black('', _, _, '')) :- !, fail. rb_lookup_range_(Key, KeyRange, Value, Tree) :- arg(2, Tree, Start-End), compare(CmpS, Key, Start), compare(CmpE, Key, End), rb_lookup_range_(t(CmpS, CmpE), Key, Start-End, KeyRange, Value, Tree). rb_lookup_range_(t(>, <), _, Start-End, KeyRange, Value, Tree) => arg(3, Tree, Value), KeyRange = Start-End. rb_lookup_range_(t(=, _), _, Start-End, KeyRange, Value, Tree) => arg(3, Tree, Value), KeyRange = Start-End. rb_lookup_range_(t(_, =), _, Start-End, KeyRange, Value, Tree) => arg(3, Tree, Value), KeyRange = Start-End. rb_lookup_range_(t(<, _), Key, _, KeyRange, Value, Tree) => arg(1, Tree, NTree), rb_lookup_range_(Key, KeyRange, Value, NTree). rb_lookup_range_(t(_, >), Key, _, KeyRange, Value, Tree) => arg(4, Tree, NTree), rb_lookup_range_(Key, KeyRange, Value, NTree).
Use it like this:
?- L = [(0-50)-1, (51-100)-2, (101-150)-3], list_to_rbtree(L, Rb), rb_lookup_range(5, KR, V, Rb), format("Found ~w in range ~w: Value ~q~n", [5, KR, V]).
If you ever need to do something like this, hope this is useful!