This is a less formal answer, but describes the same thing as Alin's answer above.
Standard binary merkle tree:
R
/ \
N N
/ \ / \
N N N N
/ \ / \ / \ / \
6 3 9 0 8 4 7 2
Verifier knows only R, and for prover to prove membership they have to supply leaves along the path from given member towards R. So far so good.
To prove non-membership, what you can do is to have a sorted merkle tree:
R
/ \
N N
/ \ / \
N N N N
/ \ / \ / \ / \
0 2 3 4 6 7 8 9 <-- values
0 1 2 3 4 5 6 7 <-- binary index
To prove that 5 is not in the set, you supply proof of membership for 4 and 6, which are in successive binary order (3 and 4 in this case), and the hashed values cover a range the query value falls into. And since we have the assumption of order, 5 can't appear anywhere else.
To be specific, the properties are:
The verifier has to trust R to be result of honestly ordered tree. This can be verified probabilistically (including Fiat-Shamir) by samping with few queries and observing the order is always maintained. This may become quite heavy depending on the nature of data (if keys allow for large gaps or not). Better is to simply assume the R we know is honest.
The worst-case proof size, with completely distinct branches is only 2log2(n)
of the set.
Worst of all, you cannot easily update the tree without rebuilding it. To make an update you need to know the whole universe, not just the tip R as is the case for membership. Thus the construct is suitable for static dictionaries which are seldom updated, as well as short round membership protocols like joinmarket below.
The tree must be binary and of known size, if it is truncated (some part of the DAG terminate earlier), use some graph rule, like that truncation may occur along the rightmost branches.
R
/ \
N N
/ \ / \
N N N 7
/ \ / \ /
0 2 3 4 6
For more long-winded description, see https://gist.github.com/chris-belcher/eb9abe417d74a7b5f20aabe6bff10de0