154 lines
5.5 KiB
Python
154 lines
5.5 KiB
Python
"""
|
|
Double decker train problem:
|
|
|
|
|^^^^^^^^^^^^^^| |^^^^^^^^^^^^^^|
|
|
| Double cargo | | wagon |
|
|
~~~~ ____ |--------------| |--------------|
|
|
Y_,___|[]| | Double | | cargo wagon |
|
|
{|_|_|_|PU|_,_|______________|_,_|______________|
|
|
//oo---OO=OO O-O O-O ^ O-O O-O ^
|
|
|
|
This is a deceptively simple programming puzzle where you get to make an ordering of rail wagons.
|
|
Each wagon has two floors and there is text on the outside of the top and bottom floor.
|
|
When your chosen wagons are linked together, the whole text on the top should match the whole bottom.
|
|
Output an empty list only when there is no non-empty choice of wagons with matching text.
|
|
Your ordering may include any wagon any number of times.
|
|
|
|
Example: (illustrated above)
|
|
Input wagons: [("Double cargo ", "Double "), ("wagon", "cargo wagon")]
|
|
Output ordering: [0, 1]
|
|
|
|
Explanation:
|
|
Both the top and bottom will read "Double cargo wagon" when concatenated.
|
|
Note that there is no need to mutate whitespace characters in the input.
|
|
"""
|
|
|
|
from typing import Callable, List, Optional, Tuple
|
|
|
|
Wagon = Tuple[str, str]
|
|
|
|
|
|
MAX_LENGTH = 10
|
|
|
|
def my_solver(wagons: List[Wagon]) -> List[int]:
|
|
# Handle empty wagon case
|
|
for i, wagon in enumerate(wagons):
|
|
if wagon[0] + wagon[1] == "":
|
|
return [i]
|
|
return solve("", "", [], prepare(wagons))
|
|
|
|
def prepare(wagons: List[Wagon]) -> List[Tuple[int, Wagon]]:
|
|
seen = set()
|
|
prepared = []
|
|
for i, wagon in enumerate(wagons):
|
|
if wagon in seen:
|
|
continue
|
|
prepared.append((i, wagon))
|
|
seen.add(wagon)
|
|
return prepared
|
|
|
|
def solve(top_text: str, bottom_text: str, ordering: List[int], wagons: List[Tuple[int, Wagon]]) -> List[int]:
|
|
if len(top_text) == 0 and len(bottom_text) == 0 and len(ordering) > 0:
|
|
return ordering
|
|
if len(ordering) > MAX_LENGTH:
|
|
return []
|
|
|
|
for z, (i, wagon) in enumerate(wagons):
|
|
top = top_text + wagon[0]
|
|
bottom = bottom_text + wagon[1]
|
|
reordered = wagons[:z] + wagons[z+1:] + [(i, wagon)]
|
|
if top.startswith(bottom):
|
|
solution = solve(top[len(bottom):], "", [*ordering, i], reordered)
|
|
if len(solution) > 0:
|
|
return solution
|
|
if bottom.startswith(top):
|
|
solution = solve("", bottom[len(top):], [*ordering, i], reordered)
|
|
if len(solution) > 0:
|
|
return solution
|
|
|
|
return []
|
|
|
|
def check_ordering(wagons: List[Wagon], ordering: List[int]) -> bool:
|
|
"""Checks if the text on top matches the text on the bottom"""
|
|
sentence0 = ""
|
|
sentence1 = ""
|
|
for i in ordering:
|
|
assert 0 <= i < len(wagons), f"wagon index out of range: {i}"
|
|
sentence0 += wagons[i][0]
|
|
sentence1 += wagons[i][1]
|
|
return sentence0 == sentence1
|
|
|
|
|
|
def run_test_case(
|
|
wagons: List[Wagon],
|
|
example_ordering: List[int],
|
|
ordering: List[int],
|
|
) -> str:
|
|
if ordering == []:
|
|
if example_ordering == []:
|
|
return "PASS"
|
|
return "You falsely claimed it was impossible."
|
|
|
|
if check_ordering(wagons, ordering):
|
|
if example_ordering == []:
|
|
return "PASS (and the problem setter is a dummy)"
|
|
return "PASS"
|
|
|
|
sentence0 = ""
|
|
sentence1 = ""
|
|
for i in ordering:
|
|
assert 0 <= i < len(wagons), f"wagon index out of range: {i}"
|
|
sentence0 += wagons[i][0]
|
|
sentence1 += wagons[i][1]
|
|
|
|
# return f"Your selected wagons had different sentences on the top and bottom.\n{ordering}\n{sentence0!r}\n{sentence1!r}"
|
|
|
|
return "Your selected wagons had different sentences on the top and bottom."
|
|
|
|
|
|
def run_test_cases(solver: Callable[[List[Wagon]], List[int]]):
|
|
test_cases: Tuple[List[Wagon], List[int]] = [
|
|
([("Double cargo ", "Double "), ("wagon", "cargo wagon")], [0, 1]),
|
|
([("wagon", "cargo wagon"), ("Double cargo ", "Double ")], [1, 0]),
|
|
([("Hello ", "Hello"), ("World", " World")], [0, 1]),
|
|
([("", "")], [0]),
|
|
([(".", "-.."), (".-", ".."), ("--.", "--")], [2, 1, 2, 0]),
|
|
([("bc", "ca"), ("a", "ab"), ("ca", "a"), ("abc", "c")], [1, 0, 1, 3]),
|
|
([("yy", "y"), ("xy", "yx"), ("z", "yz")], [0, 1, 1, 1, 1, 1, 2]),
|
|
([("", "42"), ("42", "")], [1, 0]),
|
|
([("aaaa", ""), ("", "a")], [0, 1, 1, 1, 1]),
|
|
([], []),
|
|
([("0", "1")], []),
|
|
([("0", "1"), (" ", " ")], []),
|
|
([("", " ")], []),
|
|
([("aa", "a"), ("aa", "a"), ("b", "ab")], [0, 2]),
|
|
([("aa", ""), ("ab", ""), ("", "aa")], [0, 2]),
|
|
([("aa", ""), ("ab", "")], []),
|
|
([("ab", "a"), ("ab", "b")], []),
|
|
([("aaaa", ""), ("", "a")], [0, 1, 1, 1, 1]),
|
|
([("ab"*1000, ""), ("a", "ab"), ("b", "ab")], [0] + [1,2]*1000),
|
|
([("a"*(2**4*3**5), ""), ("", "aa"), ("", "aaa")], [0]+[1]*2**4 +[2]*3**5),
|
|
|
|
# ([("ab", "a"), ("ab", "b")], []),
|
|
# TODO: add more test cases
|
|
]
|
|
for i, (wagons, example_ordering) in enumerate(test_cases):
|
|
print(f"Test case {i:>2}: ", end="")
|
|
if not check_ordering(wagons, example_ordering):
|
|
sentence0 = ""
|
|
sentence1 = ""
|
|
for i in example_ordering:
|
|
assert 0 <= i < len(wagons), f"wagon index out of range: {i}"
|
|
sentence0 += wagons[i][0]
|
|
sentence1 += wagons[i][1]
|
|
print(sentence0)
|
|
print(sentence1)
|
|
|
|
|
|
assert check_ordering(wagons, example_ordering), "example is invalid, dummy."
|
|
|
|
print(run_test_case(wagons, example_ordering, solver(wagons)))
|
|
|
|
|
|
run_test_cases(my_solver)
|