Letterpress tray
Refill a compositor tray so the busiest letter sits nearest to hand, by sorting sixty-odd groups instead of a hundred thousand tiles.
A compositor has finished a job and the metal letter tiles come back to the bench in whatever order they were lifted. The tray has to be refilled so the letter used most sits in the front compartment.
The problem
The shop sets lines of type by hand. Every character is a separate metal tile, and when a line is broken up the tiles land in a heap. Refilling the tray means grouping that heap: all copies of one tile together, and the heaviest group first, because the front compartment is the one the hand reaches without looking.
Two tiles are the same only if they print the same glyph, so T and t are
different tiles and live in different compartments. When two groups hold the
same number of tiles, the shop's rule puts the one whose character comes first
in ASCII order in front — digits before capitals, capitals before lowercase.
Return the heap rewritten as one string in tray order.
Input. line — a string of ASCII letters and digits, the tiles as they came
off the bench.
Output. A string holding exactly the same tiles, with equal tiles adjacent, groups ordered by size descending and by character ascending on a tie.
Example.
line = "peppermint" -> "pppeeimnrt"
Three p tiles lead, then two e. The five singletons follow in ASCII order:
i, m, n, r, t.
A second example, where case matters and a tie has to be broken:
line = "Tattoo" -> "oottTa"
o and t both appear twice, and o comes first in ASCII, so its compartment
goes in front. T is a different tile from t: it stays a group of one, and
because capitals sort before lowercase it lands ahead of a.
Constraints.
0 <= len(line) <= 10^5- every character is
a-z,A-Zor0-9, so at most 62 distinct tiles - the empty heap is a valid input and gives the empty string
Hints
Hint 1
The output is fixed once you know how many of each tile there are. The position a tile came from carries no information at all.
Hint 2
There are at most 62 distinct tiles however long the line is. What should the sort be sorting — 100,000 things, or 62?
Hint 3
The order has two levels: count descending, character ascending. A tuple key
(-count, tile) says both at once, because tuples compare left to right.
Approach
Brute force
Walk the 62 possible tiles, scanning the whole heap to count each one, then repeatedly pick the largest count not yet emitted: 62 passes over n plus a quadratic pick-the-max loop over the groups — around 6.2 million character reads at n = 100,000, where one pass would do. Sorting the tiles themselves under a frequency comparator is better but still pays n log n comparisons on all 100,000 of them.
The insight
The answer is a function of the tally alone, so counting collapses the input to at most 62 groups and the sort runs on the groups, not on the tiles.
Nothing in the required output depends on where a tile sat in the heap: two heaps with the same tally produce the same tray, so one counting pass is a lossless summary. Sorting 62 groups then costs the same whether n is 60 or 60,000, which is why the log factor attaches to the alphabet and not to the input.
Algorithm
- Tally the string into
(tile, count)pairs in one pass. - Sort the pairs by the key
(-count, tile)— bigger group first, smaller character code first on a tie. - Emit each tile repeated
counttimes and join the pieces.
Complexity
Time O(n + k log k), with n tiles and k ≤ 62 distinct ones — one pass to count, a sort whose size does not grow with n, one pass to build the answer. Space O(n) for the output string; the tally itself is O(k), bounded by 62.
Solution
"""Letterpress tray — tally the tiles, then sort the groups by (-count, tile)."""
from collections import Counter
def solve(line):
tally = Counter(line)
# Groups are what gets sorted, not tiles: at most 62 of them however long
# the line is. Negating the count sorts it downward while the tile itself
# still sorts upward, which is the tie rule the tray wants.
groups = sorted(tally.items(), key=lambda pair: (-pair[1], pair[0]))
return "".join(tile * count for tile, count in groups)The cases that ran
TESTS = [
(("peppermint",), "pppeeimnrt"),
(("Tattoo",), "oottTa"),
(("",), ""),
(("q",), "q"),
(("abcabc",), "aabbcc"),
(("7777",), "7777"),
(("aA0",), "0Aa"),
]Pitfalls
- Sorting the characters directly by
-countand leaning on stability. Python keeps ties in first-appearance order, so"Tattoo"comes out"ttooTa"—tin front because it appeared earlier. The rule asked for ASCII order, which is a second key, not an accident of the input. - Normalising case before counting. Lowercasing merges
Tintotand returns"tttooa", a string containing a tile the shop never had and missing one it did. - Taking
max(tally)on an empty heap. Withline = ""the tally is empty and a maximum of nothing raisesValueError; the answer is"".
Variants
- Trailhead campsites — the same two-level tuple key, except both levels run downward and a filter comes first.
- Consecutive van runs — also starts from a tally, but consumes it in a forced order instead of sorting it.