ICPC 2019
ICPC 2019

I. Karel the Robot

Did you know that the word “robot” is almost 100 years old? It was first introduced in 1920, in the science-fiction theatrical play R.U.R., written by Karel Čapek. As a tribute to this Czech writer, an educational programming language was named Karel many...

Updated May 21, 2026
Track ICPC
Year 2019
Statement Text + PDF
TeXC++Statement textStatement PDF

Problem Statement

Formatted from the contest statement text, with sample tests broken out into copyable blocks.

Time limit 10 seconds

Did you know that the word “robot” is almost 100 years old? It was first introduced in 1920, in the science-fiction theatrical play R.U.R., written by Karel Čapek. As a tribute to this Czech writer, an educational programming language was named Karel many years later at Stanford University. Your task is to implement an interpreter of a simplified version of this programming language. The Karel programming language controls a robot named Karel, who lives in a grid of unit squares. Some of the squares are free, while others contain a barrier. Karel always occupies one of the free squares and faces one of the four cardinal directions. The two basic commands are “move forward” and “turn left.” The language also provides simple conditional and looping statements. The main educational potential of the language lies in the possibility of defining new procedures for more complex tasks. Our simplified version of the language can be described by the following grammar:

<program>         :=     "" | <command> <program>
<command>         :=     "m" | "l" | <proc-call> |
                         "i" <condition> "(" <program> ")(" <program> ")" |
                         "u" <condition> "(" <program> ")"
<condition> :=           "b" | "n" | "s" | "e" | "w"
<proc-call> :=           <uppercase-letter>
<proc-def> :=            <uppercase-letter> "=" <program>

There are five types of commands:

m (“move forward”) advances Karel’s position by one grid square in its current heading, unless there
  is a barrier, in which case the command has no effect.
l (“turn left”) makes Karel turn left 90 degrees.
X where X is any uppercase letter, invokes the procedure named X.
i (“if”) followed by a single-letter condition, and two programs in parentheses. If the condition is
  satisfied, the first program is executed. Otherwise, the second program is executed.
u (“until”) followed by a single-letter condition, and a program in parentheses. If the condition is
  satisfied, nothing is done. Otherwise, the program is executed and then the command is repeated.

A condition can be either ‘b’, which is satisfied if and only if there is a barrier in the next square in Karel’s current heading, or one of the four directional letters ‘n’, ‘s’, ‘e’, or ‘w’, which is satisfied if and only if Karel’s current heading is north, south, east, or west, respectively. For instance, a simple program ub(m) can be understood to mean: “keep moving forward until there is a barrier,” while un(l) means “turn to the north.” A procedure definition R=lll defines a new procedure ‘R’ which effectively means “turn right.”

Input

The first line of input contains four integers r, c, d, and e, where r and c (1 ≤ r, c ≤ 40) are the dimensions of the grid in which Karel lives, d (0 ≤ d ≤ 26) is the number of procedure definitions, and e (1 ≤ e ≤ 10) is the number of programs to be executed.

Then follow r lines describing the grid (running north to south), each containing c characters (running west to east), each character being either ‘.’ (denoting a free square) or ‘#’ (denoting a barrier). All squares outside this given area are considered barriers, which means Karel may never leave the area. Each of the next d lines contains a procedure definition, associating a procedure name (one uppercase letter) with a program forming the procedure body. No procedure name is defined more than once. Procedure bodies may contain invocations of procedures that have not yet been defined. The last 2e lines describe the programs to be executed. Each such description consists of a pair of lines. The first line of each pair contains two integers i and j and a character h, where i (1 ≤ i ≤ r) is the row and j (1 ≤ j ≤ c) is the column of Karel’s initial position, and h ∈ {n, s, e, w} represents Karel’s initial heading. It is guaranteed that the initial position is a free square. The second line of each pair contains a program to be executed from that initial position. All procedure bodies and all programs to be executed are at least 1 and at most 100 characters long, syn- tactically correct, and only contain invocations of procedures that are defined. The lines with procedure definitions and programs to be executed contain no whitespace characters.

Output

For each program execution, output the final position of Karel after the complete program is executed from the respective initial position. Follow the format used to describe initial positions, that is, two numbers and a directional character. If a particular execution never terminates, output inf instead.

Sample Tests

Sample 1
Sample Input
 4 8 5 7
 .......#
 ..#....#
 .###...#
 .....###
 R=lll
 G=ub(B)
 B=ub(m)lib(l)(m)
 H=ib()(mmHllmll)
 I=III
 1 1 w
 1 1 e
 2 2 n
 2 6 w
 BR
 4 1 s
 ib(lib()(mmm))(mmmm)
 1 1 e
 2 2 s
Sample Output
1 1   w
inf
1 1   w
2 4   s
4 4   e
1 4   e
inf

Editorial

The solution write-up is rendered from the LaTeX source, with equations kept live through MathJax.

Key Observations

  • The robot configuration is finite: there are only $4rc \le 6400$ possible pairs \[ (\text{position}, \text{heading}). \]

  • The program text is also finite. If we represent every suffix of every parsed program body as a node, then execution is always ``at'' one such node.

  • Therefore the full execution state can be represented as \[ (\text{program node}, \text{robot configuration}), \] and there are at most $O(rcs)$ such states, where $s$ is the number of program nodes.

  • The language is deterministic. Hence if DFS evaluation ever revisits the same state while that state is still on the recursion stack, the execution has entered a cycle and will never terminate.

Algorithm

  1. Parse every procedure body and every query program into a linked representation of program suffixes. Each node stores one command and a pointer to the suffix executed afterwards. Nested programs inside if and until are parsed recursively and referenced by their root nodes.

  2. Precompute all robot transitions for the $4rc$ configurations: turning left, moving forward, and whether there is a barrier ahead.

  3. Let solve(node, state) mean: execute the program suffix rooted at node starting from robot configuration state.

  4. Evaluate this function with DFS and memoization.

    • For m and l, apply the corresponding transition and continue with next.

    • For a procedure call, recursively evaluate the procedure body, then continue with next.

    • For i cond (A)(B), choose the branch determined by the current state, execute it, then continue with next.

    • For u cond (A), if the condition is already true, continue with next; otherwise execute A and then recursively execute the same until node again.

    • Memoize the result of every state. A special marker in progress is used during DFS; touching such a state again means non-termination. enumerate

      Correctness Proof

      We prove that the algorithm returns the correct output for every query.

      Lemma 1.

      For every parsed program suffix $P$ and robot configuration $s$, the value computed by solve(P, s) is exactly the result of executing suffix $P$ from state $s$, unless the execution is infinite, in which case it returns inf.

      Proof.

      We inspect the first command of $P$. If it is m or l, the operational semantics applies one primitive robot action and then continues with the remaining suffix, which is exactly what the recursion does. If it is a procedure call, the language semantics executes the procedure body completely and then resumes with the caller's suffix; the recursive call followed by next matches this exactly. If it is an if, both the language and the algorithm choose the same branch from the current state, execute it, and then continue with the suffix. If it is an until, both either skip the loop when the condition is already true, or execute the body once and repeat the same until command. Thus every recursive case follows the language semantics exactly. □

      Lemma 2.

      If the DFS re-enters a state $(P,s)$ that is already marked in progress, then executing $P$ from $s$ does not terminate.

      Proof.

      The language is deterministic, so from a fixed state $(P,s)$ the future execution is uniquely determined. If the recursion reaches $(P,s)$ again before finishing the first evaluation of $(P,s)$, then the execution path from $(P,s)$ has returned to the same state and will repeat forever. Hence termination is impossible. □

      Lemma 3.

      If solve(P, s) returns a finite robot configuration $t$, then executing $P$ from $s$ terminates in exactly $t$.

      Proof.

      By Lemma 1 each recursive step mirrors the true execution semantics. By Lemma 2 the only executions declared infinite are genuine cycles. Therefore any state that is memoized with a finite result must terminate, and the returned configuration is the actual final configuration. □

      Theorem.

      For each query, the algorithm outputs inf iff the program never terminates; otherwise it outputs the correct final position and heading of Karel.

      Proof.

      Each query is evaluated as solve(root, start_state). If the answer is inf, Lemma 2 shows the execution is non-terminating. Otherwise Lemma 3 shows the returned configuration is exactly the true final state. Thus every printed answer is correct. □

      Complexity Analysis

      Let $s$ be the total number of parsed program nodes across all procedures and query programs. There are $4rc$ robot configurations, so at most $4rcs$ states. Each state is solved once and each transition does only $O(1)$ extra work. Therefore the running time is $O(rcs)$ and the memory usage is also $O(rcs)$.

      Implementation Notes

      • The parser naturally creates one node for every non-empty program suffix, which is exactly the granularity needed for memoization.

      • Empty programs can appear inside parentheses and are represented by a null node.

      • Because $4rc \le 6400$, the memo table fits comfortably in memory even when stored for all parsed nodes.

Code

C++ solution used for this page.

C++

Clean code view with a raw-file link when you want the original source.

Raw file
#include <bits/stdc++.h>
using namespace std;

namespace {

struct Node {
    char type = 0;
    char cond = 0;
    char proc = 0;
    int next = 0;
    int first = 0;
    int second = 0;
};

constexpr short INF_STATE = -1;
constexpr short UNVISITED = -2;
constexpr short IN_PROGRESS = -3;

int rows_count;
int cols_count;
vector<string> grid_cells;
vector<Node> nodes(1);
int proc_root[26];
vector<int> query_root;

int state_count;
vector<int> move_forward_state;
vector<int> turn_left_state;
vector<unsigned char> barrier_ahead;
vector<short> memo;

int encode_state(int r, int c, int dir) {
    return ((r * cols_count + c) << 2) | dir;
}

void decode_state(int state, int& r, int& c, int& dir) {
    dir = state & 3;
    state >>= 2;
    c = state % cols_count;
    r = state / cols_count;
}

bool check_condition(int state, char cond) {
    if (cond == 'b') {
        return barrier_ahead[state] != 0;
    }
    int dir = state & 3;
    if (cond == 'n') {
        return dir == 0;
    }
    if (cond == 'e') {
        return dir == 1;
    }
    if (cond == 's') {
        return dir == 2;
    }
    return dir == 3;
}

int parse_program(const string& s, int& pos);

int add_node(const Node& node) {
    nodes.push_back(node);
    return static_cast<int>(nodes.size()) - 1;
}

int parse_command_and_rest(const string& s, int& pos) {
    Node node;
    char ch = s[pos++];
    if (ch == 'm' || ch == 'l' || ('A' <= ch && ch <= 'Z')) {
        node.type = ch;
    } else if (ch == 'i') {
        node.type = 'i';
        node.cond = s[pos++];
        ++pos;  // '('
        node.first = parse_program(s, pos);
        ++pos;  // ')'
        ++pos;  // '('
        node.second = parse_program(s, pos);
        ++pos;  // ')'
    } else {
        node.type = 'u';
        node.cond = s[pos++];
        ++pos;  // '('
        node.first = parse_program(s, pos);
        ++pos;  // ')'
    }
    node.next = parse_program(s, pos);
    return add_node(node);
}

int parse_program(const string& s, int& pos) {
    if (pos >= static_cast<int>(s.size()) || s[pos] == ')') {
        return 0;
    }
    return parse_command_and_rest(s, pos);
}

int state_key(int node_id, int state) {
    return node_id * state_count + state;
}

short lookup_result(int node_id, int state) {
    if (node_id == 0) {
        return static_cast<short>(state);
    }
    return memo[state_key(node_id, state)];
}

short solve_state(int root_node, int root_state) {
    if (root_node == 0) {
        return static_cast<short>(root_state);
    }
    int root_key = state_key(root_node, root_state);
    if (memo[root_key] != UNVISITED) {
        return memo[root_key] == IN_PROGRESS ? INF_STATE : memo[root_key];
    }

    struct Frame {
        int node_id;
        int state;
        int stage;
        short temp;
    };

    vector<Frame> stack;
    stack.push_back({root_node, root_state, 0, 0});
    memo[root_key] = IN_PROGRESS;

    while (!stack.empty()) {
        Frame& frame = stack.back();
        int key = state_key(frame.node_id, frame.state);
        const Node& node = nodes[frame.node_id];

        auto request = [&](int child_node, int child_state, int next_stage) -> bool {
            short value = lookup_result(child_node, child_state);
            if (value == UNVISITED) {
                frame.stage = next_stage;
                memo[state_key(child_node, child_state)] = IN_PROGRESS;
                stack.push_back({child_node, child_state, 0, 0});
                return true;
            }
            if (value == IN_PROGRESS) {
                memo[key] = INF_STATE;
                stack.pop_back();
                return true;
            }
            frame.temp = value;
            frame.stage = next_stage;
            return false;
        };

        if (node.type == 'm') {
            if (frame.stage == 0) {
                if (request(node.next, move_forward_state[frame.state], 1)) {
                    continue;
                }
            }
            frame.temp = lookup_result(node.next, move_forward_state[frame.state]);
            memo[key] = frame.temp;
            stack.pop_back();
            continue;
        }

        if (node.type == 'l') {
            if (frame.stage == 0) {
                if (request(node.next, turn_left_state[frame.state], 1)) {
                    continue;
                }
            }
            frame.temp = lookup_result(node.next, turn_left_state[frame.state]);
            memo[key] = frame.temp;
            stack.pop_back();
            continue;
        }

        if ('A' <= node.type && node.type <= 'Z') {
            if (frame.stage == 0) {
                if (request(proc_root[node.type - 'A'], frame.state, 1)) {
                    continue;
                }
            }
            if (frame.stage == 1) {
                frame.temp = lookup_result(proc_root[node.type - 'A'], frame.state);
                if (frame.temp == INF_STATE) {
                    memo[key] = INF_STATE;
                    stack.pop_back();
                    continue;
                }
                if (request(node.next, frame.temp, 2)) {
                    continue;
                }
            }
            frame.temp = lookup_result(node.next, lookup_result(proc_root[node.type - 'A'], frame.state));
            memo[key] = frame.temp;
            stack.pop_back();
            continue;
        }

        if (node.type == 'i') {
            int branch = check_condition(frame.state, node.cond) ? node.first : node.second;
            if (frame.stage == 0) {
                if (request(branch, frame.state, 1)) {
                    continue;
                }
            }
            if (frame.stage == 1) {
                frame.temp = lookup_result(branch, frame.state);
                if (frame.temp == INF_STATE) {
                    memo[key] = INF_STATE;
                    stack.pop_back();
                    continue;
                }
                if (request(node.next, frame.temp, 2)) {
                    continue;
                }
            }
            frame.temp = lookup_result(node.next, lookup_result(branch, frame.state));
            memo[key] = frame.temp;
            stack.pop_back();
            continue;
        }

        if (frame.stage == 0) {
            if (check_condition(frame.state, node.cond)) {
                if (request(node.next, frame.state, 2)) {
                    continue;
                }
            } else {
                if (request(node.first, frame.state, 1)) {
                    continue;
                }
            }
        }
        if (frame.stage == 1) {
            frame.temp = lookup_result(node.first, frame.state);
            if (frame.temp == INF_STATE) {
                memo[key] = INF_STATE;
                stack.pop_back();
                continue;
            }
            if (request(frame.node_id, frame.temp, 2)) {
                continue;
            }
        }
        if (check_condition(frame.state, node.cond)) {
            frame.temp = lookup_result(node.next, frame.state);
        } else {
            frame.temp = lookup_result(frame.node_id, lookup_result(node.first, frame.state));
        }
        memo[key] = frame.temp;
        stack.pop_back();
    }

    return memo[root_key];
}

void build_transitions() {
    state_count = rows_count * cols_count * 4;
    move_forward_state.resize(state_count);
    turn_left_state.resize(state_count);
    barrier_ahead.resize(state_count);

    const int dr[4] = {-1, 0, 1, 0};
    const int dc[4] = {0, 1, 0, -1};
    for (int r = 0; r < rows_count; ++r) {
        for (int c = 0; c < cols_count; ++c) {
            for (int dir = 0; dir < 4; ++dir) {
                int state = encode_state(r, c, dir);
                turn_left_state[state] = encode_state(r, c, (dir + 3) & 3);

                int nr = r + dr[dir];
                int nc = c + dc[dir];
                bool blocked = nr < 0 || nr >= rows_count || nc < 0 || nc >= cols_count ||
                               grid_cells[nr][nc] == '#';
                barrier_ahead[state] = static_cast<unsigned char>(blocked);
                move_forward_state[state] = blocked ? state : encode_state(nr, nc, dir);
            }
        }
    }
}

void solve() {
    int definition_count, execution_count;
    cin >> rows_count >> cols_count >> definition_count >> execution_count;
    grid_cells.resize(rows_count);
    for (int i = 0; i < rows_count; ++i) {
        cin >> grid_cells[i];
    }

    fill(proc_root, proc_root + 26, 0);
    for (int i = 0; i < definition_count; ++i) {
        string line;
        cin >> line;
        int pos = 2;
        proc_root[line[0] - 'A'] = parse_program(line, pos);
    }

    vector<int> start_state(execution_count);
    query_root.resize(execution_count);
    for (int i = 0; i < execution_count; ++i) {
        int r, c;
        char heading;
        cin >> r >> c >> heading;
        string program;
        cin >> program;

        int dir = 0;
        if (heading == 'e') {
            dir = 1;
        } else if (heading == 's') {
            dir = 2;
        } else if (heading == 'w') {
            dir = 3;
        }
        start_state[i] = encode_state(r - 1, c - 1, dir);

        int pos = 0;
        query_root[i] = parse_program(program, pos);
    }

    build_transitions();
    memo.assign(static_cast<size_t>(nodes.size()) * state_count, UNVISITED);

    const char dir_name[4] = {'n', 'e', 's', 'w'};
    for (int i = 0; i < execution_count; ++i) {
        short result = solve_state(query_root[i], start_state[i]);
        if (result == INF_STATE) {
            cout << "inf\n";
            continue;
        }

        int r, c, dir;
        decode_state(result, r, c, dir);
        cout << (r + 1) << ' ' << (c + 1) << ' ' << dir_name[dir] << '\n';
    }
}

}  // namespace

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    solve();
    return 0;
}

Source Files and Assets

Raw files are still available here when you want the original TeX, C++, or statement assets.

Show raw files