スコルの知恵袋

主にプログラミング関係の気になったこと等をまとめたブログ

AtCoder Beginner Contest 177 感想

atcoder.jp

もう髪の毛の先くらいは青に染まってると思うねんけど。

A - Don't be late

入力例3の高橋君、やる気がなさすぎる。下の「間に合いません。」でウケた。

提出コード

リンク: https://atcoder.jp/contests/abc177/submissions/16304998

void solve() {
    ll D, T, S;
    cin >> D >> T >> S;
    prints()(D <= T * S ? "Yes" : "No");
}

B - Substring

最初、「連続」を読み飛ばしていて、「ん?むずくね?」となった。

提出コード

リンク: https://atcoder.jp/contests/abc177/submissions/16312925

void solve() {
    string S, T;
    cin >> S >> T;
    ll ans = L_INF;
    for (ll i = 0; i < (ll)S.size(); i++) {
        if (i + (ll)T.size() > (ll)S.size()) break;
        ll cnt = 0;
        for (ll j = 0; j < (ll)T.size(); j++) {
            if (S[i + j] != T[j]) {
                ++cnt;
            }
        }
        chmin(ans, cnt);
    }
    prints()(ans);
}

C - Sum of product of pairs

愚直にやるのは  O(N^2) なのできびしい。

 \mathrm{ans}

 \displaystyle{
    \mathrm{ans} = \sum_{i = 1}^{N-1} \sum_{j = i+1}^{N} A_i A_j = \sum_{i=1}^{N-1} A_i \sum_{j=i+1}^{N} A_j
}

と表せるので、累積和を前計算しておけば解を高速に求められる。

計算量は前計算が  O(N)、解の計算が  O(N)

この手の総和の順番を入れ替えて高速化する問題は度々出てるとはいえ、灰diffなのはびっくりした。茶色くらいはあると思った。

提出コード

リンク: https://atcoder.jp/contests/abc177/submissions/16317845

// ModInt 略

constexpr ll MOD = 1e9 + 7;
using Mint = ModInt<MOD>;
 
void solve() {
    ll N;
    cin >> N;
    vector<Mint> A(N);
    for (ll i = 0; i < N; i++) {
        cin >> A[i];
    }
    vector<Mint> cum(N + 1);
    for (ll i = 0; i < N; i++) {
        cum[i + 1] = cum[i] + A[i];
    }
    Mint ans = 0;
    for (ll i = 0; i < N; i++) {
        ans += A[i] * (cum[N] - cum[i + 1]);
    }
    prints()(ans);
}

D - Friends

Union-Find を持っていたら秒で終わる問題。

Union-FInd で友達関係を素集合として管理する。素集合の最大要素数 m とすると

  • 明らかに  m 未満のグループ数では目的は達成できない(鳩ノ巣原理)。
  • グループ数が  m 以上ならば、目的を達成できるグループの分け方が必ず存在する。

なので、目的を達成できる最小のグループ数は  m

提出コード

リンク: https://atcoder.jp/contests/abc177/submissions/16322341

class UnionFind {
    using i64 = std::int64_t;
 
private:
    std::vector<i64> par;
    std::vector<i64> sz;
 
public:
    UnionFind(i64 N) : par(N), sz(N, 1) {
        for (i64 i = 0; i < N; ++i) par[i] = i;
    }
    i64 root(i64 x) {
        return par[x] == x ? x : par[x] = root(par[x]);
    }
    bool same(i64 x, i64 y) {
        return root(x) == root(y);
    }
    void unite(i64 x, i64 y) {
        x = root(x);
        y = root(y);
        if (x == y) return;
        if (sz[x] < sz[y]) std::swap(x, y);
        par[y] = x;
        sz[x] += sz[y];
    }
    i64 size(i64 x) {
        return sz[root(x)];
    }
    void reset(i64 N) {
        par.resize(N);
        for (i64 i = 0; i < N; ++i) par[i] = i;
        sz.assign(N, 1);
    }
};
 
void solve() {
    ll N, M;
    cin >> N >> M;
    UnionFind uf(N);
    for (ll i = 0; i < M; i++) {
        int a, b;
        cin >> a >> b;
        --a, --b;
        uf.unite(a, b);
    }
    ll ans = 0;
    for (ll i = 0; i < N; i++) {
        chmax(ans, uf.size(i));
    }
    prints()(ans);
}

E - Coprime

 d(x) を次のように定義する。

 \displaystyle{
    d(x) := (\{ A_i \} の中で x を素因数として持つものの個数)
}

このとき、

  •  \{ A_i \} が pairwise coprime ⇔  \max_{x \in \mathbb{P}} d(x) \le 1
  •  \{ A_i \} が setwise coprime ⇔  1 \lt \max_{x \in \mathbb{P}} d(x) \lt N

が言えるので、これを使って判定する。

 d(x) A_i素因数分解すると求められるけど、試し割り法で愚直にやるのは  O(N \sqrt{\max A_i}) かかるのでちょっと嫌な気分になる(でもなんかこれでも通るらしい?)。 実はもっと高速に素因数分解できる方法がある。ちょっと前にそれを Twitter で見かけられたのは幸運だった。

 x の最小の素因数を  p(x) と書くことにする。また、  M = \max A_i とおく。  p(x)\ (x \in \{2,\ldots,M\}) はエラトステネスの篩の要領で  O(M \log \log M) で求められて、 これを使うと、 2 \le n \le M を満たす整数  n O(\log n)素因数分解できる。 この方法で、各  A_i素因数分解すればいい。

 p(x) の構築に  O(M \log \log M) N 個の素因数分解クエリに  O(N \log M)かかるので、 \{ A_i \}素因数分解にかかる計算量は全部で  O(M \log \log M + N \log M)

 p(x) の構築を  O(M) でできる実装もある → https://cp-algorithms.com/algebra/prime-sieve-linear.html

まぁ  \log \log M なんてほとんど気にならない差なのでエラトステネスの篩でいいかな。

提出コード

リンク: https://atcoder.jp/contests/abc177/submissions/16336620

void solve() {
    constexpr int MX_V = 1000000;
    int cp[MX_V + 1];
    Vi ps;  // <- これいらん(笑)焦りすぎでしょ。
    for (ll i = 0; i <= MX_V; i++) {
        cp[i] = -1;
    }
    for (ll i = 2; i <= MX_V; i++) {
        if (cp[i] == -1) {
            ps.emplace_back(i);
            cp[i] = i;
        }
        for (ll j = i + i; j <= MX_V; j += i) {
            if (cp[j] != -1) continue;
            cp[j] = i;
        }
    }
 
    ll N;
    cin >> N;
    Vl A(N);
    for (ll i = 0; i < N; i++) {
        cin >> A[i];
    }
 
    int ns[MX_V + 1];
    for (ll i = 0; i <= MX_V; i++) {
        ns[i] = 0;
    }
 
    set<int> st;
    for (ll i = 0; i < N; i++) {
        st.clear();
        int t = A[i];
        while (t > 1) {
            ll q = cp[t];
            if (q > 1) {
                st.emplace(q);
            }
            t /= q;
        }
        for (auto x : st) {
            ++ns[x];
        }
    }
 
    ll mx = 0;
    for (ll i = 0; i < MX_V; i++) {
        chmax(mx, ns[i]);
    }
 
    if (mx <= 1) {
        prints()("pairwise coprime");
    }
    else if (mx < N) {
        prints()("setwise coprime");
    }
    else {
        prints()("not coprime");
    }
}

F - I hate Shortest Path Problem

解けなかった。でも、コンテスト中にかなり惜しいところまで考察できてた。くやしい~~~。以下、コンテスト後の考察。

in-place DP で解ける。

 i j 列目のマスを  (i,j) と書くことにする。

 \mathrm{dp}_i [  j ] を次のように定義する。

 \displaystyle{
    \mathrm{dp}_i[ j ] := \begin{cases}
       f(i,j) & ((i,j)\ が下に動けるマスの場合) \\
       \infty & (\textrm{otherwise})
    \end{cases}
}

ただし、 f(i,j) は「マス  (i,j) に到達するために必要な横への移動回数の最小値 (到達できない場合は  \infty)」。

 k 番目の出力は  \mathrm{ans}_k = \min_{j} \left( \mathrm{dp}_k[ j ] + k \right) とすればいい。

区間min・区間更新を処理できる遅延セグ木2本を使うと  \mathrm{dp}_{k} から  \mathrm{dp}_{k+1} O(\log W) で求められる。

 \mathrm{dp}_k から  \mathrm{ans}_k を求めるのも  O(\log W) でできる。

配列の初期化なども併せて計算量は全部で  O(H \log W + W)

コンテスト後提出コード

リンク: https://atcoder.jp/contests/abc177/submissions/16405505

template <class T, class E, class F, class G, class H, class P>
class LazySegmentTree {
private:
    const F f;
    const G g;
    const H h;
    const P p;
    const T tid;
    const E eid;
    std::vector<T> vec;
    std::vector<E> lazy;
    int sz;
 
public:
    explicit LazySegmentTree(
        int n, F f, G g, H h, P p, T tid, E eid) noexcept
        : f(f), g(g), h(h), p(p), tid(tid), eid(eid) {
        sz = 1;
        while (sz < n) sz <<= 1;
        vec.assign(sz * 2 - 1, tid);
        lazy.assign(sz * 2 - 1, eid);
    }
    T& operator[](int idx) noexcept {
        return vec[sz - 1 + idx];
    }
    void set_value(int idx, T val) noexcept {
        vec[sz - 1 + idx] = val;
    }
    void build() noexcept {
        for (int i = sz - 2; i >= 0; i--) {
            vec[i] = f(vec[i * 2 + 1], vec[i * 2 + 2]);
        }
    }
    void eval(int k, int l, int r) noexcept {
        if (lazy[k] == eid) return;
        if (r - l > 1) {
            lazy[k * 2 + 1] = h(lazy[k * 2 + 1], lazy[k]);
            lazy[k * 2 + 2] = h(lazy[k * 2 + 2], lazy[k]);
        }
        vec[k] = g(vec[k], p(lazy[k], r - l));
        lazy[k] = eid;
    }
    void update(int a, int b, E x) noexcept {
        _update(a, b, x, 0, 0, sz);
    }
    T query(int a, int b) noexcept {
        return _query(a, b, 0, 0, sz);
    }
 
private:
    T _update(int a, int b, E x, int k, int l, int r) noexcept {
        eval(k, l, r);
        if (b <= l || r <= a) return vec[k];
        if (a <= l && r <= b) {
            lazy[k] = h(lazy[k], x);
            return g(vec[k], p(lazy[k], r - l));
        }
        else {
            T t1 = _update(a, b, x, k * 2 + 1, l, (l + r) >> 1);
            T t2 = _update(a, b, x, k * 2 + 2, (l + r) >> 1, r);
            return vec[k] = f(t1, t2);
        }
    }
    T _query(int a, int b, int k, int l, int r) noexcept {
        if (b <= l || r <= a) return tid;
        eval(k, l, r);
        if (a <= l && r <= b) {
            return vec[k];
        }
        else {
            T t1 = _query(a, b, k * 2 + 1, l, (l + r) >> 1);
            T t2 = _query(a, b, k * 2 + 2, (l + r) >> 1, r);
            return f(t1, t2);
        }
    }
};
 
void solve() {
    ll H, W;
    cin >> H >> W;
    vector<Pii> walls(H);
    for (ll i = 0; i < H; i++) {
        int a, b;
        cin >> a >> b;
        --a, --b;
        walls[i] = {a, b};
    }
 
    ll tid = L_INF;
    ll eid = -1;
    auto opf = [](ll a, ll b) -> ll { return min(a, b); };
    auto opg = [&](ll a, ll b) -> ll { return b == eid ? a : b; };
    auto oph = [&](ll a, ll b) -> ll { return b == eid ? a : b; };
    auto opp = [](ll a, ll k) -> ll { return a; };
    LazySegmentTree seg1(W, opf, opg, oph, opp, tid, eid);
    LazySegmentTree seg2(W, opf, opg, oph, opp, tid, eid);
    for (ll i = 0; i < W; i++) {
        seg1[i] = 0;
        seg2[i] = W - 1 - i;
    }
    seg1.build();
    seg2.build();
 
    for (ll i = 0; i < H; i++) {
        auto [left, right] = walls[i];
        if (right + 1 < W) {
            ll q = seg2.query(left, right + 2);
            if (q != tid) {
                q -= W - 1 - (right + 1);
                seg1.update(right + 1, right + 2, q);
                seg2.update(right + 1, right + 2, q + W - 1 - (right + 1));
            }
        }
        seg1.update(left, right + 1, tid);
        seg2.update(left, right + 1, tid);
        ll res = seg1.query(0, W);
        if (res == tid) {
            prints()(-1);
        }
        else {
            prints()(res + i + 1);
        }
 
        // for (ll i = 0; i < W; i++) {
        //     prints("", " ")(seg1.query(i, i + 1));
        // }
        // prints()();
        // for (ll i = 0; i < W; i++) {
        //     prints("", " ")(seg2.query(i, i + 1));
        // }
        // prints()();
    }
}