// Datacenter Imprisonment (D) — v4: hybrid static-scan + adaptive brain.
//
// Maze model (from provided generator source): rooms at odd (r,c) are always
// empty, even/even cells always walls; the only unknowns are the E = 2m(m-1)
// door cells between adjacent rooms, m = (N+1)/2. Empty cells form a
// spanning tree of the rooms, so two sound inference rules apply everywhere:
//   cycle rule: endpoints already connected -> door closed (free)
//   cut rule:   component with exactly one undetermined incident door -> open
//
// HYBRID mode (E large relative to A): workers 1..A-1 statically scan
// diagonal stripes of the maze (band-near-diagonal doors first) with local
// inference, and send packed result bits to agent 0 at synchronized turn
// milestones (every D-th own turn), so agent 0 polls with zero misses.
// Agent 0 (the brain) maintains a global closure (DSU + per-component
// undetermined-door sets with cut/cycle cascades) plus side labels: rooms
// reachable from (1,1) are side S, from (N,N) side G. Every turn it either
// reads a due worker message or makes one FRESH adaptive query on the best
// frontier door (key = treedist + 4*manhattan-to-other-corner, alternating
// sides). It claims the moment S and G connect. Fresh queries are ~2.5x more
// informative per turn than static ones, which is where the speedup at
// low/mid agent counts comes from.
//
// STATIC mode (small E per agent): v2 protocol — contiguous Hilbert ranges,
// local inference, eager packed sends, staggered loads, DSU early claim.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

static const char* ALPH =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static int VAL[256];

static int N, A, ID, B;   // maze size, num agents, my id, max msg len
static ll m, E, L;        // rooms per side, candidate door edges, bits per msg

static string rd() {
    string s;
    if (!getline(cin, s)) exit(0);
    return s;
}
static void wr(const string& s) {
    cout << s << '\n';
    cout.flush();
}

struct EdgeI {
    int u, v;   // room indices (row*m+col)
    char dir;   // direction u -> v: 'R' or 'D'
    int qr, qc; // door cell to query
};

// Raw edge enumeration: for each room row r: (m-1) horizontal doors, then
// (for r < m-1) m vertical doors below the row.
static EdgeI edgeInfoRaw(ll idx) {
    ll rowBlock = 2 * m - 1;
    ll full = (m - 1) * rowBlock;
    ll r, off;
    if (idx < full) {
        r = idx / rowBlock;
        off = idx % rowBlock;
    } else {
        r = m - 1;
        off = idx - full;
    }
    if (off < m - 1) {
        int u = (int)(r * m + off);
        return {u, u + 1, 'R', (int)(2 * r + 1), (int)(2 * off + 2)};
    }
    ll c = off - (m - 1);
    int u = (int)(r * m + c);
    return {u, (int)(u + m), 'D', (int)(2 * r + 2), (int)(2 * c + 1)};
}

static bool probeCell(int qr, int qc) {
    char buf[48];
    snprintf(buf, sizeof buf, "? %d %d", qr, qc);
    wr(buf);
    string r = rd();
    return !r.empty() && r[0] == '1';
}

static ll hilbertD(int x, int y, int order) {
    ll d = 0;
    for (int s = order / 2; s > 0; s /= 2) {
        int rx = (x & s) > 0, ry = (y & s) > 0;
        d += (ll)s * s * ((3 * rx) ^ ry);
        if (ry == 0) {
            if (rx == 1) {
                x = s - 1 - x;
                y = s - 1 - y;
            }
            swap(x, y);
        }
    }
    return d;
}

// ---------------- shared path-building state ----------------

static vector<uint8_t> openR, openD;  // per room: right/down door open

static string buildPath() {
    int R = (int)(m * m), target = R - 1;
    vector<int> par(R, -1);
    vector<char> pd(R, 0);
    deque<int> q;
    par[0] = 0;
    q.push_back(0);
    int mm = (int)m;
    while (!q.empty()) {
        int u = q.front();
        q.pop_front();
        if (u == target) break;
        int r = u / mm, c = u % mm;
        if (c + 1 < mm && openR[u] && par[u + 1] < 0) {
            par[u + 1] = u; pd[u + 1] = 'R'; q.push_back(u + 1);
        }
        if (r + 1 < mm && openD[u] && par[u + mm] < 0) {
            par[u + mm] = u; pd[u + mm] = 'D'; q.push_back(u + mm);
        }
        if (c > 0 && openR[u - 1] && par[u - 1] < 0) {
            par[u - 1] = u; pd[u - 1] = 'L'; q.push_back(u - 1);
        }
        if (r > 0 && openD[u - mm] && par[u - mm] < 0) {
            par[u - mm] = u; pd[u - mm] = 'U'; q.push_back(u - mm);
        }
    }
    if (par[target] < 0) return "";
    string dirs;
    for (int cur = target; cur != 0; cur = par[cur]) dirs.push_back(pd[cur]);
    reverse(dirs.begin(), dirs.end());
    string out;
    out.reserve(dirs.size() * 2);
    for (char c : dirs) {
        out.push_back(c);
        out.push_back(c);
    }
    return out;
}

[[noreturn]] static void claimAndExit() {
    wr("! " + buildPath());
    exit(0);
}

// Local inference used while scanning (cycle + cut rules against my own
// discovered forest; out-of-my-knowledge doors stay counted as unknown).
struct Infer {
    vector<int> par;
    vector<ll> slot;
    Infer(int n) : par(n), slot(n, 0) {
        iota(par.begin(), par.end(), 0);
        int mm = (int)m;
        for (int r = 0; r < mm; ++r)
            for (int c = 0; c < mm; ++c)
                slot[r * mm + c] = (r > 0) + (r + 1 < mm) + (c > 0) + (c + 1 < mm);
    }
    int find(int x) {
        while (par[x] != x) x = par[x] = par[par[x]];
        return x;
    }
    void unite(int a, int b) {
        a = find(a);
        b = find(b);
        if (a == b) return;
        par[a] = b;
        slot[b] += slot[a];
    }
    pair<bool, bool> resolve(const EdgeI& e) {  // {open, queried}
        int ru = find(e.u), rv = find(e.v);
        if (ru == rv) {  // cycle rule
            slot[ru] -= 2;
            return {false, false};
        }
        if (slot[ru] == 1 || slot[rv] == 1) {  // cut rule
            slot[ru]--;
            slot[rv]--;
            unite(e.u, e.v);
            return {true, false};
        }
        bool open = probeCell(e.qr, e.qc);
        slot[ru]--;
        slot[rv]--;
        if (open) unite(e.u, e.v);
        return {open, true};
    }
};

// ================= STATIC protocol (v2) =================

static vector<ll> PERM;  // position -> raw edge index (Hilbert order)

static void buildPerm() {
    int order = 1;
    while (order < N + 2) order <<= 1;
    vector<pair<ll, ll>> keys(E);
    for (ll e = 0; e < E; ++e) {
        EdgeI ei = edgeInfoRaw(e);
        keys[e] = {hilbertD(ei.qr, ei.qc, order), e};
    }
    sort(keys.begin(), keys.end());
    PERM.resize(E);
    for (ll i = 0; i < E; ++i) PERM[i] = keys[i].second;
}

static inline EdgeI edgeInfoP(ll idx) { return edgeInfoRaw(PERM[idx]); }

static const ll SLACK = 1;

static ll qNeed(ll p) {  // conservative queries bound after inference
    int pct;
    if (p < 300) pct = 100;
    else if (p < 1000) pct = 96;
    else if (p < 3000) pct = 93;
    else if (p < 20000) pct = 91;
    else pct = 89;
    return (p * pct + 99) / 100;
}

static ll sendsFor(ll p) { return p ? (p + L - 1) / L : 0; }

static ll maxProbes(ll cap) {
    if (cap < 2) return 0;
    ll lo = 0, hi = cap * 2 + 4;
    while (lo < hi) {
        ll mid = (lo + hi + 1) / 2;
        if (qNeed(mid) + sendsFor(mid) <= cap)
            lo = mid;
        else
            hi = mid - 1;
    }
    return lo;
}

static ll maxRootProbes(ll budget) {
    if (budget <= 0) return 0;
    ll lo = 0, hi = budget * 2 + 4;
    while (lo < hi) {
        ll mid = (lo + hi + 1) / 2;
        if (qNeed(mid) <= budget)
            lo = mid;
        else
            hi = mid - 1;
    }
    return lo;
}

static bool feasible(ll T, vector<ll>& pout) {
    vector<ll> pmax(A, 0);
    for (int i = 1; i < A; ++i) {
        ll cap = T - 2 - SLACK - (ll)(A - 1 - i);
        pmax[i] = maxProbes(cap);
    }
    ll M = 0;
    for (int iter = 0; iter < 400; ++iter) {
        ll p0 = min(E, maxRootProbes(T - 1 - SLACK - M));
        ll rem = E - p0, Mact = 0;
        vector<ll> p(A, 0);
        p[0] = p0;
        for (int i = A - 1; i >= 1 && rem > 0; --i) {
            p[i] = min(pmax[i], rem);
            rem -= p[i];
            Mact += sendsFor(p[i]);
        }
        if (rem > 0) return false;
        if (qNeed(p0) + Mact + 1 + SLACK <= T) {
            pout = p;
            return true;
        }
        if (Mact <= M) return false;
        M = Mact;
    }
    return false;
}

static vector<ll> computeSchedule() {
    ll lo = 1, hi = E + 2LL * A + 64;
    vector<ll> best;
    while (lo <= hi) {
        ll mid = lo + (hi - lo) / 2;
        vector<ll> p;
        if (feasible(mid, p)) {
            best = p;
            hi = mid - 1;
        } else {
            lo = mid + 1;
        }
    }
    if (best.empty()) {
        best.assign(A, 0);
        best[0] = E;
    }
    return best;
}

static void runWorkerStatic(ll S, ll cnt) {
    if (cnt == 0) return;  // exit now; costs no turns
    Infer inf((int)(m * m));
    string buf;
    int acc = 0, nb = 0;
    for (ll t = 0; t < cnt; ++t) {
        EdgeI e = edgeInfoP(S + t);
        auto [open, queried] = inf.resolve(e);
        if (open) acc |= 1 << nb;
        if (++nb == 6) {
            buf.push_back(ALPH[acc]);
            acc = 0;
            nb = 0;
        }
        if (queried && (int)buf.size() >= B) {
            wr("> 0 " + buf.substr(0, B));
            buf.erase(0, B);
            rd();
        }
    }
    if (nb) buf.push_back(ALPH[acc]);
    while (!buf.empty()) {
        int take = min<int>(B, (int)buf.size());
        wr("> 0 " + buf.substr(0, take));
        buf.erase(0, take);
        rd();
    }
    // exit; merlin treats EOF like halt but without consuming a turn
}

struct DSU {
    vector<int> p;
    DSU(int n) : p(n) { iota(p.begin(), p.end(), 0); }
    int find(int x) {
        while (p[x] != x) x = p[x] = p[p[x]];
        return x;
    }
    void uni(int a, int b) {
        a = find(a);
        b = find(b);
        if (a != b) p[a] = b;
    }
};

// ---------- relay-aggregated static (tiny shares, many agents) ----------
// Roles: root 0; relays 1..R; leaves R+1..A-1, leaf i -> relay 1+((i-R-1)%R).
// Each leaf sends ONE message to its relay; each relay concatenates its own
// chars + its leaves' bodies (fixed order) and sends to root in ceil(chars/B)
// messages. Root receives only ~R messages instead of A-1.

static int relayCount() { return max(1, (int)lround(sqrt((double)(A - 1)))); }

// Measured: relay aggregation only pays when many agents each hold a tiny
// share (root receive turns dominate the static schedule).
static bool relayMode() {
    if (A < 35 || E < 500) return false;
    ll share = E / max(1, A - 1);
    return share <= 500;
}

struct RelayPlan {
    ll T;
    vector<ll> p;            // probes per agent id
    vector<int> relayOf;     // for leaves
    vector<vector<int>> group;  // relay -> leaf ids (fixed order)
};

static bool relayFeasible(ll T, RelayPlan& out) {
    int R = relayCount();
    out.relayOf.assign(A, 0);
    out.group.assign(R + 1, {});
    for (int i = R + 1; i < A; ++i) {
        int r = 1 + (i - R - 1) % R;
        out.relayOf[i] = r;
        out.group[r].push_back(i);
    }
    ll capBits = 6LL * B;  // per-leaf cap: whole message
    vector<ll> sr(R + 1, 1);
    ll total = 0, Mroot = 0;
    for (int iter = 0; iter < 4; ++iter) {
        out.p.assign(A, 0);
        total = 0;
        Mroot = 0;
        // stagger relay deadlines by the message counts of later relays
        vector<ll> Dr(R + 1);
        ll off = 0;
        for (int r = R; r >= 1; --r) {
            Dr[r] = T - 2 - off;
            off += sr[r];
        }
        bool changed = false;
        for (int r = 1; r <= R; ++r) {
            ll g = (ll)out.group[r].size();
            // relay: qNeed(p_r) + g receives + s_r sends <= Dr
            ll pr = min(capBits, maxRootProbes(Dr[r] - g - sr[r]));
            out.p[r] = max(0LL, pr);
            total += out.p[r];
            ll chars = (out.p[r] + 5) / 6;
            for (ll k = 1; k <= g; ++k) {
                int leaf = out.group[r][k - 1];
                // leaf's send must precede the relay's k-th receive:
                // qNeed(p_l) + 1 <= Dr - s_r - g + k - 1
                ll cap = Dr[r] - sr[r] - g + k - 2;
                ll pl = min(capBits, maxRootProbes(cap));
                out.p[leaf] = max(0LL, pl);
                total += out.p[leaf];
                chars += (out.p[leaf] + 5) / 6;
            }
            ll s = max<ll>(chars > 0 ? 1 : 0, (chars + B - 1) / B);
            if (s != sr[r]) {
                sr[r] = s;
                changed = true;
            }
            Mroot += s;
        }
        if (!changed) break;
    }
    ll p0 = maxRootProbes(T - 1 - SLACK - Mroot);
    out.p[0] = max(0LL, min(p0, E));
    total += out.p[0];
    if (total < E) return false;
    // trim overshoot (root keeps its full share; trim leaves from the end,
    // which only makes them finish earlier)
    ll excess = total - E;
    for (int i = A - 1; i >= 0 && excess > 0; --i) {
        ll cut = min(excess, out.p[i]);
        out.p[i] -= cut;
        excess -= cut;
    }
    out.T = T;
    return true;
}

static bool computeRelayPlan(RelayPlan& plan) {
    ll lo = 4, hi = E + 2LL * A + 64, best = -1;
    RelayPlan tmp;
    while (lo <= hi) {
        ll mid = lo + (hi - lo) / 2;
        if (relayFeasible(mid, tmp)) {
            best = mid;
            plan = tmp;
            hi = mid - 1;
        } else {
            lo = mid + 1;
        }
    }
    return best > 0;
}

// pack the resolution results of range [S, S+cnt) into chars (local inference)
static string scanRange(ll S, ll cnt) {
    string buf;
    if (cnt == 0) return buf;
    Infer inf((int)(m * m));
    int acc = 0, nb = 0;
    for (ll t = 0; t < cnt; ++t) {
        EdgeI e = edgeInfoP(S + t);
        auto [open, queried] = inf.resolve(e);
        (void)queried;
        if (open) acc |= 1 << nb;
        if (++nb == 6) {
            buf.push_back(ALPH[acc]);
            acc = 0;
            nb = 0;
        }
    }
    if (nb) buf.push_back(ALPH[acc]);
    return buf;
}

static void runLeafRelay(const RelayPlan& plan, const vector<ll>& S) {
    ll cnt = plan.p[ID];
    if (cnt == 0) return;
    string buf = scanRange(S[ID], cnt);
    int target = plan.relayOf[ID];
    wr("> " + to_string(target) + " " + buf);
    rd();
    // exit
}

static void runRelay(const RelayPlan& plan, const vector<ll>& S) {
    string own = scanRange(S[ID], plan.p[ID]);
    // receive one message from each of my leaves (those with p>0)
    vector<int> expect;
    for (int leaf : plan.group[ID])
        if (plan.p[leaf] > 0) expect.push_back(leaf);
    vector<string> got(A);
    vector<uint8_t> have(A, 0);
    int need = (int)expect.size();
    int miss = 0;
    while (need > 0 && miss < 600) {
        wr("< ?");
        string line = rd();
        if (!line.empty() && line[0] == '-') {
            ++miss;
            continue;
        }
        size_t sp = line.find(' ');
        if (sp == string::npos) continue;
        int snd = atoi(line.substr(0, sp).c_str());
        if (snd <= 0 || snd >= A || have[snd]) continue;
        have[snd] = 1;
        got[snd] = line.substr(sp + 1);
        --need;
    }
    // concatenate own + leaves in fixed order; pad missing leaves with '-'
    string body = own;
    for (int leaf : plan.group[ID]) {
        if (plan.p[leaf] == 0) continue;
        ll chars = (plan.p[leaf] + 5) / 6;
        if (have[leaf])
            body += got[leaf];
        else
            body.append((size_t)chars, '-');
    }
    for (size_t pos = 0; pos < body.size(); pos += B) {
        int take = (int)min<size_t>(B, body.size() - pos);
        wr("> 0 " + body.substr(pos, take));
        rd();
    }
    // exit
}

static void runRootRelay(const RelayPlan& plan, const vector<ll>& S) {
    vector<uint8_t> knownBit(E, 0);
    DSU dsu((int)(m * m));
    auto connectedSG = [&]() { return dsu.find(0) == dsu.find((int)(m * m - 1)); };
    auto applyBit = [&](ll idx, bool open) {
        if (idx >= E || knownBit[idx]) return;
        knownBit[idx] = 1;
        if (!open) return;
        EdgeI e = edgeInfoP(idx);
        if (e.dir == 'R')
            openR[e.u] = 1;
        else
            openD[e.u] = 1;
        dsu.uni(e.u, e.v);
    };

    {
        Infer inf((int)(m * m));
        for (ll t = 0; t < plan.p[0]; ++t) {
            EdgeI e = edgeInfoP(t);
            auto [open, queried] = inf.resolve(e);
            (void)queried;
            applyBit(t, open);
            if (open && connectedSG()) claimAndExit();
        }
    }

    int R = relayCount();
    vector<ll> msgsExp(A, 0), charsGot(A, 0);
    vector<string> stream(A);
    ll totMsgs = 0;
    for (int r = 1; r <= R; ++r) {
        ll chars = (plan.p[r] + 5) / 6;
        for (int leaf : plan.group[r]) chars += (plan.p[leaf] + 5) / 6;
        if (chars > 0) {
            msgsExp[r] = (chars + B - 1) / B;
            totMsgs += msgsExp[r];
        }
    }
    ll gotMsgs = 0;
    int miss = 0;
    while (gotMsgs < totMsgs) {
        wr("< ?");
        string line = rd();
        if (!line.empty() && line[0] == '-') {
            if (++miss > 600) break;
            continue;
        }
        miss = 0;
        size_t sp = line.find(' ');
        if (sp == string::npos) continue;
        int snd = atoi(line.substr(0, sp).c_str());
        if (snd < 1 || snd > R || msgsExp[snd] == 0) continue;
        --msgsExp[snd];
        ++gotMsgs;
        stream[snd] += line.substr(sp + 1);
        // decode this relay's stream so far: relay unit then leaf units,
        // each unit char-aligned at ceil(cnt/6) chars
        ll charBase = 0;
        auto decodeUnit = [&](ll Su, ll cnt) {
            for (ll i = 0; i < cnt; ++i) {
                ll ci = charBase + i / 6;
                if (ci >= (ll)stream[snd].size()) break;
                int v = VAL[(unsigned char)stream[snd][ci]];
                if (v < 0) continue;  // '-' padding: unknown, fallback later
                applyBit(Su + i, (v >> (i % 6)) & 1);
            }
            charBase += (cnt + 5) / 6;
        };
        decodeUnit(S[snd], plan.p[snd]);
        for (int leaf : plan.group[snd]) decodeUnit(S[leaf], plan.p[leaf]);
        if (connectedSG()) claimAndExit();
    }

    for (ll idx = 0; idx < E; ++idx) {  // fallback
        if (knownBit[idx]) continue;
        EdgeI e = edgeInfoP(idx);
        applyBit(idx, probeCell(e.qr, e.qc));
        if (connectedSG()) claimAndExit();
    }
    claimAndExit();
}

static void runRootStatic(const vector<ll>& p) {
    vector<uint8_t> knownBit(E, 0);
    DSU dsu((int)(m * m));
    auto connectedSG = [&]() { return dsu.find(0) == dsu.find((int)(m * m - 1)); };
    auto applyBit = [&](ll idx, bool open) {
        if (knownBit[idx]) return;
        knownBit[idx] = 1;
        if (!open) return;
        EdgeI e = edgeInfoP(idx);
        if (e.dir == 'R')
            openR[e.u] = 1;
        else
            openD[e.u] = 1;
        dsu.uni(e.u, e.v);
    };

    vector<ll> S(A + 1, 0);
    for (int i = 0; i < A; ++i) S[i + 1] = S[i] + p[i];

    {
        Infer inf((int)(m * m));
        for (ll t = 0; t < p[0]; ++t) {
            EdgeI e = edgeInfoP(t);
            auto [open, queried] = inf.resolve(e);
            (void)queried;
            applyBit(t, open);
            if (open && connectedSG()) claimAndExit();
        }
    }

    vector<ll> bitsGot(A, 0), msgsExp(A, 0);
    ll totMsgs = 0;
    for (int i = 1; i < A; ++i) {
        ll chars = (p[i] + 5) / 6;
        msgsExp[i] = (chars + B - 1) / B;
        totMsgs += msgsExp[i];
    }
    ll gotMsgs = 0;
    int miss = 0;
    while (gotMsgs < totMsgs) {
        wr("< ?");
        string line = rd();
        if (!line.empty() && line[0] == '-') {
            if (++miss > 300) break;  // a worker probably died
            continue;
        }
        miss = 0;
        size_t sp = line.find(' ');
        if (sp == string::npos) continue;
        int snd = atoi(line.substr(0, sp).c_str());
        if (snd < 1 || snd >= A) continue;
        ++gotMsgs;
        for (size_t ci = sp + 1; ci < line.size(); ++ci) {
            int v = VAL[(unsigned char)line[ci]];
            if (v < 0) continue;
            for (int b = 0; b < 6 && bitsGot[snd] < p[snd]; ++b, ++bitsGot[snd])
                applyBit(S[snd] + bitsGot[snd], (v >> b) & 1);
        }
        if (connectedSG()) claimAndExit();
    }

    for (ll idx = 0; idx < E; ++idx) {  // fallback
        if (knownBit[idx]) continue;
        EdgeI e = edgeInfoP(idx);
        applyBit(idx, probeCell(e.qr, e.qc));
        if (connectedSG()) claimAndExit();
    }
    claimAndExit();
}

// ================= HYBRID protocol =================

// Digger sub-mode (A <= 5): instead of static stripes, workers dig
// adaptively from the two corners (alternating sides, fanned by a tilt on
// r-c), with sound local inference, and report (door,result) finds to the
// brain every DIG_D turns. Much better than static stripes when there are
// too few workers to cover the maze. Message: finds sorted by door id,
// value = (delta<<1)|bit, delta-1 for subsequent, varint over the message
// alphabet (5 payload bits + continuation bit per char).

static bool DIG_MODE = false;
static const ll DIG_D = 64;

static void encVar(string& s, ll v) {
    for (;;) {
        int g = (int)(v & 31);
        v >>= 5;
        if (v) {
            s.push_back(ALPH[g | 32]);
        } else {
            s.push_back(ALPH[g]);
            break;
        }
    }
}

static void runWorkerDigger() {
    int sideS = (ID % 2) ? 1 : 2;
    int rank = (ID - 1) / 2;
    int tilt = ((rank % 2) ? 1 : -1) * ((rank + 1) / 2) * 8;
    int mm = (int)m;
    vector<int8_t> det(E, -1);
    vector<uint8_t> inComp(m * m, 0);
    vector<int> depth(m * m, 0);
    typedef pair<int, int> HE;
    priority_queue<HE, vector<HE>, greater<HE>> heap;
    vector<pair<int, int>> finds;
    uint64_t jmask = 0x9e3779b97f4a7c15ULL * (ID + 1);

    auto mdT = [&](int room) {
        int r = room / mm, c = room % mm;
        if (sideS == 1) return (mm - 1 - r) + (mm - 1 - c);
        return r + c;
    };
    auto jitter = [&](int door) {
        uint64_t h = ((uint64_t)door * 0x9e3779b97f4a7c15ULL) ^ jmask;
        h ^= h >> 33;
        return (int)(h & 7);
    };
    ll rowBlock = 2 * m - 1;
    auto pushRoom = [&](int room) {
        int r = room / mm, c = room % mm;
        auto add = [&](ll d, int nb) {
            if (det[d] != -1) return;
            int nr = nb / mm, nc = nb % mm;
            heap.push({depth[room] + 4 * mdT(nb) + tilt * (nr - nc) / 8 +
                           jitter((int)d),
                       (int)d});
        };
        if (c + 1 < mm) add((ll)r * rowBlock + c, room + 1);
        if (c > 0) add((ll)r * rowBlock + (c - 1), room - 1);
        if (r + 1 < mm) add((ll)r * rowBlock + (m - 1) + c, room + mm);
        if (r > 0) add((ll)(r - 1) * rowBlock + (m - 1) + c, room - mm);
    };
    int corner = sideS == 1 ? 0 : (int)(m * m - 1);
    inComp[corner] = 1;
    pushRoom(corner);

    ll myTurn = 0;
    for (;;) {
        if ((myTurn + 1) % DIG_D == 0) {
            // milestone: send as many finds as fit
            sort(finds.begin(), finds.end());
            string body;
            size_t fit = 0;
            ll prev = -1;
            for (size_t i = 0; i < finds.size(); ++i) {
                ll delta = prev < 0 ? finds[i].first : finds[i].first - prev - 1;
                string piece;
                encVar(piece, (delta << 1) | finds[i].second);
                if (body.size() + piece.size() > (size_t)B) break;
                body += piece;
                prev = finds[i].first;
                fit = i + 1;
            }
            finds.erase(finds.begin(), finds.begin() + fit);
            wr("> 0 " + body);
            rd();
            ++myTurn;
            continue;
        }
        // one query turn (inference resolutions are free and accumulate)
        bool queried = false;
        while (!heap.empty() && !queried) {
            auto [key, d] = heap.top();
            heap.pop();
            if (det[d] != -1) continue;
            EdgeI ei = edgeInfoRaw(d);
            int from = inComp[ei.u] ? ei.u : (inComp[ei.v] ? ei.v : -1);
            if (from < 0) continue;
            int nb = ei.u == from ? ei.v : ei.u;
            if (inComp[nb]) {  // cycle rule: closed for free
                det[d] = 0;
                finds.push_back({d, 0});
                continue;
            }
            bool op = probeCell(ei.qr, ei.qc);
            queried = true;
            det[d] = op ? 1 : 0;
            finds.push_back({d, op ? 1 : 0});
            if (op) {
                inComp[nb] = 1;
                depth[nb] = depth[from] + 1;
                pushRoom(nb);
            }
        }
        if (!queried) {
            wr(".");
            rd();
        }
        ++myTurn;
    }
}

// stripe assignment: doors sorted by diagonal projection (qr+qc, Hilbert),
// cut into A-1 equal chunks; each chunk scanned band-near-diagonal first
static void buildStripes(vector<vector<int>>& assign) {
    int horder = 1;
    while (horder < N + 2) horder <<= 1;
    int W = A - 1;
    vector<pair<pair<int, ll>, int>> pk(E);
    vector<int> QR(E), QC(E);
    for (ll e = 0; e < E; ++e) {
        EdgeI ei = edgeInfoRaw(e);
        QR[e] = ei.qr;
        QC[e] = ei.qc;
        pk[e] = {{ei.qr + ei.qc, hilbertD(ei.qr, ei.qc, horder)}, (int)e};
    }
    sort(pk.begin(), pk.end());
    assign.assign(A, {});
    for (int w = 1; w <= W; ++w) {
        ll s = (ll)(w - 1) * E / W, t = (ll)w * E / W;
        auto& lst = assign[w];
        lst.reserve(t - s);
        for (ll i = s; i < t; ++i) lst.push_back(pk[i].second);
        stable_sort(lst.begin(), lst.end(), [&](int x, int y) {
            return abs(QR[x] - QC[x]) < abs(QR[y] - QC[y]);
        });
    }
}

// Milestone interval (worker turns between sends), same on all agents.
// Chosen so (a) the brain's receive load stays ~<=12% of its turns,
// (b) a message's results fit in one body even with inference surplus,
// (c) the LAST milestone lands right around the expected scan end (~0.97 of
// the share needs a query turn), so final results are not held hostage.
static ll milestoneD() {
    ll W = A - 1;
    ll share = (E + W - 1) / W;
    ll cap = 6LL * B * 4 / 5;
    ll dbase = max<ll>(64, min(8LL * W, cap));
    ll target = max<ll>(1, share * 97 / 100);
    ll k = max<ll>(1, (target + dbase / 2) / dbase);
    ll D = (target + k - 1) / k;
    return max<ll>(48, min(D, cap));
}

static void runWorkerHybrid(const vector<int>& doors) {
    ll D = milestoneD();
    Infer inf((int)(m * m));
    string buf;         // complete chars ready to send
    int acc = 0, nb = 0;
    ll myTurn = 0;
    size_t cur = 0;
    ll resultsSent = 0, resultsTotal = (ll)doors.size();
    auto flushChar = [&]() {
        if (nb > 0) {
            buf.push_back(ALPH[acc]);
            acc = 0;
            nb = 0;
        }
    };
    while (resultsSent < resultsTotal) {
        if ((myTurn + 1) % D == 0) {
            // milestone send (whatever is buffered; pad-flush only when done)
            if (cur >= doors.size()) flushChar();
            int take = min<int>(B, (int)buf.size());
            wr("> 0 " + buf.substr(0, take));
            buf.erase(0, take);
            rd();
            ++myTurn;
            resultsSent = min(resultsTotal, resultsSent + 6LL * take);
            continue;
        }
        if (cur < doors.size()) {
            // resolve until a real query consumes the turn
            bool queried = false;
            while (cur < doors.size() && !queried) {
                EdgeI e = edgeInfoRaw(doors[cur]);
                auto [open, q] = inf.resolve(e);
                queried = q;
                if (open) acc |= 1 << nb;
                if (++nb == 6) {
                    buf.push_back(ALPH[acc]);
                    acc = 0;
                    nb = 0;
                }
                ++cur;
            }
            if (queried) {
                ++myTurn;
                continue;
            }
            // range finished purely by inference; loop to milestone wait
        }
        // nothing to scan; wait for next milestone
        wr(".");
        rd();
        ++myTurn;
    }
    // exit (EOF; costs no turn)
}

struct Brain {
    int mm, rooms, W;
    ll D;
    ll myTurn = 0;
    bool connected = false;

    // closure state
    vector<int> par;
    vector<int> rootSide;
    vector<vector<int>> members;
    vector<unordered_set<int>> doorSet;
    vector<int8_t> det;
    vector<int8_t> side;
    vector<int> dist;
    deque<int> work;

    typedef pair<int, int> HE;  // (key, door)
    priority_queue<HE, vector<HE>, greater<HE>> heap[3];
    int toggle = 1;
    vector<int> spec;
    size_t specCur = 0;

    // worker streams
    vector<vector<int>>* assign;
    vector<ll> got;       // results decoded per worker
    vector<int> accBits;  // (unused; chars are always full 6 bits)

    Brain(vector<vector<int>>* as) : assign(as) {
        mm = (int)m;
        rooms = mm * mm;
        W = A - 1;
        D = DIG_MODE ? DIG_D : milestoneD();
        par.resize(rooms);
        iota(par.begin(), par.end(), 0);
        rootSide.assign(rooms, 0);
        members.assign(rooms, {});
        for (int r = 0; r < rooms; ++r) members[r] = {r};
        doorSet.assign(rooms, {});
        det.assign(E, -1);
        side.assign(rooms, 0);
        dist.assign(rooms, 0);
        got.assign(A, 0);
        for (ll e = 0; e < E; ++e) {
            EdgeI ei = edgeInfoRaw(e);
            doorSet[ei.u].insert((int)e);
            doorSet[ei.v].insert((int)e);
        }
        // speculative order: corner blobs
        spec.resize(E);
        vector<pair<int, int>> keys(E);
        for (ll e = 0; e < E; ++e) {
            EdgeI ei = edgeInfoRaw(e);
            int ur = ei.u / mm, uc = ei.u % mm;
            int d1 = ur + uc;
            int d2 = (mm - 1 - ur) + (mm - 1 - uc);
            keys[e] = {min(d1, d2), (int)e};
        }
        sort(keys.begin(), keys.end());
        for (ll i = 0; i < E; ++i) spec[i] = keys[i].second;

        label(0, 1, 0);
        label(rooms - 1, 2, 0);
        rootSide[find(0)] = 1;
        rootSide[find(rooms - 1)] = 2;
    }
    int find(int x) {
        while (par[x] != x) x = par[x] = par[par[x]];
        return x;
    }
    int mdTo(int room, int s) {
        int r = room / mm, c = room % mm;
        if (s == 1) return (mm - 1 - r) + (mm - 1 - c);
        return r + c;
    }
    void pushDoors(int room) {
        int s = side[room];
        if (!s) return;
        int r = room / mm, c = room % mm;
        ll rowBlock = 2 * m - 1;
        auto add = [&](ll d, int nb) {
            if (det[d] != -1) return;
            heap[s].push({dist[room] + 4 * mdTo(nb, s), (int)d});
        };
        if (c + 1 < mm) add((ll)r * rowBlock + c, room + 1);
        if (c > 0) add((ll)r * rowBlock + (c - 1), room - 1);
        if (r + 1 < mm) add((ll)r * rowBlock + (m - 1) + c, room + mm);
        if (r > 0) add((ll)(r - 1) * rowBlock + (m - 1) + c, room - mm);
    }
    void label(int room, int s, int d) {
        if (side[room]) return;
        side[room] = (int8_t)s;
        dist[room] = d;
        pushDoors(room);
    }
    void setClosed(int e) {
        if (det[e] != -1) return;
        det[e] = 0;
        EdgeI ei = edgeInfoRaw(e);
        int ru = find(ei.u), rv = find(ei.v);
        doorSet[ru].erase(e);
        if (rv != ru) doorSet[rv].erase(e);
        work.push_back(ru);
        if (rv != ru) work.push_back(rv);
    }
    void setOpen(int e) {
        if (det[e] != -1) return;
        det[e] = 1;
        EdgeI ei = edgeInfoRaw(e);
        if (ei.dir == 'R')
            openR[ei.u] = 1;
        else
            openD[ei.u] = 1;
        int ru = find(ei.u), rv = find(ei.v);
        doorSet[ru].erase(e);
        if (rv != ru) doorSet[rv].erase(e);
        if (ru == rv) return;
        int su = rootSide[ru], sv = rootSide[rv];
        if (su && sv && su != sv) {
            connected = true;
            return;
        }
        int labRoom = side[ei.u] ? ei.u : (side[ei.v] ? ei.v : -1);
        int db = labRoom >= 0 ? dist[labRoom] + 1 : 0;
        int s = su ? su : sv;
        if (members[ru].size() + doorSet[ru].size() >
            members[rv].size() + doorSet[rv].size()) {
            swap(ru, rv);
            swap(su, sv);
        }
        par[ru] = rv;
        vector<int> internal;
        for (int d : doorSet[ru]) {
            auto it = doorSet[rv].find(d);
            if (it != doorSet[rv].end()) {
                doorSet[rv].erase(it);
                internal.push_back(d);
            } else
                doorSet[rv].insert(d);
        }
        doorSet[ru].clear();
        for (int d : internal)
            if (det[d] == -1) det[d] = 0;
        if (s) {
            if (su == 0)
                for (int room : members[ru]) label(room, s, db);
            else if (sv == 0)
                for (int room : members[rv]) label(room, s, db);
        }
        rootSide[rv] = s;
        if (members[ru].size() > members[rv].size()) members[ru].swap(members[rv]);
        for (int room : members[ru]) members[rv].push_back(room);
        members[ru].clear();
        work.push_back(rv);
    }
    void drain() {
        while (!work.empty()) {
            if (connected) return;
            int r = work.front();
            work.pop_front();
            r = find(r);
            if (doorSet[r].size() == 1) {
                int d = *doorSet[r].begin();
                if (det[d] == -1) setOpen(d);
            }
        }
    }
    void reveal(int e, bool open) {
        if (det[e] != -1) return;
        if (open)
            setOpen(e);
        else
            setClosed(e);
        if (!connected) drain();
    }
    // one fresh adaptive query; false if nothing left to ask
    bool freshQuery() {
        int door = -1;
        for (int attempt = 0; attempt < 2 && door < 0; ++attempt) {
            int s = toggle;
            toggle = 3 - toggle;
            auto& h = heap[s];
            while (!h.empty()) {
                auto [key, d] = h.top();
                h.pop();
                if (det[d] == -1) {
                    door = d;
                    break;
                }
            }
        }
        if (door < 0) {
            while (specCur < (size_t)E) {
                int d = spec[specCur++];
                if (det[d] == -1) {
                    door = d;
                    break;
                }
            }
        }
        if (door < 0) return false;
        EdgeI e = edgeInfoRaw(door);
        bool open = probeCell(e.qr, e.qc);
        ++myTurn;
        reveal(door, open);
        return true;
    }
    void applyMsg(int snd, const string& body) {
        if (snd < 1 || snd >= A) return;
        if (DIG_MODE) {
            // varint (delta<<1)|bit stream, doors ascending
            ll prev = -1;
            size_t pos = 0;
            while (pos < body.size()) {
                ll v = 0;
                int shift = 0;
                bool ok = false;
                while (pos < body.size()) {
                    int g = VAL[(unsigned char)body[pos++]];
                    if (g < 0) return;
                    v |= (ll)(g & 31) << shift;
                    shift += 5;
                    if (!(g & 32)) {
                        ok = true;
                        break;
                    }
                }
                if (!ok) return;
                ll bit = v & 1, delta = v >> 1;
                ll d = prev < 0 ? delta : prev + delta + 1;
                prev = d;
                if (d < 0 || d >= E) return;
                reveal((int)d, bit != 0);
                if (connected) return;
            }
            return;
        }
        auto& lst = (*assign)[snd];
        for (char ch : body) {
            int v = VAL[(unsigned char)ch];
            if (v < 0) continue;
            for (int b = 0; b < 6; ++b) {
                if (got[snd] >= (ll)lst.size()) break;
                int d = lst[got[snd]++];
                if (det[d] == -1) reveal(d, (v >> b) & 1);
                if (connected) return;
            }
        }
    }
    void run() {
        ll nextWave = 1;
        ll pending = 0;
        int miss = 0;
        bool deadMode = false;
        for (;;) {
            if (connected) claimAndExit();
            if (myTurn + 1 > nextWave * D) {
                // wave boundary crossed: all live workers sent at turn kD
                if (!deadMode) {
                    if (DIG_MODE)
                        pending += A - 1;  // diggers always send at milestones
                    else
                        for (int w2 = 1; w2 < A; ++w2)
                            if (got[w2] < (ll)(*assign)[w2].size()) ++pending;
                }
                ++nextWave;
            }
            if (pending > 0) {
                wr("< ?");
                string line = rd();
                ++myTurn;
                if (line.empty() || line[0] == '-') {
                    if (++miss > 2 * A + 8) {
                        // some worker died; stop expecting waves, go solo
                        deadMode = true;
                        pending = 0;
                        miss = 0;
                    }
                    continue;
                }
                miss = 0;
                --pending;
                size_t sp = line.find(' ');
                if (sp == string::npos) continue;
                int snd = atoi(line.substr(0, sp).c_str());
                applyMsg(snd, line.substr(sp + 1));
                continue;
            }
            if (freshQuery()) continue;
            // nothing to ask (everything determined or in workers' hands)
            if (!connected) {
                // all doors determined implies tree known & connected; if we
                // are here with undetermined doors, wait for next wave
                bool anyUndet = false;
                for (ll e2 = 0; e2 < E; ++e2)
                    if (det[e2] == -1) {
                        anyUndet = true;
                        break;
                    }
                if (!anyUndet) {
                    connected = true;
                    continue;
                }
                wr(".");
                rd();
                ++myTurn;
            }
        }
    }
};

// ================= main =================

// Measured boundary (official judge, 3 seeds per config): hybrid wins when
// each worker's stripe is large enough that the brain's fresh adaptive
// queries dominate the runtime; static wins when the scan is receive-bound.
static bool hybridMode() {
    if (A < 2) return false;
    ll share = E / max(1, A - 1);
    return share >= max<ll>(320, 16LL * A);
}

int main() {
    fill(begin(VAL), end(VAL), -1);
    for (int i = 0; i < 64; ++i) VAL[(unsigned char)ALPH[i]] = i;

    string first = rd();
    if (sscanf(first.c_str(), "%d %d %d %d", &N, &A, &ID, &B) != 4) return 0;
    m = (N + 1) / 2;
    E = 2 * m * (m - 1);
    L = 6LL * B;

    if (E == 0) {  // N == 1
        if (ID == 0) wr("! ");
        return 0;
    }

    openR.assign(m * m, 0);
    openD.assign(m * m, 0);

    // Measured (8 same-seed runs per config): diggers beat hybrid stripes
    // consistently only at A=3 (all N); at A=4..5 hybrid is equal or better.
    DIG_MODE = (A >= 2 && A <= 3 && E >= 300);
    if (DIG_MODE) {
        vector<vector<int>> assign;  // unused by the brain in digger mode
        if (ID == 0) {
            Brain brain(&assign);
            brain.run();
        } else {
            runWorkerDigger();
        }
    } else if (hybridMode()) {
        vector<vector<int>> assign;
        buildStripes(assign);
        if (ID == 0) {
            Brain brain(&assign);
            brain.run();
        } else {
            runWorkerHybrid(assign[ID]);
        }
    } else {
        buildPerm();
        RelayPlan plan;
        if (relayMode() && computeRelayPlan(plan)) {
            vector<ll> S(A + 1, 0);
            for (int i = 0; i < A; ++i) S[i + 1] = S[i] + plan.p[i];
            int R = relayCount();
            if (ID == 0)
                runRootRelay(plan, S);
            else if (ID <= R)
                runRelay(plan, S);
            else
                runLeafRelay(plan, S);
        } else {
            vector<ll> p = computeSchedule();
            if (ID == 0) {
                runRootStatic(p);
            } else {
                ll S = 0;
                for (int i = 0; i < ID; ++i) S += p[i];
                runWorkerStatic(S, p[ID]);
            }
        }
    }
    return 0;
}
