combinatorics
If you have generic functions like combinations
and permutations
you can solve this problem easily -
function canConstruct (target = "", parts = [])
{ for (c of combinations(parts))
for (p of permutations(c))
if (p.join("") == target)
return true
return false
}
Where combinations
is defined as -
function* combinations (t)
{ if (t.length == 0)
yield t
else
for (const c of combinations(t.slice(1)))
( yield c
, yield [t[0], ...c]
)
}
And permutations
is defined as -
function permutations (t)
{ if (t.length < 2)
return [t]
else
return permutations(t.slice(1))
.flatMap(p => rotations(p, t[0]))
}
function rotations (t, e)
{ if (t.length == 0)
return [[e]]
else
return [[e,...t], ...rotations(t.slice(1), e).map(r => [t[0], ...r])]
}
Let's test it out -
console.log(canConstruct("abcdef", ["ab", "abc", "cd", "def", "abcd"]))
console.log(canConstruct("abcddc", ["ab", "abc", "cd", "def", "abcd"]))
console.log(canConstruct("abcabc", ["ab", "abc", "cd", "def", "abcd"]))
console.log(canConstruct("abcabc", ["ab", "abc", "cd", "abc", "def", "abcd"]))
console.log(canConstruct("abcabcabc", ["ab", "abc", "cd", "abc", "def", "abcd"]))
Expand the snippet below to verify the results in your own browser -
function* combinations (t)
{ if (t.length == 0)
yield t
else
for (const c of combinations(t.slice(1)))
( yield c
, yield [t[0], ...c]
)
}
function permutations (t)
{ if (t.length < 2)
return [t]
else
return permutations(t.slice(1))
.flatMap(p => rotations(p, t[0]))
}
function rotations (t, e)
{ if (t.length == 0)
return [[e]]
else
return [[e,...t], ...rotations(t.slice(1), e).map(r => [t[0], ...r])]
}
function canConstruct (target = "", parts = [])
{ for (c of combinations(parts))
for (p of permutations(c))
if (p.join("") == target)
return true
return false
}
console.log(canConstruct("abcdef", ["ab", "abc", "cd", "def", "abcd"]))
console.log(canConstruct("abcddc", ["ab", "abc", "cd", "def", "abcd"]))
console.log(canConstruct("abcabc", ["ab", "abc", "cd", "def", "abcd"]))
console.log(canConstruct("abcabc", ["ab", "abc", "cd", "abc", "def", "abcd"]))
console.log(canConstruct("abcabcabc", ["ab", "abc", "cd", "abc", "def", "abcd"]))
true
false
false
true
false
Scott's brilliant answer has an adaptation that allows for a given part to be reused multiple times, ie where canConstruct("abab", ["ab"])
is true
. I can't see a variant of this answer that easily enables such a behaviour.
short-circuit
Above we use a simple implementation of permutations
but if we use generators, like we did with combinations
, we can achieve a desirable short-circuiting behavior for our program -
function* permutations (t)
{ if (t.length < 2)
yield t
else
for (const p of permutations(t.slice(1))) // <- lazy
for (const r of rotations(p, t[0])) // <- lazy
yield r
}
function* rotations (t, e)
{ if (t.length == 0)
yield [e]
else
yield *chain
( [[e, ...t]]
, map(rotations(t.slice(1), e), r => [t[0], ...r])
)
}
This depends on two generic functions for working with generators, map
and chain
-
function* map (t, f)
{ for (const e of t)
yield f(e)
}
function* chain (...ts)
{ for (const t of ts)
for (const e of t)
yield e
}
Implementation of canConstruct
does not require any changes. permutations
will generate permutations one-by-one and when the return
is encountered, no additional permutations will be computed -
function canConstruct (target = "", parts = [])
{ for (c of combinations(parts))
for (p of permutations(c))
if (p.join("") == target)
return true
return false
}
Expand the snippet below to verify the results in your own browser -
console.log(canConstruct("abcdef", ["ab", "abc", "cd", "def", "abcd"]))
console.log(canConstruct("abcddc", ["ab", "abc", "cd", "def", "abcd"]))
console.log(canConstruct("abcabc", ["ab", "abc", "cd", "def", "abcd"]))
console.log(canConstruct("abcabc", ["ab", "abc", "cd", "abc", "def", "abcd"]))
console.log(canConstruct("abcabcabc", ["ab", "abc", "cd", "abc", "def", "abcd"]))
function* combinations (t)
{ if (t.length == 0)
yield t
else
for (const c of combinations(t.slice(1)))
( yield c
, yield [t[0], ...c]
)
}
function* permutations (t)
{ if (t.length < 2)
yield t
else
for (const p of permutations(t.slice(1)))
for (const r of rotations(p, t[0]))
yield r
}
function* rotations (t, e)
{ if (t.length == 0)
yield [e]
else
yield *chain
( [[e, ...t]]
, map(rotations(t.slice(1), e), r => [t[0], ...r])
)
}
function* map (t, f)
{ for (const e of t)
yield f(e)
}
function* chain (...ts)
{ for (const t of ts)
for (const e of t)
yield e
}
function canConstruct (target = "", parts = [])
{ for (c of combinations(parts))
for (p of permutations(c))
if (p.join("") == target)
return true
return false
}
console.log(canConstruct("abcdef", ["ab", "abc", "cd", "def", "abcd"]))
console.log(canConstruct("abcddc", ["ab", "abc", "cd", "def", "abcd"]))
console.log(canConstruct("abcabc", ["ab", "abc", "cd", "def", "abcd"]))
console.log(canConstruct("abcabc", ["ab", "abc", "cd", "abc", "def", "abcd"]))
console.log(canConstruct("abcabcabc", ["ab", "abc", "cd", "abc", "def", "abcd"]))
true
false
false
true
false
For additional explanation and other benefits to using generators to solve this problem, see this related Q&A.