The shade chart
Pick out every shade name that is spelled by two or more other names on the same chart, by walking one prefix tree from each reachable cut point.
A paint counter names its blends by gluing shorter shade names together. The chart does not say which names are blends, so you have to read that back out of the names themselves.
The problem
A paint shop's chart lists shade names, all lowercase letters, all distinct.
Some are base shades, mixed straight from pigment. The rest are blends, and the
naming rule is strict: a blend's name is two or more names already on the chart
written end to end, with nothing between them. With sea and foam listed,
seafoam is a legal blend name; a name may be used twice, so ink makes
inkink legal too.
Report every name on the chart that is a blend. A name that reads only as one whole piece is a base shade, however promising its opening looks.
Input. chart — a list of distinct names, lowercase letters.
Output. The blend names, sorted alphabetically.
Example.
chart = ["sea", "foam", "seafoam", "seaweed", "moss", "green", "mossgreen", "sun"]
-> ["mossgreen", "seafoam"]
seafoam is sea + foam and mossgreen is moss + green. seaweed opens
with the chart name sea, but weed is not on the chart, so it is a base shade.
A second example, where the longest opening piece is the wrong one:
chart = ["ice", "iceblue", "bluerose", "icebluerose"]
-> ["icebluerose"]
Cutting icebluerose after the longest name that fits, iceblue, leaves rose,
which is not on the chart. Cutting after ice leaves bluerose, which is.
iceblue itself is a base shade: blue was never listed.
Constraints.
1 <= len(chart) <= 10^41 <= len(name) <= 30, lowercase a-z, names distinct- total characters across the chart at most
6 * 10^5
Hints
Hint 1
Take one name at a time. What you choose is a set of cut positions, and every piece between two cuts has to be on the chart.
Hint 2
Two different ways of cutting up the first seven letters leave you facing exactly the same remainder. So what is worth remembering about a position?
Hint 3
From a cut point, one walk down a prefix tree of the whole chart spells the remainder letter by letter, and every end marker it passes is a legal next cut.
Approach
Brute force
A name of length L has 2^(L−1) ways to cut it, each checked piece by piece against a set. At L = 30 that is 5 · 10⁸ cut sets for one name, and the chart holds 10⁴ names.
The insight
Whether the rest of a name can be spelled depends only on where the last cut fell, not on how the name got there — so one boolean per position replaces the search over cut sets.
The pieces are laid down left to right and never overlap, so two ways of spelling the first i letters leave the same job behind: spell the rest. That collapses 2^(L−1) cut sets to L positions. The trie supplies the moves between them — from position i, one walk spells the remainder a letter at a time, and every end marker passed on the way is a cut you can reach, found in one pass instead of L substring lookups.
The "two or more" rule needs no counter: the only one-piece split runs from 0 to the last letter, so skipping that single end marker rules it out.
Algorithm
- Build one trie over the whole chart, with an end marker on each name's last node.
- For a name of length n, keep
reachable[0..n], all False butreachable[0]. - For each i where
reachable[i]is True, walk the trie from the root over the name from i onward, stopping when a letter has no child. - Whenever that walk stands on an end marker at position j, set
reachable[j + 1], unless i is 0 and j is the last letter — that is the name itself. - The name is a blend when
reachable[n]is True. Collect those and sort.
Complexity
Time O(S + N · L²) — one pass over S total characters to build, then at most L walks of L steps per name, so 900 steps each at L = 30. Space O(S) for the trie, plus a table of L booleans reused per name.
Solution
"""The shade chart — a prefix tree walked from every reachable cut point."""
# Names are lowercase letters only, so "#" is safe as the end-of-name marker.
END = "#"
def build(names):
"""One node per distinct prefix of the whole chart."""
root = {}
for name in names:
node = root
for ch in name:
node = node.setdefault(ch, {})
node[END] = True
return root
def is_blend(root, name):
n = len(name)
# reachable[i] is True when name[:i] can be spelled with chart names.
# How it was spelled never matters, only that it was.
reachable = [False] * (n + 1)
reachable[0] = True
for i in range(n):
if not reachable[i]:
continue
node = root
for j in range(i, n):
node = node.get(name[j])
if node is None:
break # no chart name starts here
if END in node and not (i == 0 and j == n - 1):
# Skipping the one whole-name match is what enforces "two or
# more parts": every other split has at least two pieces.
reachable[j + 1] = True
return reachable[n]
def solve(chart):
root = build(chart)
return sorted(name for name in chart if is_blend(root, name))The cases that ran
TESTS = [
# "seaweed" opens with the chart name "sea" and still is not a blend.
(
(["sea", "foam", "seafoam", "seaweed", "moss", "green", "mossgreen", "sun"],),
["mossgreen", "seafoam"],
),
# Cutting after the longest name that fits ("iceblue") dead-ends on "rose";
# cutting after "ice" works. "iceblue" itself is not a blend: no "blue".
((["ice", "iceblue", "bluerose", "icebluerose"],), ["icebluerose"]),
# A name may be reused inside a blend, and a blend may have three parts.
((["ink", "inkink", "blueinkink", "blue"],), ["blueinkink", "inkink"]),
# One name on the chart splits into nothing.
((["rose"],), []),
# No name is a prefix of another: nothing can be a blend.
((["sea", "moss", "sun"],), []),
# Shortest possible names, where reuse is the only way to build a blend.
((["a", "aa", "aaa"],), ["aa", "aaa"]),
]Pitfalls
- Forgetting to exclude the whole name. Every name trivially spells itself in one piece, so without that one skipped marker the answer is the entire chart.
- Stopping the walk at the first end marker. On
icebluerosethe walk from position 0 passesiceand theniceblue; taking only the last, or only the first, and moving on loses the split that works. Record every marker. - Marking a position because a node exists. The node for
seawexists becauseseaweedis on the chart. Marking reachable positions without testing the end marker makes every name a blend.
Variants
- Filling the grid — one walk per query with blanks in the pattern, instead of many walks with cuts in the name.
- The dispatch console — the same tree carrying a ranking at every node rather than a marker at the ends.