Problem Analysis
Each successful login reveals three characters from the passcode in the exact order they occur. A login like "abc" therefore encodes two ordering facts: a appears before b, and b appears before c. Gathering every such "X before Y" fact across all fifty logins yields a partial order over the distinct observed digits. The shortest passcode consistent with every observation is one that lists each observed digit exactly once, arranged so that no ordering fact is violated. (I assume each digit occurs only once in the passcode; if any digit repeated, the constraints could force a longer answer, but this keylog is consistent with a single occurrence of each digit.)
This is the canonical setting for topological sorting. I model the constraints as a directed acyclic graph (DAG): each distinct digit becomes a node, and an edge from X to Y means "X must precede Y". A topological sort linearises the nodes so that every edge points forward, which is exactly the passcode I want. Because every login succeeded, the facts cannot contradict each other - the graph has no cycles - so a valid ordering is guaranteed to exist. With at most ten distinct decimal digits and fifty logins of length three, the DAG has at most ten nodes and a handful of distinct edges. The work is O(D^2) in the number of distinct digits D, which is effectively constant here.
Solution Approach
Reverse Topological Sort by Repeated Sink Removal
Both the Python and C solutions implement the same algorithm. I chose repeated sink removal because it maps cleanly onto the constraint structure and is straightforward to reason about and verify.
Step 1 - Parse and deduplicate. I read the keylog, split it into lines, and discard duplicates. Seeing the same login twice contributes no new ordering information, so removing duplicates early keeps the inner loops lean. Python uses a set of strings, which deduplicates for free. The C version scans the lines already seen with strcmp - linear, but acceptable because the line count is bounded by a small constant (MAX_LINES = 200).
Step 2 - Build the successor graph. For each login and each consecutive pair at positions (i, i+1), I record that the character at i must precede the character at i+1. I store this as a successor set: for each digit, the set of digits that must follow it. Python uses a dict mapping each character to a set of successors. C uses a bitmask array: successor[i] is an unsigned integer whose bit j is set when digit j must follow digit i. Both versions derive the number of consecutive pairs from asked_characters - 1, so the same code generalises beyond exactly three characters per login.
Step 3 - Repeatedly remove a sink. A sink is a node with no remaining successors among the digits not yet placed. Such a node can safely sit furthest to the right of whatever ordering I build, because nothing still active must come after it. I find a sink, prepend it to the result string, then deactivate it and strip it from every other node's successor set. This is Kahn's algorithm run sink-first rather than source-first; prepending each removed sink naturally yields the passcode in left-to-right order. In Python, the loop scans the dictionary for a key whose successor set is empty and uses a for/else to detect the impossible cycle case. In C, I build an active_mask of all still-active nodes and look for an i where successor[i] & active_mask is zero - a single bitwise AND tells me whether any active successor remains.
Language differences. The Python solution is concise and reads almost like the algorithm description, thanks to dict and set. The C solution is more verbose but turns the "any active successor?" test into one bitwise operation and avoids dynamic allocation entirely. Both finish in negligible time and return the same answer, 73162890.