Observations:
Consider the trivially simple ConstraintMap:
constraints = fromList [ ("a", ["b","d"])
, ("b", ["a","c","d"])
, ("c", ["a"])
, ("d", ["b","c"])
]
There are only two Hamiltonian cycles satisfying the ConstraintMap:
"c" -> "a" "c" -> "a"
^ | and ^ |
| v | v
"d" <- "b" "b" <- "d"
There are $4! = 24$ permutations but only $2$ are valid Hamiltonian cycle solutions. Therefore we should devise an algorithm which only uses the significantly smaller search space of valid Hamiltonian cycles!
We can do this by viewing all the possible constructions as a tree
We select an arbitrary element as the root node (WLOG "a"
). Then the branches of this element are it's recipients elements, minus any recipient element in it's parental line. We apply this structure recursively to depth $d = length(\;constraints\;)$ to construct all valid Hamiltonian cycles.
"a"
/ \
"b" "d" -- Note no "a" in "b"'s children because "a" is "b"'s parent
/ | \ / \ -- The same rules of excluding the parental line follows for all nodes!
"c" X "d" "c" "b"
| / | | / | \
X X "c" X X "c" X
^ ^
+---------+--- Valid terminal nodes because "c" maps to "a"
As you can see, we have in fact constructed all $2$ valid Hamiltonian cycles:
"a"
/ \
"b" "d"
\ \
"d" "b"
| |
"c" "c"
We can select a single uniformly random valid Hamiltonian cycle by
preforming a depth limited search over a uniformly randomized search space.
We do this in a manor similar to the tree construction above.
Algorithm:
- We first fix an arbitrary element as the root node.
- We then populate the branches of this element with the possible recipients elements, minus any recipient element in it's parental line.
- We then randomly permute the branches of the element and begin a depth limited search on the first element in the permutation.
- We apply this recursively to depth $d = length(\;constraints\;)$.
- We return the first path of depth $d$ which satisfies the Hamiltonian constraints.
This runs in $\mathcal{O}(n)$ memory and $\Omega(n)$ & $\mathcal{O}(n!)$ time.
Best Case Complexity:
- Linear time to generate a valid solution in the first few attempts
Worst Case Complexity:
- Factorial time to generate all possible solutions and determine none satisfy constraints
Average Case Complexity:
- I don't know, but for my test battery of real world familial constraints with arrangement histories my Haskell implementation returns a valid cycle instantaneously ($time\;<\;\frac{1}{20}\;sec$).
Implementation:
My Haskell implementation is viewable here:
Imperative Pseudocode:
List<String> selectCyclicArrangement(Map<String,Set<String>> constraints) {
root = chooseRandom(keys(constraints));
height = length(constraints);
return lazySelectArrangement( root
, height
, constraints
, constraints
, 1
, root);
}
List<String>? lazySelectArrangement(String root
, int height
, Map<String, Set<String>> originalConstraints
, Map<String, Set<String>> constraints
, int depth
, String key) {
if(depth == height) {
if(originalConstraints[key].contains(root))
return (new List<String>()).add(key);
else
return null;
}
branches = constraints[key];
randomlyPermute(branches);
if(branches.length == 0)
return null;
reducedConstraints = constraints.removeFromAll(key);
result = null
foreach(branch in branches) {
result = lazySelectArrangement( root
, height
, originalConstraints
, reducedConstraints
, depth+1
, branch);
if(result != null)
break;
}
if(result != null)
return result.add(key);
else
return null;
}
A->B->C->A
andD->E->F->D
is a valid solution to a Secret Santa arrangement. $\endgroup$