skip to content

Generating permutations of a multiset, why must sort-and-skip test that the earlier twin is unused?

level: seniorimportance: should knowfreq 55%

answer

  1. Why does sorting come first?
  2. Identical items are interchangeable
  3. Pick one canonical order among twins
  4. Which twin is allowed to go first?
  5. Reverse it and the second twin is stranded

basics

~20 s

Sorting makes equal items adjacent so the check can be local. The guard then fixes one canonical order among identical items: a twin may only be placed once its left neighbour already is. Reverse the test and the second twin can never be placed at all.

solid answer

~50 s

Duplicates appear because identical items are interchangeable — swap two copies of the same track and you have produced the same ordering twice. Sorting puts equal items next to each other so a one-step check suffices, and the guard `a[i] == a[i-1] and used[i-1] is false, then skip` enforces a canonical order: among identical items you must consume the leftmost free one first, so exactly one of the `k!` internal arrangements of a group of `k` twins survives. There are two ways to get it wrong. Drop the guard and every group of `k` identical items multiplies your output by `k!`. Flip it to skip when the earlier twin *is* used and it is not merely stricter — the second twin can then never be placed on any branch, so complete outputs vanish silently. And without the sort the guard is meaningless, because equal items are no longer neighbours.

code

pseudocode · 14 lines
pseudocode
build(path):
    if length(path) == n:
        output(path)
        return
    for i in 0..n-1:
        if used[i]:
            continue
        if i > 0 and a[i] == a[i-1] and used[i-1] == false:
            continue
        used[i] = true
        append(path, a[i])
        build(path)
        removeLast(path)
        used[i] = false

go deeper

for a junior

Know that repeated values produce repeated results because the search treats two equal items as distinct candidates, and that sorting the input first is what makes a neighbour-based fix possible at all.

for a middle

Explain the canonical rule the guard enforces — equal items are consumed left to right — and be able to trace two identical items by hand to show why exactly one branch survives.

for a senior

Diagnose both failure directions from their symptoms: inflated output by a factorial factor when the guard is missing, silently vanished results when it is reversed. Say why pruning at the node beats filtering the output.

for a principal

Own the correctness-risk argument. A dedup rule whose reversed form fails silently is exactly the kind of code that needs a result-count assertion in tests and a comment stating the invariant, not a reviewer who happens to remember which way it goes.

## Why duplicates appear at all A multiset is a collection that may contain repeated values. Take a playlist where the same track appears twice. The enumeration machinery does not know the two copies are the same thing: it sees candidate index 0 and candidate index 1, treats them as distinct, and happily produces both "copy A then copy B" and "copy B then copy A". As sequences of *values* those two results are identical, so the output contains the same ordering twice. Generalising: if the multiset contains a group of `k` identical items, every distinct output is generated `k!` times, once per internal arrangement of that group. Two twins doubles the output; three identical tracks sextuples it. And the cost is not just in the printing — each duplicate is a fully explored subtree, so the wasted work is proportional to the duplication factor. ## The canonical-choice fix The fix is the same idea used everywhere in enumeration: of the many search paths that produce the same result, allow exactly one and prune the rest at the node where they diverge. For identical items, choose this canonical rule: **within a group of equal items, they must be consumed left to right**. If the copy at index `i` and the copy at index `i-1` are equal, then placing `i` is only allowed when `i-1` has already been placed. Any branch that tries to place the right-hand twin first is a relabelling of a branch already explored, so it is cut. Expressed as a skip test inside the candidate loop, with the candidate list sorted so equal items are adjacent: > if `i > 0` and `a[i] == a[i-1]` and `used[i-1]` is false, skip `i`. Sorting is a precondition, not a nicety. The test only looks one position back, so it can only catch duplicates that are neighbours. On unsorted input the same value can be scattered and the guard silently misses most of it, leaving duplicates in the output. ## Trace the smallest case Two identical items, `a = [x, x]`. There is exactly one distinct ordering. With the correct guard: at the top level, index 0 is allowed (there is no earlier neighbour) and placing it marks index 0 used. At the next level index 1 is considered — its neighbour is equal, but that neighbour *is* used, so it is not skipped, and the ordering completes. Back at the top level, index 1 is now considered as a first pick: its neighbour is equal and unused, so it is skipped. One result. Correct. Now flip the condition to skip when the earlier twin *is* used. At the top level index 0 is placed. At the next level index 1 sees its equal neighbour as used and skips — the branch dead-ends without emitting anything. Back at the top, index 1 sees its equal neighbour unused and skips too. Total output: nothing at all. The reversed guard does not dedupe more aggressively; it makes it impossible to ever place the second member of a group, so every branch that needs a repeat dies. That asymmetry is the whole point of the question, and it is why "either direction works, one just prunes earlier" is a wrong answer worth being able to refute with the two-element trace. ## An alternative that needs no sort Sorting requires the items to be comparable, which is not always true. A second technique dedupes per level instead: at each recursion level, keep a set of values already tried in this loop, and skip a candidate whose value is already in it. Different levels get different sets. This is equivalent in output and needs only equality rather than ordering. The costs are a set allocated per level — extra memory proportional to depth times branching — and the loss of lexicographic output order that a sorted candidate list gives you for free. Choose it when items cannot be ordered or when the input arrives unsorted and sorting it would disturb something else. ## The same guard for unordered selections When you are choosing groups rather than orderings, the same canonical idea applies with a simpler test. In a search that carries a start index and loops `i` from `start`, skip a candidate that equals its predecessor **when it is not the first iteration of this loop**. The reasoning is identical: at a given level, trying the same value twice explores the same subtree twice. No availability markers are needed, because the increasing start index already prevents an item being reused. ## Why filtering afterwards is the wrong answer The tempting shortcut is to generate everything and deduplicate the results at the end. It is correct and it is the wrong trade. You have already paid to explore every duplicate subtree — a factor of `k!` per group of twins — and now you additionally hold or hash the entire inflated output. The guard costs one comparison per candidate and eliminates the subtree before it is entered. On a search that is already factorial in size, prune at the node, never filter at the leaf. ## What a strong answer sounds like Name the cause (interchangeable items produce relabelled duplicate paths), name the fix (a canonical left-to-right consumption order made local by sorting), state the guard, state both failure directions with their symptoms — missing guard means output inflated by `k!`, reversed guard means outputs disappear — and mention the per-level-set alternative for uncomparable items.

  • How do you dedupe when the items cannot be sorted?
    Keep a set of values already tried at the current recursion level and skip any candidate whose value is already in it. It needs only equality rather than ordering, and it dedupes per level exactly as the neighbour guard does. The price is a set allocated per level and the loss of the lexicographic output order that sorted input gives you for free.
  • What is the cost of generating everything and filtering duplicates at the end?
    You pay for every duplicate subtree before discarding its results: a group of k identical items inflates the search by a factor of k!, and you then have to hold or hash the whole inflated output to filter it. The guard costs one comparison per candidate and cuts the subtree before it is entered, which is the difference between pruning and mopping up.
  • Does the same idea work when you are choosing unordered groups rather than orderings?
    Yes, with a simpler test. In a search that loops from a start index, skip a candidate equal to its predecessor whenever this is not the first iteration of the current loop — trying the same value twice at one level explores the same subtree twice. No availability markers are needed there, because the advancing start index already prevents reuse.

saying these in an interview costs you the question

  • Says either direction of the twin test dedupes fine
  • Applies the neighbour guard without sorting first
  • Plans to filter duplicates after generating everything
  • Claims duplicates cost extra output but not extra search
  • Thinks the guard compares against the whole partial ordering

context