第九回 アルゴリズム実技検定

A - アトラクション

問題文の通りに判定する。

h, w = map(int, input().split())
a, b = map(int, input().split())
if a >= h and b <= w:
    print("Yes")
else:
    print("No")

B - 穴の開いた硬貨

全ての硬貨が互いに倍数・約数関係にあるので、大きい順に貪欲に使うことで硬貨の枚数を最小化できる。

従って、各 $C _ i := B _ i - A _ i$ に対して $C _ i \bmod 100 \geq 50$ なら $50$ 円玉が一枚、$C _ i \bmod 10 \geq 5$ なら $5$ 円玉が一枚必要となる。

ans = 0
for _ in range(int(input())):
    a, b = map(int, input().split())
    r = b - a
    ans += r % 100 >= 50
    ans += r % 10 >= 5
print(ans)

C - 最速正解者

問題文の通りに実装する。Python だと ordchr で都度変換してると面倒なので dict で処理すると楽そう。

d = {}
for i in range(int(input())):
    p, v = input().split()
    if v == 'WA' or p in d:
        continue
    d[p] = i + 1
for key in 'ABCDEF':
    print(d[key])

D - 試験

問題文で与えられている通りに順列 $P = (1, 2, \ldots, N)$ をソートする。

#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>

int main() {
    int n;
    std::cin >> n;
    std::vector<int> a(n), b(n);
    for (auto &e : a) std::cin >> e;
    for (auto &e : b) std::cin >> e;

    std::vector<int> p(n);
    std::iota(p.begin(), p.end(), 0);

    std::sort(
        p.begin(), p.end(),
        [&](int i, int j) {
            if (a[i] + b[i] != a[j] + b[j]) {
                return a[i] + b[i] > a[j] + b[j];
            } else if (a[i] != a[j]) {
                return a[i] > a[j];
            } else {
                return i < j;
            }
        }
    );

    for (int i = 0; i < n; ++i) {
        std::cout << p[i] + 1 << " \n"[i == n - 1];
    }
    return 0;
}

E - キーボード

問題文通りに注意深く実装する。

#include <iostream>

int main() {
    std::string s;
    std::cin >> s;
    int n = s.size();

    long long ans = 0;

    char p;

    for (int i = 0; i < n; ++i) {
        if (i == 0) {
            ans += 500;
        } else if (p == s[i]) {
            ans += 301;
        } else if (('1' <= p and p <= '5') == ('1' <= s[i] and s[i] <= '5')) {
            ans += 210;
        } else {
            ans += 100;
        }
        p = s[i];
    }
    std::cout << ans << std::endl;

    return 0;
}

F - 将棋のように

幅優先探索。予め可能な遷移 $(dx,dy)$ を列挙しておくと楽?

#include <iostream>
#include <deque>
#include <vector>

int main() {
    int a, b;
    std::cin >> a >> b;
    --a, --b;

    std::vector<std::pair<int, int>> trans;
    for (int i = -1; i <= +1; ++i) {
        std::string s;
        std::cin >> s;
        for (int j = -1; j <= +1; ++j) {
            if (s[j + 1] == '#') {
                trans.emplace_back(i, j);
            }
        }
    }

    std::vector vis(9, std::vector(9, false));
    vis[a][b] = true;
    int ans = 0;

    std::deque<std::pair<int, int>> dq { { a, b } };
    while (dq.size()) {
        auto [i, j] = dq.front();
        dq.pop_front();
        ++ans;
        for (auto [dx, dy] : trans) {
            int ni = i + dx, nj = j + dy;
            if (ni < 0 or ni >= 9 or nj < 0 or nj >= 9 or vis[ni][nj]) continue;
            vis[ni][nj] = true;
            dq.emplace_back(ni, nj);
        }
    }
    std::cout << ans << std::endl;
    return 0;
}

G - 連結

Dynamic Connectivity に見えてぎょっとするけど、制約が緩いので毎回愚直に処理してよい。

#include <iostream>
#include <atcoder/dsu>

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

    int n, q;
    std::cin >> n >> q;
    std::vector<std::vector<int>> g(n);
    while (q --> 0) {
        int t, u, v;
        std::cin >> t >> u >> v;
        --u, --v;
        if (t == 1) {
            if (auto it_u = std::find(g[u].begin(), g[u].end(), v); it_u != g[u].end()) {
                auto it_v = std::find(g[v].begin(), g[v].end(), u);
                g[u].erase(it_u);
                g[v].erase(it_v);
            } else {
                g[u].push_back(v);
                g[v].push_back(u);
            }
        } else {
            atcoder::dsu uf(n);
            for (int i = 0; i < n; ++i) {
                for (int j : g[i]) {
                    uf.merge(i, j);
                }
            }
            if (uf.same(u, v)) {
                std::cout << "Yes\n";
            } else {
                std::cout << "No\n";
            }
        }
    }
    return 0;
}

H - 最長非共通部分列

最長共通部分列 (LCS) と同様の DP で解くことが出来る。

$S[:k]$ で $S$ の先頭 $k$ 文字からなる文字列、つまり $S[ : k ] = S _ 0 S _ 1 \cdots S _ { k - 1 } $ とする。

$$ \mathrm{dp}[i][j] := S[:i], T[:j] に対する問題の答え $$

で定義すれば、$\mathrm{dp}[0][0]=0$ で初期化でき、遷移は以下のように計算出来る。

$$ \mathrm{dp}[i][j] = \begin{cases} \mathrm{dp}[i - 1][j - 1] + 1 & \text{if\; $S _ { i - 1 } \neq T _ { j - 1 } $} \\ \max \{ \mathrm{dp}[i - 1][j], \mathrm{dp}[i][j - 1] \} & \text{otherwise} \end{cases} $$

状態数は $O(NM)$、遷移は $O(1)$ なので、全体 $O(NM)$ でこの問題を解くことが出来た。

#include <iostream>
#include <vector>

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

    std::string s, t;
    std::cin >> s >> t;

    const int n = s.size(), m = t.size();

    std::vector dp(n + 1, std::vector(m + 1, 0));

    for (int i = 0; i <= n; ++i) for (int j = 0; j <= m; ++j) {
        if (i and j and s[i - 1] != t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
        if (i) dp[i][j] = std::max(dp[i][j], dp[i - 1][j]);
        if (j) dp[i][j] = std::max(dp[i][j], dp[i][j - 1]);
    }
    
    std::cout << dp[n][m] << std::endl;

    return 0;
}

I - 直通エレベーター

$1$ 階と $N$ 階とエレベーターがある階だけを残したグラフを構築して最短路問題を解けばよい。頂点数は高々 $2M+2$、辺数は高々 $3M+1$ なので Dijkstra 法を用いれば全体 $O(M \log M)$ でこの問題が解ける。

#include <algorithm>
#include <iostream>
#include <limits>
#include <queue>
#include <tuple>
#include <vector>

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

    long long n;
    int m;
    std::cin >> n >> m;

    std::vector<long long> xs { 1, n };

    std::vector<std::tuple<long long, long long, long long>> ps(m);
    for (int i = 0; i < m; ++i) {
        long long u, v, c;
        std::cin >> u >> v >> c;
        ps[i] = { u, v, c };
        xs.push_back(u);
        xs.push_back(v);
    }

    std::sort(xs.begin(), xs.end());
    xs.erase(std::unique(xs.begin(), xs.end()), xs.end());

    const int k = xs.size();

    std::vector<std::vector<std::pair<int, long long>>> g(k);

    for (int i = 0; i < k - 1; ++i) {
        long long cost = xs[i + 1] - xs[i];
        g[i].emplace_back(i + 1, cost);
        g[i + 1].emplace_back(i, cost);
    }

    for (auto &[u, v, w] : ps) {
        u = std::lower_bound(xs.begin(), xs.end(), u) - xs.begin();
        v = std::lower_bound(xs.begin(), xs.end(), v) - xs.begin();
        g[u].emplace_back(v, w);
        g[v].emplace_back(u, w);
    }

    using state = std::pair<long long, int>;
    std::priority_queue<state, std::vector<state>, std::greater<state>> pq;
    std::vector<long long> dist(k, std::numeric_limits<long long>::max());

    pq.emplace(dist[0] = 0, 0);
    while (pq.size()) {
        auto [du, u] = pq.top();
        pq.pop();
        if (du != dist[u]) continue;
        for (auto [v, cost] : g[u]) {
            if (long long dv = du + cost; dv < dist[v]) {
                pq.emplace(dist[v] = dv, v);
            }
        }
    }

    std::cout << dist[k - 1] << std::endl;
    return 0;
}

J - 回転と反転

各操作でマス $(i,j)$ がどう移動するかを考える。

  • 時計回り

$$ \begin{pmatrix} 0 & 1 & 0 \\ -1 & 0 & n - 1 \\ 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} i \\ j \\ 1 \end{pmatrix} = \begin{pmatrix} n - j - 1 \\ i \\ 1 \end{pmatrix} $$

  • 反時計回り

$$ \begin{pmatrix} 0 & -1 & n - 1 \\ 1 & 0 & 0 \\ 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} i \\ j \\ 1 \end{pmatrix} = \begin{pmatrix} j \\ n - i - 1 \\ 1 \end{pmatrix} $$

  • 上下反転

$$ \begin{pmatrix} -1 & 0 & n - 1 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} i \\ j \\ 1 \end{pmatrix} = \begin{pmatrix} n - i - 1 \\ j \\ 1 \end{pmatrix} $$

  • 左右反転

$$ \begin{pmatrix} 1 & 0 & 0 \\ 0 & -1 & n - 1 \\ 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} i \\ j \\ 1 \end{pmatrix} = \begin{pmatrix} i \\ n - j - 1 \\ 1 \end{pmatrix} $$

従って、これらの行列を用いて各時点の変換行列を保持すればよい。

#include <array>
#include <iostream>
#include <vector>

using Row = std::array<int, 3>;
using Vec = Row;
using Matrix = std::array<Row, 3>;

Matrix operator*(const Matrix &A, const Matrix &B) {
    Matrix C{};
    for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) for (int k = 0; k < 3; ++k) {
        C[i][k] += A[i][j] * B[j][k];
    }
    return C;
}

Vec operator*(const Matrix &A, const Vec &x) {
    Vec y{};
    for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) {
        y[i] += A[i][j] * x[j];
    }
    return y;
}

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

    int n, q;
    std::cin >> n >> q;

    Matrix CW_inv {
        Row { 0, -1, n - 1 },
        Row { 1, 0, 0 },
        Row { 0, 0, 1 }
    };
    Matrix CCW_inv {
        Row { 0, 1, 0 },
        Row { -1, 0, n - 1 },
        Row { 0, 0, 1 }
    };
    Matrix UD_inv {
        Row { -1, 0, n - 1 },
        Row { 0, 1, 0 },
        Row { 0, 0, 1 }
    };
    Matrix LR_inv {
        Row { 1, 0, 0 },
        Row { 0, -1, n - 1 },
        Row { 0, 0, 1 }
    };

    Matrix A {
        Row { 1, 0, 0 },
        Row { 0, 1, 0 },
        Row { 0, 0, 1 }
    };

    std::vector ans(n, std::vector(n, 0));

    while (q --> 0) {
        int t;
        std::cin >> t;
        if (t == 1) {
            int x, y;
            std::cin >> x >> y;
            --x, --y;
            Vec pos = A * Vec { x, y, 1 };
            ans[pos[0]][pos[1]] ^= 1;
        } else if (t == 2) {
            char c;
            std::cin >> c;
            A = A * (c == 'A' ? CW_inv : CCW_inv);
        } else if (t == 3) {
            char c;
            std::cin >> c;
            A = A * (c == 'A' ? UD_inv : LR_inv);
        }
    }

    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            Vec pos = A * Vec { i, j, 1 };
            std::cout << ans[pos[0]][pos[1]];
        }
        std::cout << '\n';
    }
    return 0;
}

(これ難しくない?)

K - ガソリンスタンド

ガソリンスタンドの個数が少ないので、どのガソリンスタンドを経由するかを全探索することを考える。$a _ i$ を経由する場合の $s, t$ 間の最短距離 $d _ i (s, t)$ は $d _ i (s, t) = \mathrm{dist}(a _ i, s) + \mathrm{dist}(a _ i, t)$ と書けるので、各 $i = 1,\ldots, K$ に対して $\mathrm{dist}(a _ i, v)$ を計算しておくことにすれば前計算 $O(K (N + M))$、クエリ $O(K)$ でこの問題を解くことが出来る。全体の計算量は $O(K (N + M + Q))$。

#include <deque>
#include <iostream>
#include <limits>
#include <vector>

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

    int n, m, q, k;
    std::cin >> n >> m >> q >> k;
    std::vector<int> a(k);
    for (int &e : a) {
        std::cin >> e;
        --e;
    }
    std::vector<std::vector<int>> g(n);
    for (int i = 0; i < m; ++i) {
        int u, v;
        std::cin >> u >> v;
        --u, --v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    std::vector dist(k, std::vector(n, -1));
    for (int i = 0; i < k; ++i) {
        dist[i][a[i]] = 0;
        std::deque<int> dq { a[i] };
        while (dq.size()) {
            int u = dq.front();
            dq.pop_front();
            for (int v : g[u]) {
                if (dist[i][v] < 0) {
                    dist[i][v] = dist[i][u] + 1;
                    dq.push_back(v);
                }
            }
        }
    }

    while (q --> 0) {
        int s, t;
        std::cin >> s >> t;
        --s, --t;
        int ans = std::numeric_limits<int>::max();
        for (int i = 0; i < k; ++i) {
            ans = std::min(ans, dist[i][s] + dist[i][t]);
        }
        std::cout << ans << '\n';
    }
    return 0;
}

L - 嘘つきな生徒たち

$A$ を reverse して狭義単調増加にするために必要な要素の変更回数を求める。

添え字 $i _ 1 \lt i _ 2 \lt \cdots \lt i _ k$ を変更せずに残すとすると、狭義単調増加の条件から $j = 1, \ldots, k - 1$ に対して $A _ { i _ j } + (i _ { j + 1 } - i _ j) \leq A _ { i _ { j + 1} }$、すなわち $A _ { i _ j } - i _ j \leq A _ { i _ { j + 1} } - i _ { j + 1 }$ を満たす必要がある。

実はこれでは十分ではなく、添字 $0,\ldots, i _ 1 - 1$ および $i _ k + 1, \ldots, N - 1$ で変更後の値が $0$ 以上 $P$ 以下に収まるために、追加で $0 \leq A _ { i _ 1 } - { i _ 1 }$ および $A _ { i _ k } - { i _ k } \leq P - N + 1$ を満たす必要がある。

以上より、条件をまとめると以下のように書け、これは必要十分条件である。 $$ 0 \leq A _ { i _ 1 } - i _ 1 \leq \cdots \leq A _ { i _ k } - i _ k \leq P - N + 1 $$

条件を満たす添字列のうち最長のものが求まればよく、これはおおよそ Longest Increasing Sequence (LIS) と同様にして $O(N \log N)$ で求められる。

#include <algorithm>
#include <iostream>
#include <limits>
#include <vector>

constexpr int inf = std::numeric_limits<int>::max();

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

    int n, p;
    std::cin >> n >> p;

    std::vector<int> a(n);
    for (auto it = a.rbegin(); it != a.rend(); ++it) std::cin >> *it;

    for (int i = 0; i < n; ++i) {
        a[i] -= i;
        if (a[i] > p - n + 1 or a[i] < 0) {
            a[i] = -inf;
        }
    }

    std::vector dp(n + 1, inf);
    dp[0] = 0;

    for (int v : a) {
        if (v == -inf) continue;
        int x = std::upper_bound(dp.begin(), dp.end(), v) - dp.begin();
        dp[x] = v;
    }

    for (int i = n; i >= 0; --i) {
        if (dp[i] == inf) continue;
        std::cout << n - i << std::endl;
        break;
    }

    return 0;
}

M - 名前の変更

index アクセスのできる set があれば 殴れる (解説を確認したところ、想定解でした) 問題。

#include <iostream>

#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>

using namespace __gnu_pbds;

template <typename T>
using ordered_set = tree<T, null_type, std::less<T>, rb_tree_tag, tree_order_statistics_node_update>;

int main() {
    int n, q;
    std::cin >> n >> q;
    std::vector<std::string> ans(n);
    ordered_set<std::pair<std::string, int>> mp;
    for (int i = 0; i < n; ++i) {
        std::string s;
        std::cin >> s;
        mp.insert(std::pair { ans[i] = s, i });
    }
    while (q --> 0) {
        int x;
        std::string t;
        std::cin >> x >> t;
        --x;
        auto [old_name, i] = *mp.find_by_order(x);
        mp.erase(std::pair { old_name, i });
        mp.insert(std::pair { ans[i] = t, i });
    }
    for (int i = 0; i < n; ++i) {
        std::cout << ans[i] << " \n"[i == n - 1];
    }
    return 0;
}

N - 共通部分

Convex Cut を知っていますか?という問題。

$S$ に対して $T$ の各辺で上記問題の要領で Cut すれば $O(NM)$ で共通部分の凸多角形を求められる。

#include <complex>
#include <iomanip>
#include <iostream>
#include <vector>

using coordinate_t = long double;
using Point = std::complex<coordinate_t>;
using Line = std::pair<Point, Point>;
using Polygon = std::vector<Point>;

constexpr coordinate_t EPS = 1e-9;

int sgn(coordinate_t x) {
    return x < -EPS ? -1 : x > +EPS ? +1 : 0;
}

coordinate_t det(const Point &x, const Point &y) {
    return x.real() * y.imag() - x.imag() * y.real();
}

Point cross_point(const Line &l1, const Line &l2) {
    Point u = l1.second - l1.first, v = l2.first - l2.second, c = l2.first - l1.first;
    return l2.first - det(u, c) / det(u, v) * v;
}

auto convex_cut(const Polygon &convex, const Line &l) {
    const auto &[la, lb] = l;
    Polygon res;
    int sz = convex.size();
    for (int i = 0; i < sz; ++i) {
        int j = i + 1;
        if (j == sz) j -= sz;
        const Point &a = convex[i], &b = convex[j];
        int da = sgn(det(lb - la, a - la));
        if (da >= 0) res.push_back(a);
        int db = sgn(det(lb - la, b - la));
        if (da * db < 0) res.push_back(cross_point(l, Line { a, b }));
    }
    return res;
}

coordinate_t signed_area(const Polygon &poly) {
    coordinate_t res = 0;
    int n = poly.size();
    for (int i = 0; i < n; ++i) res += det(poly[i], poly[(i + 1) % n]);
    return res / 2;
}

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

    int n, m;
    std::cin >> n >> m;

    Polygon s(n), t(m);
    for (int i = 0; i < n; ++i) {
        coordinate_t x, y;
        std::cin >> x >> y;
        s[i] = { x, y };
    }
    for (int i = 0; i < m; ++i) {
        coordinate_t x, y;
        std::cin >> x >> y;
        t[i] = { x, y };
    }
    
    for (int i = 0; i < m; ++i) {
        s = convex_cut(s, Line { t[i], t[(i + 1) % m] });
    }

    std::cout << std::fixed << std::setprecision(20) << std::abs(signed_area(s)) << std::endl;

    return 0;
}

O - ペアスコア

$B _ j - B _ i = k\; (1\leq k \leq N)$ を満たす $i, j$ の答えへの寄与を考える (主客転倒)。$i, j$ の並べ方は $N - (B _ j - B _ i)$ 通り、それ以外の並べ方は $(N - 2) !$ 通りである。従って、寄与は $S _ k \cdot (N - k) \cdot (N - 2) !$ と求められる。

以上より、$C _ k := \# \{ (i, j) \mid B _ j - B _ i = k \}$ と定義すれば、求める答えは $\displaystyle (N - 2) ! \cdot \sum _ {k = 1} ^ N C _ k \cdot S _ k \cdot (N - k)$ である。

あとは各 $C _ k$ を高速に求められればよいが、$\displaystyle M := \sup_i B_i = 100000$ に対して $B ' _ i = M - B _ i \geq 0$ として代わりに $B _ i + B ' _ j = k + M $ を満たす $i, j$ の組を数えることにする。和の条件になったので、これは高速フーリエ変換を用いて計算できる。計算量は $O(N + M \log M)$。

#include <iostream>
#include <atcoder/modint>
#include <atcoder/convolution>

using mint = atcoder::modint998244353;

constexpr int M = 100000;

int main() {
    int n;
    std::cin >> n;

    std::vector<mint> f(M + 1), g(M + 1);
    for (int i = 0; i < n; ++i) {
        int v;
        std::cin >> v;
        ++f[v];
        ++g[M - v];
    }

    std::vector<int> s(n + 1);
    for (int i = 1; i <= n; ++i) {
        std::cin >> s[i];
    }

    auto c = atcoder::convolution(f, g);
    c.erase(c.begin(), c.begin() + M);
    c.resize(n + 1, 0);

    mint ans = 0;
    for (int i = 1; i <= n; ++i) {
        ans += c[i] * (n - i) * s[i];
    }
    for (int i = 1; i <= n - 2; ++i) {
        ans *= i;
    }
    std::cout << ans.val() << std::endl;
    return 0;
}