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

/*
  Midnight Code Cup - Datacenter Imprisonment v3

  Improvement over the full-map baseline:
    - The official maze is a Kruskal tree on odd/odd room cells.
    - Potential corridors are the room-graph edges.
    - We query corridors in a diagonal-first order, because the corner-to-corner
      path is usually contained in a significantly smaller diagonal envelope.
    - Agents stream compact bit chunks to agent 0 during scanning.
    - Agent 0 maintains a DSU of discovered open corridors and claims as soon as
      (1,1) and (N,N) become connected, instead of waiting for the whole map.

  Correctness does not depend on the heuristic order: if early connection does
  not happen, the agents eventually query every possible corridor, reconstruct
  the exact tree path, and claim it.
*/

struct Edge {
    int u, v;      // room ids, 0-based in an M x M room grid
    int r, c;      // corridor cell, 1-based in the N x N maze
    int diag2;     // doubled distance from main diagonal, used for ordering
    int tie1, tie2;
};

static int N, NUM_AGENTS, AGENT_ID, MAX_MSG_LEN;
static int Mrooms;
static vector<Edge> edges;
static vector<vector<int>> known_adj;

[[noreturn]] static void leave_now() { exit(0); }

static string command_reply(const string &cmd) {
    cout << cmd << '\n' << flush;
    string line;
    if (!getline(cin, line)) leave_now();
    return line;
}

static bool query_cell(int r, int c) {
    string rep = command_reply("? " + to_string(r) + " " + to_string(c));
    return !rep.empty() && rep[0] == '1';
}

static void send_msg(int to, const string &body) {
    string rep = command_reply("> " + to_string(to) + " " + body);
    (void)rep;
}

static pair<int, string> recv_any() {
    string rep = command_reply("< ?");
    if (rep == "- -") return {-1, string()};
    size_t sp = rep.find(' ');
    if (sp == string::npos) return {-1, string()};
    int sender = -1;
    try { sender = stoi(rep.substr(0, sp)); }
    catch (...) { return {-1, string()}; }
    return {sender, rep.substr(sp + 1)};
}

[[noreturn]] static void halt_agent() {
    cout << "halt\n" << flush;
    leave_now();
}

[[noreturn]] static void claim_path(const string &path) {
    if (path.empty()) cout << "!\n" << flush;
    else cout << "! " << path << '\n' << flush;
    leave_now();
}

struct DSU {
    vector<int> p, sz;
    DSU() {}
    explicit DSU(int n) { init(n); }
    void init(int n) {
        p.resize(n); sz.assign(n, 1);
        iota(p.begin(), p.end(), 0);
    }
    int find(int x) {
        while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; }
        return x;
    }
    bool unite(int a, int b) {
        a = find(a); b = find(b);
        if (a == b) return false;
        if (sz[a] < sz[b]) swap(a, b);
        p[b] = a; sz[a] += sz[b];
        return true;
    }
    bool same(int a, int b) { return find(a) == find(b); }
};

static size_t assigned_count(size_t total, int who, int agents) {
    if ((size_t)who >= total) return 0;
    return (total - 1 - (size_t)who) / (size_t)agents + 1;
}

static void build_ordered_edges() {
    Mrooms = (N + 1) / 2;
    edges.clear();
    edges.reserve(2LL * Mrooms * max(0, Mrooms - 1));

    auto add_edge = [&](int u, int v, int r, int c) {
        int ur = u / Mrooms, uc = u % Mrooms;
        int vr = v / Mrooms, vc = v % Mrooms;
        int mid_r2 = ur + vr; // doubled room-coordinate midpoint
        int mid_c2 = uc + vc;
        Edge e;
        e.u = u; e.v = v; e.r = r; e.c = c;
        e.diag2 = abs(mid_r2 - mid_c2);
        // Tie-breakers keep each diagonal band deterministic and reasonably even.
        e.tie1 = abs((mid_r2 + mid_c2) - 2 * (Mrooms - 1));
        e.tie2 = max(abs(mid_r2 - (Mrooms - 1)), abs(mid_c2 - (Mrooms - 1)));
        edges.push_back(e);
    };

    for (int i = 0; i < Mrooms; ++i) {
        for (int j = 0; j < Mrooms; ++j) {
            int u = i * Mrooms + j;
            if (i + 1 < Mrooms) {
                int v = (i + 1) * Mrooms + j;
                // Room cells are (2*i+1, 2*j+1) in 1-based maze coordinates.
                add_edge(u, v, 2 * i + 2, 2 * j + 1);
            }
            if (j + 1 < Mrooms) {
                int v = i * Mrooms + (j + 1);
                add_edge(u, v, 2 * i + 1, 2 * j + 2);
            }
        }
    }

    sort(edges.begin(), edges.end(), [](const Edge &a, const Edge &b) {
        if (a.diag2 != b.diag2) return a.diag2 < b.diag2;
        if (a.tie1 != b.tie1) return a.tie1 < b.tie1;
        if (a.tie2 != b.tie2) return a.tie2 < b.tie2;
        if (a.r != b.r) return a.r < b.r;
        return a.c < b.c;
    });
}

static string reconstruct_room_path() {
    int total = Mrooms * Mrooms;
    int start = 0, goal = total - 1;
    if (start == goal) return string();

    vector<int> parent(total, -2);
    queue<int> q;
    parent[start] = -1;
    q.push(start);
    while (!q.empty() && parent[goal] == -2) {
        int v = q.front(); q.pop();
        for (int u : known_adj[v]) {
            if (parent[u] == -2) {
                parent[u] = v;
                q.push(u);
                if (u == goal) break;
            }
        }
    }
    if (parent[goal] == -2) return string();

    vector<int> rooms;
    for (int v = goal; v != -1; v = parent[v]) rooms.push_back(v);
    reverse(rooms.begin(), rooms.end());

    string path;
    path.reserve(2 * (rooms.size() - 1));
    for (size_t i = 1; i < rooms.size(); ++i) {
        int a = rooms[i - 1], b = rooms[i];
        int ar = a / Mrooms, ac = a % Mrooms;
        int br = b / Mrooms, bc = b % Mrooms;
        if (br == ar + 1 && bc == ac) path += "DD";
        else if (br == ar - 1 && bc == ac) path += "UU";
        else if (bc == ac + 1 && br == ar) path += "RR";
        else if (bc == ac - 1 && br == ar) path += "LL";
        else return string(); // Should never happen.
    }
    return path;
}

static void add_open_edge_and_maybe_claim(DSU &dsu, int edge_index) {
    const Edge &e = edges[(size_t)edge_index];
    known_adj[e.u].push_back(e.v);
    known_adj[e.v].push_back(e.u);
    dsu.unite(e.u, e.v);
    int start = 0, goal = Mrooms * Mrooms - 1;
    if (dsu.same(start, goal)) {
        string path = reconstruct_room_path();
        if (!path.empty() || N == 1) claim_path(path);
    }
}

static int choose_chunk_chars() {
    // Smaller mazes benefit from smaller chunks because the coordinator can stop
    // with less overshoot. Larger mazes need larger chunks to keep messaging
    // overhead under control. Each char carries 6 query bits.
    long long e = (long long)edges.size();
    int c;
    if (e <= 6000) c = 16;
    else if (e <= 25000) c = 32;
    else c = 64;
    if (MAX_MSG_LEN < c) c = max(1, MAX_MSG_LEN);
    return c;
}

static void worker_main() {
    const int chunk_chars = choose_chunk_chars();
    const int chunk_bits = 6 * chunk_chars;
    int val = 0, bit = 0, bits_in_chunk = 0;
    string body;
    body.reserve((size_t)chunk_chars);

    auto flush_body = [&]() {
        if (!body.empty()) {
            send_msg(0, body);
            body.clear();
        }
    };

    size_t local_bit_index = 0;
    for (size_t global = (size_t)AGENT_ID; global < edges.size(); global += (size_t)NUM_AGENTS) {
        bool open = query_cell(edges[global].r, edges[global].c);
        if (open) val |= (1 << bit);
        ++bit;
        ++bits_in_chunk;
        ++local_bit_index;
        if (bit == 6) {
            body.push_back(char(33 + val)); // 33..96: printable, no spaces.
            bit = 0;
            val = 0;
        }
        if (bits_in_chunk >= chunk_bits) {
            if (bit != 0) {
                body.push_back(char(33 + val));
                bit = 0;
                val = 0;
            }
            flush_body();
            bits_in_chunk = 0;
        }
    }
    if (bit != 0) body.push_back(char(33 + val));
    flush_body();
    halt_agent();
}

static void apply_message_bits(
    int sender,
    const string &body,
    vector<int> &seen_bits,
    DSU &dsu
) {
    if (sender < 0 || sender >= NUM_AGENTS) return;
    for (unsigned char ch : body) {
        int val = int(ch) - 33;
        if (val < 0 || val >= 64) continue;
        for (int b = 0; b < 6; ++b) {
            size_t global = (size_t)sender + (size_t)seen_bits[sender] * (size_t)NUM_AGENTS;
            if (global < edges.size()) {
                if ((val >> b) & 1) add_open_edge_and_maybe_claim(dsu, (int)global);
            }
            ++seen_bits[sender];
        }
    }
}

static void coordinator_main() {
    int total_rooms = Mrooms * Mrooms;
    known_adj.assign((size_t)total_rooms, {});
    DSU dsu(total_rooms);

    const int chunk_chars = choose_chunk_chars();
    const int chunk_bits = 6 * chunk_chars;
    const size_t total_edges = edges.size();

    vector<int> seen_bits(NUM_AGENTS, 0);
    vector<int> expected_chars(NUM_AGENTS, 0), got_chars(NUM_AGENTS, 0);
    for (int id = 0; id < NUM_AGENTS; ++id) {
        size_t bits = assigned_count(total_edges, id, NUM_AGENTS);
        expected_chars[id] = (int)((bits + 5) / 6);
    }
    got_chars[0] = expected_chars[0];

    int remaining_chars = 0;
    for (int id = 1; id < NUM_AGENTS; ++id) remaining_chars += expected_chars[id];

    size_t my_local_bit = 0;
    size_t my_total_bits = assigned_count(total_edges, 0, NUM_AGENTS);

    auto query_one_mine = [&]() -> bool {
        if (my_local_bit >= my_total_bits) return false;
        size_t global = my_local_bit * (size_t)NUM_AGENTS; // sender 0's global index
        if (global < total_edges) {
            bool open = query_cell(edges[global].r, edges[global].c);
            if (open) add_open_edge_and_maybe_claim(dsu, (int)global);
        }
        ++my_local_bit;
        ++seen_bits[0];
        return true;
    };

    while (true) {
        // Query one local chunk. Workers do the same in parallel.
        int did = 0;
        while (did < chunk_bits && query_one_mine()) ++did;

        // One bridge turn lets workers' just-sent messages become readable.
        // Use it for useful local work if possible; otherwise a no-op is safe.
        if (remaining_chars > 0) {
            if (!query_one_mine()) {
                string rep = command_reply(".");
                (void)rep;
            }
        }

        // Drain roughly one chunk from every worker. If the queue is empty, stop
        // draining and continue scanning; later chunks/messages will be read in
        // subsequent iterations.
        int reads = max(1, NUM_AGENTS - 1);
        for (int k = 0; k < reads && remaining_chars > 0; ++k) {
            auto [sender, body] = recv_any();
            if (sender < 0) break;
            if (sender == 0 || sender >= NUM_AGENTS) continue;
            int useful = min<int>((int)body.size(), expected_chars[sender] - got_chars[sender]);
            if (useful <= 0) continue;
            if (useful < (int)body.size()) body.resize((size_t)useful);
            apply_message_bits(sender, body, seen_bits, dsu);
            got_chars[sender] += useful;
            remaining_chars -= useful;
        }

        if (my_local_bit >= my_total_bits && remaining_chars <= 0) break;
    }

    // If no early claim happened, all corridors are known by now, so this should work.
    string path = reconstruct_room_path();
    if (!path.empty() || N == 1) claim_path(path);
    halt_agent();
}

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

    if (!(cin >> N >> NUM_AGENTS >> AGENT_ID >> MAX_MSG_LEN)) return 0;
    string dummy;
    getline(cin, dummy);

    if (N <= 0 || NUM_AGENTS <= 0 || AGENT_ID < 0 || AGENT_ID >= NUM_AGENTS) return 0;
    if (N == 1) {
        if (AGENT_ID == 0) claim_path("");
        halt_agent();
    }

    build_ordered_edges();
    if (AGENT_ID == 0) coordinator_main();
    else worker_main();
    return 0;
}
