第18回 アルゴリズム実技検定 (PAST 018)

https://atcoder.jp/contests/past18-open

G - 二回の交換

$A = B$ のとき、$A_1, A_2$ を交換する操作を2回行えば良い。

$A \neq B$ のときを考える。 交換操作を行うする左端の index として $i < j$ なる $i,j$ を選ぶ。 このとき $(A_i, A_{i+1})$ の交換と $(A_j, A_{j+1})$ の交換操作は一般に非可換である。 一方で今回の問題では交換回数が2回なので次の2通りの置換のみ考えれば十分である。

\begin{eqnarray*} \begin{pmatrix} 1 & 2 & 3 \\ 3 & 1 & 2 \end{pmatrix} \begin{pmatrix} 1 & 2 & 3 \\ 2 & 3 & 1 \end{pmatrix} \end{eqnarray*}

題意を満たすような操作方法は以下の3通りである

(2) に関しては数列を反転させて (1) と同じ操作を行えば良い。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vint a(n), b(n);
    rep(i, n) cin >> a[i];
    rep(i, n) cin >> b[i];

    auto check = [](vint x, vint y) -> bool {
        int m = x.size();
        int cont = 0;
        int cnt = 0;

        if (x == y)
            return true;
        rep(i, m) {
            if (i + 1 < m)
                cont += x[i] == x[i + 1];
            if (x[i] == y[i])
                continue;
            if (i + 1 < m) {
                cnt++;
                swap(x[i], x[i + 1]);
            }
        }
        if (x != y)
            return false;
        if (cnt > 2)
            return false;
        if (cnt == 2)
            return true;
        if (cnt == 1 && cont > 0) {
            // 連続している部分を先んじて swap しておく or 操作が終わった後に連続している部分があったらその部分を swap する
            return true;
        }
        return false;
    };

    bool ok = false;
    ok = ok || check(a, b);
    reverse(all(a));
    reverse(all(b));
    ok = ok || check(a, b);
    yesno(ok);
}

H - Cを探せ

$(i, j)$ を左上、$(i+L, j+L)$ の正方形の中に C があるか判定する。 $(i,j) \in [i,i+L+1) \times [j,j+L+1)$ の領域に含まれる # の個数を $O$ 、$(i,j) \in [i+1,i+L) \times [j+1,j+L)$ に含まれる # の個数を $I$ とする。 $I = 0$ かつ $O = 3L-2$ のとき、レベル $L-2$ の C が存在する。

個数の判定は二次元累積和を用いて判定する。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int N;
    cin >> N;
    vector<string> grid(N);
    rep(i, N) cin >> grid[i];

    vvint cumsum(N + 1, vint(N + 1));
    rep(i, N) rep(j, N) cumsum[i + 1][j + 1] = grid[i][j] == '#';
    rep(i, N + 1) rep(j, N) cumsum[i][j + 1] += cumsum[i][j];
    rep(i, N) rep(j, N + 1) cumsum[i + 1][j] += cumsum[i][j];

    auto num_sharp = [&](int si, int sj, int ti, int tj) -> ll {
        ll ret = 0;
        ret = cumsum[ti][tj];
        ret -= cumsum[si][tj];
        ret -= cumsum[ti][sj];
        ret += cumsum[si][sj];
        return ret;
    };

    ll ans = 0;
    rep2(sz, 3, N + 1) {
        rep(i, N) {
            rep(j, N) {
                if (i + sz > N || j + sz > N)
                    continue;

                ll outside = num_sharp(i, j, i + sz, j + sz);
                ll inside = num_sharp(i + 1, j + 1, i + sz - 1, j + sz);

                if (inside)
                    continue;

                if (outside == sz * 3 - 2)
                    chmax(ans, sz - 2);
            }
        }
    }

    cout << ans << endl;
}

I - 改行コード

カーソルが右に動くのは type 1 のときだけで、その他のときは左に動く。

空文字列から始めて以下の操作を行う

カーソルが上に移動することはないから、$H = \text{deque の数}$, $W = (\text{現在見ている文字列の長さ}) + 1$ である。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int Q;
    cin >> Q;

    vector<deque<string>> editor(1);
    editor[0].push_back("");
    rep(_, Q) {
        int t;
        cin >> t;
        if (t == 1) {
            char c;
            cin >> c;
            editor.back()[0].push_back(c);
        } else if (t == 2) {
            editor.back().push_front("");
        } else {
            editor.push_back(deque<string>({""}));
        }
    }

    int H = editor.size(), W = editor.back().front().size() + 1;
    cout << H << ' ' << W << endl;
    rep(i, H) {
        cout << "# ";
        for (string s : editor[i])
            cout << s;
        cout << endl;
    }
}

J - 反転ゲーム

盤面の構成、どのマスにいるかということを合わせて一つの状態として見たとき、マスを移動すると別の状態に遷移しているとみなすことができる。よってこれは初期状態からマス目が全て白の状態への最短経路問題に帰着できる。 盤面の構成が $2^{HW}$ 通り、どのマスにいるかで $HW$ 通りで、合計 $2^{HW} HW$ 通りの状態がある。$H, W$ \leq 4$ なので多くとも状態数は $2^{16} \cdot 16 = 1048576$ 通りである。またある状態から次の状態への遷移方法はせいぜい4通りであるため辺の数もノード数と同じオーダーである。 移動のたびにコスト1 であるダイクストラ、または単に BFS で解くことができる。

盤面の状態は bit 列として持っておいて、移動先のマスに相当する bit 列と XOR を取ることで遷移先の状態を求めることができる。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    ll H, W;
    cin >> H >> W;
    vector<string> grid(H);
    rep(i, H) cin >> grid[i];

    ll start = 0;
    rep(i, H) rep(j, W) {
        ll x = i * W + j;
        if (grid[i][j] == '#')
            start |= 1ll << x;
    }

    vector<vvll> dist(1ll << (H * W), vvll(H, vll(W, INF)));
    dist[start][0][0] = 0;

    vint di = {0, 1, 0, -1};
    vint dj = {1, 0, -1, 0};

    using P = tuple<ll, ll, ll, ll>;
    queue<P> pq;
    pq.push({0, start, 0, 0});
    while (pq.size()) {
        auto [cost, now, i, j] = pq.front();
        pq.pop();

        if (dist[now][i][j] < cost)
            continue;

        rep(d, 4) {
            ll ni = i + di[d], nj = j + dj[d];
            if (clamp(ni, 0ll, H - 1) != ni || clamp(nj, 0ll, W - 1) != nj)
                continue;

            ll nx = now ^ (1ll << (ni * W + nj));
            if (dist[nx][ni][nj] <= cost + 1)
                continue;

            dist[nx][ni][nj] = cost + 1;
            pq.push({dist[nx][ni][nj], nx, ni, nj});
        }
    }

    ll ans = INF;
    rep(i, H) rep(j, W) chmin(ans, dist[0][i][j]);

    cout << ans << endl;
}

K - 2で割り切れる回数

L - 書き換え

遅延セグメント木を使う問題かと思ったが解説読んだらそうではなかったらしい

using S = long long;
using F = long long;

const S INF = 8e18;
const F ID = 8e18;

S op(S a, S b) {
    return std::max(a, b);
}
S e() {
    return -INF;
}
S mapping(F f, S x) {
    return (f == ID ? x : f);
}
F composition(F f, F g) {
    return (f == ID ? g : f);
}
F id() {
    return ID;
}

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int N, Q;
    cin >> N >> Q;

    vector<pair<string, ll>> sp(N);
    rep(i, N) {
        string s;
        ll p;
        cin >> s >> p;
        sp[i] = {s, p};
    }

    vector<pair<ll, int>> qsort;
    rep(i, Q) {
        ll t;
        cin >> t;
        qsort.push_back({t, i});
    }
    sort(all(qsort));

    vll q(Q);
    rep(i, Q) {
        q[i] = qsort[i].first;
    }

    lazy_segtree<S, op, e, F, mapping, composition, id> seg(q);

    int neg = 0;
    for (auto [s, p] : sp) {
        if (s == "NEGATE") {
            neg = 1 - neg;
            continue;
        }
        if (neg)
            p *= -1;
        int t = seg.max_right(0, [&](S x) -> ll {
            return x <= p;
        });
        if (s == "CHMIN" && neg || s == "CHMAX" && !neg) {
            seg.apply(0, t, p);
        } else {
            seg.apply(t, Q, p);
        }
    }

    vll ans(Q);
    rep(i, Q) {
        ll x = seg.get(i);
        ll sign = 1;
        if (neg)
            sign = -1;
        ans[qsort[i].second] = x * sign;
    }
    for (ll x : ans)
        cout << x << '\n';
}

M - グループ分け

解説読んで convolution の問題だと知りその時点で理解を諦めた