けんちょんの競プロ精進記録

競プロの精進記録や小ネタを書いていきます

JOI 本選 2007 E - 最軽量のモビール (AOJ 0520, 難易度 7)

読解が大変だけど、本質的には木 DP な問題

問題概要

 N 本の棒を用いて、下図のようなモビールを作りたい。

f:id:drken1215:20210104024203p:plain

入力としては、各棒  i についての

  • 左端から支点までの長さ  P_{i}
  • 右端から支点までの長さ  Q_{i}
  • 左端からつながっている棒の index  L_{i} (重りの場合は 0)
  • 右端からつながっている棒の index  R_{i} (重りの場合は 0)

が与えられる。重りの重さはすべて整数値でなければならない。考えられるモビールをすべて考えたときの、重りの重さの総和の最小値を求めよ。

制約

考えたこと

モビールは実際には木に過ぎない。木 DP と同じ発想が使える。

  • dp[i]モビール  i 以下のみを考えたときの、重りの重さの総和として考えられる最小値

とする。さて、dp[i] を求めるとき

  • 左側のモビールについての重さの総和を  L (= dp[L[i]])
  • 右側のモビールについての重さの総和を  R (= dp[R[i]])

としよう。このとき、左側の重さの総和は  L の倍数となり、右側の重さの総和は  R の倍数となる。よって、てこの原理を考慮して

 LP_{i}x = RQ_{i}y

を満たす正の整数  x, y の最小値を求めればよいということになる。具体的には、

  •  G = {\rm GCD}(LP_{i}, RQ_{i})

としたとき、

とすればよいことがわかる。よって、

  • dp[i] =  \frac{LR}{G}(P_{i} + Q_{i})

と求められた。このような DP を再帰的に実施すれば OK。なお、最初に根の位置を特定する必要がある。

コード

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

int main() {
    int N;
    cin >> N;
    vector<long long> P(N), Q(N), left(N), right(N);
    vector<int> par(N, -1);
    for (int i = 0; i < N; ++i) {
        cin >> P[i] >> Q[i] >> left[i] >> right[i];
        --left[i], --right[i];
        if (left[i] != -1) par[left[i]] = i;
        if (right[i] != -1) par[right[i]] = i;
    }

    // 根を特定
    int root = -1;
    for (int v = 0; v < N; ++v) if (par[v] == -1) root = v;

    // DP
    auto rec = [&](auto self, int v) -> long long {
        // 葉の場合
        if (v == -1) return 1;

        long long L = self(self, left[v]);
        long long R = self(self, right[v]);
        long long G = gcd(L*P[v], R*Q[v]);
        return L * R * (P[v] + Q[v]) / G;
    };
    cout << rec(rec, root) << endl;
}

Codeforces Round #557 (Div. 1) C. Thanos Nim (R2000)

面白かった!

問題概要

 N 個の石の山がある ( N は偶数)。 i 番目の山には  A_{i} 個の石がある。先手と後手が交互に以下の操作を行う。操作できなくなった方が負けである

  1. 石の山のうち、まだ石が 1 個以上残っている山をちょうど  \frac{N}{2} 子選ぶ
  2. そのそれぞれの山から、石を任意個数除去する (選んだ山ごとに異なる個数の石を除去してもよい)

つまり、「石の個数が 0」であるような山が過半数となった状態で手番を渡されたら負けである。双方最善を尽くしたとき、どちらが勝つか?

制約

  •  N, A_{i} \le 50

考えたこと

たとえば、 A = (8, 8) とかは対象戦略から後手必勝となる。 (8, 10) とかは先手が  (8, 8) とすることで、先手必勝となる。

 N = 2 の場合はすぐにわかった。 N = 4 の場合を考えよう。少しずつ以下のことがわかってきた。

  • (1, 1, 1, 1) は後手必勝
  • (1, 1, 1, 2以上) は後手必勝
  • (1, 1, 2以上, 2以上) は先手必勝
  • (1, 2以上, 2以上, 2以上) は先手必勝
  • (2, 2, 2, 2) は後手必勝
  • (2, 2, 2, 3以上) は後手必勝
  • (2, 2, 3以上, 3以上) は先手必勝
  • (2, 3以上, 3以上, 3以上) は先手必勝
  • ...

このようにしていくうちに、以下の予想が立った。

「最小の値が過半数の場面は後手必勝 (勝ちパターン)」

予想さえ立てられたら示すのは比較的容易だ。

  • 最小の値が過半数の状態からは、いかなる操作を行っても、そうでない状態へと遷移する
  • 最小の値が過半数でない状態からは、「最小の値が過半数である」という状態へと遷移することができる

というわけで、「最小の値が過半数」という状態を作れたら勝ち (後手必勝) であることがわかった。

コード

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

int main() {
    int N;
    cin >> N;
    vector<int> A(N);
    for (int i = 0; i < N; ++i) cin >> A[i];

    auto solve = [&]() -> bool {
        int mi = 100;
        map<int,int> ma;
        for (int i = 0; i < N; ++i) ma[A[i]]++, chmin(mi, A[i]);
        if (ma[mi] > N/2) return false;
        else return true;
    };
    cout << (solve() ? "Alice" : "Bob") << endl;
}

Codeforces Round #557 (Div. 1) B. Chladni Figure (R1900)

KMP 法で殴ったけど、愚直にやっても調和級数的計算量で間に合うね。

問題概要

円周上に等間隔に  N 個の点が打たれている。これらの点を端点とした  M 個の線分がある (下図のような感じ)。

f:id:drken1215:20210104005723p:plain

これが回転対象性をもつかどうかを判定せよ (1 周未満の回転で模様が一致するかを判定せよ)。

制約

  •  2 \le N \le 10^{5}
  •  1 \le M \le 2 \times 10^{5}

考えたこと

なんとなく文字列のように扱えそうな気がした。具体的には

  • vec[v] ← 点  v から見て隣接している頂点への差分の集合

とする (vector<vector<int>> 型などで管理)。これを、各文字が vector<int> 型であるような文字列とみなすと、KMP 法が使える。KMP 法を用いると、文字列の最小周期  d が求められる。 d \lt N かつ  d | N であれば "Yes"、そうでなければ "No"。これで計算量は  O(N + M) となる。

なお、KMP 法を使わなくても、周期  d = 1, 2, \dots, N-1 を愚直に試しても OK。これで計算量は  O(M \log N) となる。

コード

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

// T = string or vector<long long>
template<class T> struct KMP {
    T pat;
    vector<int> fail;

    // construct
    KMP(const T &p) { init(p); }
    void init(const T &p) {
        pat = p;
        int m = (int)pat.size();
        fail.assign(m+1, -1);
        for (int i = 0, j = -1; i < m; ++i) {
            while (j >= 0 && pat[i] != pat[j]) j = fail[j];
            fail[i+1] = ++j;
        }
    }

    // the period of S[0:i]
    int period(int i) { return i - fail[i]; }
    
    // the index i such that S[i:] has the exact prefix p
    vector<int> match(const T &S) {
        int n = (int)S.size(), m = (int)pat.size();
        vector<int> res;
        for (int i = 0, k = 0; i < n; ++i) {
            while (k >= 0 && S[i] != pat[k]) k = fail[k];
            ++k;
            if (k >= m) res.push_back(i - m + 1), k = fail[k];
        }
        return res;
    }
};

int main() {
    int N, M;
    cin >> N >> M;
    vector<int> X(M), Y(M);
    for (int i = 0; i < M; ++i) {
        cin >> X[i] >> Y[i];
        --X[i], --Y[i];
    }
    vector<vector<int>> str(N);
    for (int i = 0; i < M; ++i) {
        str[X[i]].push_back( (Y[i]-X[i]+N) % N );
        str[Y[i]].push_back( (X[i]-Y[i]+N) % N );
    }
    for (int i = 0; i < N; ++i) sort(str[i].begin(), str[i].end());
  
    KMP<vector<vector<int>>> kmp(str);
    int syu = kmp.period(N);
    if (syu < N && N % syu == 0) cout << "Yes" << endl;
    else cout << "No" << endl;
}

Codeforces Round #557 (Div. 1) D. Palindrome XOR (R2400)

面白かった。重み付き Union-Find を使った。

問題概要

0 と 1 と ? のみからなる長さ  N の文字列  S が与えられる。先頭の文字が 1 であることが保証されている。

以下の条件を満たす整数の組  a, b ( 1 \le a \lt b \lt 2^{60}) の個数を求めよ。

  •  a, b はともに回文数である (11 や 101 など)
  •  a XOR  b を計算したとき、 S の '?' 以外の部分については値が一致する

制約

  •  1 \le N \le 1000

考えたこと

 a, b のどちらかの桁数は  N 桁でなければならない。とりあえず  b の桁数を  N 桁に固定して考えることにした。

  •  a N 桁未満のときは、求めた個数を全体に合算
  •  a N 桁であるときには、求めた個数を 1/2 にして全体に加算 ( a \lt b であるものと  a \gt b であるものが半数ずつになるため)

 a の桁数  M M = 1, 2, \dots, N のそれぞれについて考えることにする。このとき、 a b の各桁の値は  N+M 次元の 0-1 ベクトルとみなせる。この  N+M 個の 0-1 変数は

  •  a, b の最上位の値は 1
  •  b のうち、 M 桁目より大きいところは  S に一致 (? 以外)
  •  a b 1, 2, \dots, M 桁目の XOR 和は  S の該当桁に一致 (? 以外)
  •  a b回文数

という条件を満たすようにする。でもこれは

  •  x_{v} = 0 or  1
  •  x_{u} XOR  x_{v} = 0 or  1

という 2 タイプの式なので、F2 体上の重み付き Union-Find で管理できる。「根の値の確定しないグループの個数」を  a として、 2^{a} 通りと求められる。

コード

 O(N^{2} \alpha(N) の計算量となる。

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

// modint
template<int MOD> struct Fp {
    long long val;
    constexpr Fp(long long v = 0) noexcept : val(v % MOD) {
        if (val < 0) val += MOD;
    }
    constexpr int getmod() const { return MOD; }
    constexpr Fp operator - () const noexcept {
        return val ? MOD - val : 0;
    }
    constexpr Fp operator + (const Fp& r) const noexcept { return Fp(*this) += r; }
    constexpr Fp operator - (const Fp& r) const noexcept { return Fp(*this) -= r; }
    constexpr Fp operator * (const Fp& r) const noexcept { return Fp(*this) *= r; }
    constexpr Fp operator / (const Fp& r) const noexcept { return Fp(*this) /= r; }
    constexpr Fp& operator += (const Fp& r) noexcept {
        val += r.val;
        if (val >= MOD) val -= MOD;
        return *this;
    }
    constexpr Fp& operator -= (const Fp& r) noexcept {
        val -= r.val;
        if (val < 0) val += MOD;
        return *this;
    }
    constexpr Fp& operator *= (const Fp& r) noexcept {
        val = val * r.val % MOD;
        return *this;
    }
    constexpr Fp& operator /= (const Fp& r) noexcept {
        long long a = r.val, b = MOD, u = 1, v = 0;
        while (b) {
            long long t = a / b;
            a -= t * b, swap(a, b);
            u -= t * v, swap(u, v);
        }
        val = val * u % MOD;
        if (val < 0) val += MOD;
        return *this;
    }
    constexpr bool operator == (const Fp& r) const noexcept {
        return this->val == r.val;
    }
    constexpr bool operator != (const Fp& r) const noexcept {
        return this->val != r.val;
    }
    friend constexpr istream& operator >> (istream& is, Fp<MOD>& x) noexcept {
        is >> x.val;
        x.val %= MOD;
        if (x.val < 0) x.val += MOD;
        return is;
    }
    friend constexpr ostream& operator << (ostream& os, const Fp<MOD>& x) noexcept {
        return os << x.val;
    }
    friend constexpr Fp<MOD> modpow(const Fp<MOD>& r, long long n) noexcept {
        if (n == 0) return 1;
        if (n < 0) return modpow(modinv(r), -n);
        auto t = modpow(r, n / 2);
        t = t * t;
        if (n & 1) t = t * r;
        return t;
    }
    friend constexpr Fp<MOD> modinv(const Fp<MOD>& r) noexcept {
        long long a = r.val, b = MOD, u = 1, v = 0;
        while (b) {
            long long t = a / b;
            a -= t * b, swap(a, b);
            u -= t * v, swap(u, v);
        }
        return Fp<MOD>(u);
    }
};

const int MOD = 998244353;
using mint = Fp<MOD>;

// Union-Find
template<class Abel> struct UnionFind {
    const Abel UNITY_SUM = 0;      // to be set
    vector<int> par;
    vector<Abel> diff_weight;
    vector<Abel> val;
    vector<int> onum; // 根が 0 のときの 1 の個数

    UnionFind() { }
    UnionFind(int n) : par(n, -1), diff_weight(n, UNITY_SUM)
                     , val(n, -1), onum(n, 0) {}
    int root(int x) {
        if (par[x] < 0) return x;
        else {
            int r = root(par[x]);
            diff_weight[x] ^= diff_weight[par[x]];
            return par[x] = r;
        }
    }
    
    Abel calc_weight(int x) {
        int rx = root(x);
        return diff_weight[x];
    }
    
    bool issame(int x, int y) {
        return root(x) == root(y);
    }

    void set(int x, Abel w) {
        auto rx = root(x);
        auto dw = diff_weight[x];
        val[rx] = w ^ dw;
    }
    
    bool merge(int x, int y, Abel w = 0) {
        w ^= calc_weight(x); w ^= calc_weight(y);
        x = root(x), y = root(y);
        if (x == y) return false;
        if (par[x] > par[y]) swap(x, y); // merge technique
        
        if (w == 0) onum[x] += onum[y];
        else onum[x] += -par[y] - onum[y];

        if (val[y] != -1) val[x] = val[y] ^ w;
        par[x] += par[y];
        par[y] = x;
        diff_weight[y] = w;
        return true;
    }
    
    Abel diff(int x, int y) {
        return calc_weight(y) ^ calc_weight(x);
    }
    
    int size(int x) {
        return -par[root(x)];
    }

    int get_onum(int x) {
        x = root(x);
        if (val[x] == -1) return min(onum[x], -par[x] - onum[x]);
        else if (val[x] == 0) return onum[x];
        else return -par[x] - onum[x];
    }
};

mint solve(const string &S) {
    mint res = 0;
    int N = (int)S.size();
    for (int M = 1; M <= N; ++M) {
        UnionFind<int> uf(N+M);

        auto merge = [&](int i, int j, int w) -> bool {
            if (uf.issame(i, j)) {
                if (uf.diff(i, j) != w) return false;
                else return true;
            } else {
                 w ^= uf.calc_weight(i); w ^= uf.calc_weight(j);
                i = uf.root(i), j = uf.root(j);
                if (uf.val[i] != -1 && uf.val[j] != -1 && (uf.val[i] ^ uf.val[j]) != w) return false;
                uf.merge(i, j, w);
                return true;
            }
        };
        bool ok = true;
        for (int i = 0; i < N-M; ++i) {
            if (S[i] == '0') uf.set(i, 0);
            else if (S[i] == '1') uf.set(i, 1);
        }
        uf.set(0, 1);
        uf.set(N, 1);
        for (int i = 0; i < N; ++i) if (!merge(i, N-i-1, 0)) ok = false;
        for (int i = 0; i < M; ++i) if (!merge(i+N, M-i-1 + N, 0)) ok = false;

        for (int i = N-M; i < N; ++i) {
            if (S[i] == '0') { if (!merge(i, i+M, 0)) ok = false; }
            else if (S[i] == '1') { if (!merge(i, i+M, 1)) ok = false; }
        }

        if (!ok) continue;
        
        mint tmp = 1;
        for (int v = 0; v < N+M; ++v) {
            if (uf.root(v) != v) continue;
            if (uf.val[v] == -1) tmp *= 2;
        }
        if (M == N) res += tmp / 2;
        else res += tmp;  
    }
    return res;
}

int main() {
    string S;
    cin >> S;
    cout << solve(S) << endl;
}

AtCoder ABC 187 C - 1-SAT (灰色, 300 点)

とにかく実装力を鍛えよう〜〜

問題概要

 N 個の文字列が与えられる。そのうちのいくつかは先頭の文字が ! である (それ以外はすべて英小文字)。

red
gray
!orange
yellow
!blue
cyan
!green
brown
!gray

! の付いていない文字列と、付いている文字列とで、共通しているものがあれば、そのうちの 1 つを出力せよ。共通しているものがない場合はその旨を報告せよ。

制約

  •  1 \le N \le 2 \times 10^{5}

考えたこと

問題設定が複雑だけど、結局こういう問題。


2 つの集合  A, B が与えられる。それらの共通要素を求めよ。


こういうのは過去に何度も出題されている!!!いろんな方法があるけど、こういう風にできる。

  • 集合 A の要素を std::set などで管理する
  • 集合 B の要素を順に見ていって、A にも含まれるものがあれば、それを出力する

ここで、A に含まれるかどうかを判定する部分を高速化するために、std::set などを用いる。計算量は  O(N \log N) になる。

コード

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

int main() {
    int N;
    cin >> N;
    set<string> se1, se2;
    for (int i = 0; i < N; ++i) {
        string str;
        cin >> str;
        if (str[0] == '!') se1.insert(str.substr(1));
        else se2.insert(str);
    }
    
    string res = "";
    for (auto it: se1) if (se2.count(it)) res = it;
    if (res == "") cout << "satisfiable" << endl;
    else cout << res << endl;
}