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

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

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;
}

AtCoder ABC 187 D - Choose Me (茶色, 400 点)

ペア  (A+B, A) の大きい順にソートする嘘貪欲にハマってしまった方が多そうだった

問題概要

青木君と高橋君が選挙を行う。 N 個の町があり、 i 番目の町では

  • 青木派が  A_{i} 人いる
  • 高橋派が  B_{i} 人いる

ということがわかっている。高橋君はいくつかの町で選挙活動を行う。

  • 高橋君が選挙活動を行わない町では、青木君が  A_{i} 票を獲得し、高橋君が 0 票を獲得する (高橋派は怠惰)
  • 高橋君が選挙活動を行う町では、青木君が 0 票を獲得し、高橋君が  A_{i} + B_{i} 票を獲得する (青木派は裏切り者)

高橋君が青木君よりも多く票を獲得するようにしたい。高橋君が選挙活動を行うべき町の個数の最小値を求めよ。

制約

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

考えたこと

高橋君は演説をしないと、まったく票をもらえないのねw

まず題意を把握するのに時間がかかった。こういう風に  N 個の対象物 (今回は町) それぞれに対して、二つの「属性」(今回は青木派人数と高橋派人数) が与えられるような問題は、独特の思考を要することが多い。たとえば今回でいえば

  • 高橋君の得票数を増やしたいので  A_{i} + B_{i} が大きいところを優先的に選挙したい
  • 青木君の得票数を減らしたいので  A_{i} が大きいところを優先的に選挙したい

という 2 つの価値観があるのだ。このように、二つの「属性」が登場する問題では、相異なる 2 つの価値観に基づく順序付けが登場して、そのジレンマに悩まされることが多い。

嘘解法:ペア  (A+B, A) でソート

上のようなジレンマから、次のような解法を考えた方が多かったようだ。


 A+B が大きい順に選挙活動する、ただし  A+B が等しい町同士では  A が大きい順に選挙活動する


つまり、ペア  (A+B, A) をキー (辞書順比較) として大きい順にソートするということだ。しかしこれは嘘だ。たとえば

  •  (A, B) = (10, 90), (20, 79), (81, 17)

を考えてみよう。上記の価値観に沿うと、 (10, 90), (20, 79), (81, 17) の順に演説に行きたくなる (総和が 100, 99, 98)。このとき

  •  (10, 90) の町に行っただけでは「高橋君: 100、青木君: 101」となってダメ

でも実際には

  •  (20, 79) の町に 1 個行くだけで、「高橋君: 99、青木君: 91」となるので OK

となっている。

正しくは

まず高橋君がどこにも演説に行かない場合を考えてみよう。このとき、 A_{i} の総和を  S として、

「高橋君: 0、青木君:  S

となる。このとき、 (A_{i}, B_{i}) で表される町へいくと

「高橋君:  A_{i} + B_{i}、青木君:  S - A_{i}

となる。このとき、青木君と高橋君との差がどのくらい縮まったのかに着目しよう。

  • 高橋君の票数は  A_{i} + B_{i} だけ増えた
  • 青木君の票数は  A_{i} だけ減った

ということで、差に着目すると

 (A_{i} + B_{i}) - (- A_{i}) = 2 A_{i} + B_{i}

だけ縮まることになるのだ (before の状態に依らない)。つまり、


  • 最初の「差」は  S である
  •  i で選挙すると、「差」は  2 A_{i} + B_{i} だけ減少する

ということがわかった。ここまで来ると簡単だ。 2 A_{i} + B_{i} が大きい方から順に選挙へ行けばよいのだ!!!

このように、2 つの属性値を扱う問題では、「差」に着目するとよいことがあったりする!!

コード

 2 A_{i} + B_{i} が大きい順にソートするので、計算量は  O(N \log N) となる。

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

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

    // ソート
    vector<int> ids(N);
    iota(ids.begin(), ids.end(), 0);
    sort(ids.begin(), ids.end(), [&](int i, int j) {
            return A[i]*2+B[i] > A[j]*2+B[j];
        });

    // 2A[i] + B[i] が大きい順に「差」を減らしていく
    long long diff = 0;
    for (int i = 0; i < N; ++i) diff += A[i];
    int res = 0;
    for (auto i: ids) {
        ++res;
        diff -= A[i]*2 + B[i];
        if (diff < 0) break;
    }
    cout << res << endl;
}

AtCoder ABC 187 E - Through Path (水色, 500 点)

これの応用問題って感じだった!!

drken1215.hatenablog.com

問題概要

頂点数  N の木が与えられる。 i (= 0, 1, \dots, N-2) 番目の辺は頂点  A_{i} と頂点  B_{i} とを結んでいる。はじめ、各頂点には、値 0 が書き込まれている。以下の  Q 個のクエリを処理したあとの、各頂点に書き込まれた値を求めよ。

  • クエリタイプ 1:辺番号  i と、値  x が与えられる。頂点  A_{i} から出発して頂点  B_{i} を通過することなくたどり着けるすべての頂点の値を  x だけ増やす
  • クエリタイプ 2:辺番号  i と、値  x が与えられる。頂点  B_{i} から出発して頂点  A_{i} を通過することなくたどり着けるすべての頂点の値を  x だけ増やす

制約

  •  N, Q \le 2 \times 10^{5}

考えたこと

単純化した次の問題を考えてみよう。

  • 与えられた木は根付き木であるとする
  • 各クエリでは、頂点  v を根とする部分木に含まれるすべての頂点の値を  x だけ増やす

この問題は実は過去問にそっくりそのままある!

drken1215.hatenablog.com

今回は、ちょっと応用問題と言える。というのも、まず与えられる木に根は指定されていない。よってまずは適当な頂点 (たとえば頂点 0) を根とした根付き木にしてしまおう。そして、クエリタイプ 1, 2 はいずれも、以下の 2 つの場合のいずれかの処理を行うことになる。


二頂点  p, v があって、 p v の親であるとする。

  • パターン 1: v を根とする根付き木全体に  x を足す
  • パターン 2: v を根とする根付き木以外の頂点に  x を足す


もともとのクエリタイプ 1, 2 が、このパターン 1, 2 にそのまま対応するとは限らない。いずれにしても、上に述べた 2 パターンのいずれかになる。

パターン 2 をパターン 1 へ帰着

さて、上のパターン 1 だけならば、まさに過去問の通りに解ける。パターン 2 があるのが厄介だ。しかしよく考えてみると、下図のように、パターン 2 はパターン 1 に帰着できるのだ!!!

つまり、パターン 2 は

  • すべての頂点に  x を足す (別途保持しておいて最後にまとめて足す)
  • 頂点  v を根とする根付き木全体に  -x を足す (パターン 1 と同じ処理)

というふうにすることで、結局すべてパターン 1 に帰着できる。

パターン 1

パターン 1 は、まさに次の問題そのものなので、以下の記事参照!

drken1215.hatenablog.com

コード

計算量は  O(N + Q) になる。

#include <bits/stdc++.h>
using namespace std;
using Graph = vector<vector<int>>;

int main() {
    int N;
    cin >> N;
    Graph G(N);
    vector<int> A(N-1), B(N-1);
    for (int i = 0; i < N-1; ++i) {
        cin >> A[i] >> B[i];
        --A[i], --B[i];
        G[A[i]].push_back(B[i]);
        G[B[i]].push_back(A[i]);
    }

    // DFS (根付き木にするのと、累積和をとるのを使い回す)
    int root = 0;
    vector<int> par(N, -1); // par[v]: 頂点 v の親
    vector<long long> add(N, 0);
    auto dfs = [&](auto self, int v, int p) -> void {
        par[v] = p;
        if (p != -1) add[v] += add[p];
        for (auto nv: G[v]) {
            if (nv == p) continue;
            self(self, nv, v);
        }
    };
    dfs(dfs, root, -1);

    // クエリ処理
    long long offset = 0;
    int Q;
    cin >> Q;
    while (Q--) {
        int type, id, v;
        cin >> type >> id >> v;
        --id;
        
        int a = A[id], b = B[id];
        if (type == 1) swap(a, b);
        
        if (par[b] == a) add[b] += v;
        else  add[a] -= v, offset += v;
    }
    
    // 累積和をとる
    dfs(dfs, root, -1);
    for (auto v: add) cout << v+offset << endl;    
}

AtCoder ABC 187 F - Close Group (青色, 600 点)

 O(3^{N}) な bit DP としてよく知られている問題ですね!

EDPC U - Grouping の類題と言える!

atcoder.jp

問題概要

頂点数  N、辺数  M の単純無向グラフが与えられる。頂点集合を、いくつかの頂点部分集合に分割したい。ただし、分割してできる各部分グラフがいずれもクリークとなるようにしたい。

分割されたグループの個数の最小値を求めよ。

制約

  •  1 \le N \le 18

解法 (1): O(3^{N}) の bit DP

こんな bit DP をすれば OK!!!

  • dp[bit] ← bit に対応する頂点集合をいくつかのクリークに分割するとき、分割されたクリークの個数の最小値

そして遷移は次の通り。

// 頂点集合 add が、bit と共通要素を持たず、クリークであるとする
chmin(dp[bit | add], dp[bit] + 1);

これは一見すると、bit が  2^{N} 通りあって、そのそれぞれに対して add が  O(2^{N}) 通りあって、全体で  O(4^{N}) 通りに思えてしまうかもしれない。しかし実はちゃんと解析すると  O(3^{N}) となって十分間に合うのだ。具体的には

add は、bit の補集合の部分集合しか動かない

という点に着目する。実装上はこんな感じにできる。ここで、「部分集合の部分集合を列挙する方法」を活用している。

for (int bit = 0; bit < (1<<N); ++bit) {
    int cbit = (1<<N) - 1 - bit; // 補集合
    for (int add = cbit; ; add = (add-1) & cbit) {
        if (!add) break;

        // 頂点集合 add がクリークかどうかを表す配列 ok を予め求めておく
        if (ok[add]) chmin(dp[bit|add], dp[bit] + 1);
    }
}

この処理の計算量が  O(3^{N}) であることを示そう。実は簡単だ。(bit, add) のとりうる値は次のように考えると  3^{N} 通りしかない。各頂点  i = 0, 1, \dots, N-1 について、

  • 頂点  i は頂点集合 bit に含まれる
  • 頂点  i は頂点集合 bit に含まれないが、頂点集合 add に含まれる
  • 頂点  i は頂点集合 bit, add のいずれにも含まれない

という 3 パターンしかない。したがって、考えられる状況は  3^{N} 通りなのだ。以上から、今回の bit DP の計算量が  O(3^{N}) であることがわかった。

コード

前処理として、以下の配列を求めている。

  • ok[bit] ← 頂点集合 bit がクリークかどうか
#include <bits/stdc++.h>
using namespace std;
template<class T> inline bool chmax(T& a, T b) { if (a < b) { a = b; return 1; } return 0; }
template<class T> inline bool chmin(T& a, T b) { if (a > b) { a = b; return 1; } return 0; }

int main() {
    int N, M;
    cin >> N >> M;
    vector<vector<int>> G(N, vector<int>(N, false));
    for (int i = 0; i < M; ++i) {
        int a, b; cin >> a >> b; --a, --b;
        G[a][b] = G[b][a] = true;
    }

    vector<bool> ok(1<<N, true);
    for (int bit = 0; bit < (1<<N); ++bit) {
        vector<int> vs;
        for (int i = 0; i < N; ++i) if (bit & (1<<i)) vs.push_back(i);
        for (int i = 0; i < vs.size() && ok[bit]; ++i) {
            for (int j = i+1; j < vs.size() && ok[bit]; ++j) {
                if (!G[vs[i]][vs[j]]) ok[bit] = false;
            }
        }
    }

    const int INF = 1<<29;
    vector<int> dp(1<<N, INF);
    dp[0] = 0;
    for (int bit = 0; bit < (1<<N); ++bit) {
        if (dp[bit] >= INF) continue;
        int cbit = (1<<N) - 1 - bit;
        for (int add = cbit; ; add = (add-1) & cbit) {
            if (!add) break;
            if (ok[add]) chmin(dp[bit|add], dp[bit] + 1);
        }
    }
    cout << dp[(1<<N)-1] << endl;
}

解法 (2):補グラフの彩色数

クリークといえば、補グラフをとれば「安定集合」になる。安定集合とは、

「頂点集合の部分集合であって、どの 2 頂点間にも辺がないようなもの」

のことを指す。クリークを見たら補グラフをとるのはありかもしれない。さて、もとのグラフで同一のグループの頂点は同じ色で塗り、異なるグループの頂点は異なる色で塗るとしたとき、

  • もとのグラフで条件を満たしている場合、同じ色の頂点間にはすべて辺があるので、補グラフでは辺彩色 (同じ色の頂点間には辺がない) になっている
  • 補グラフで辺彩色になっている場合、同じ色の頂点間にはすべて辺がないので、もとのグラフでは同じ色の頂点間にはすべて辺がある

というふうになっている。つまり、補グラフの彩色数を求めれば OK。補グラフの彩色数は次の記事で書いた。計算量は  O(N2^{N}) でできる!

drken1215.hatenablog.com

コード

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

// 彩色数
long long modpow(long long a, long long n, long long MOD) {
    long long res = 1;
    while (n > 0) {
        if (n & 1) res = res * a % MOD;
        a = a * a % MOD;
        n >>= 1;
    }
    return res;
}
int chromatic_number(const vector<vector<int> > &G) {
    const int MOD = 1000000007;
    int n = (int)G.size();
    vector<int> neighbor(n, 0);
    for (int i = 0; i < n; ++i) {
        int S = (1<<i);
        for (int j = 0; j < n; ++j)
            if (G[i][j])
                S |= (1<<j);
        neighbor[i] = S;
    }
    
    // I[S] := #. of inndepndet subset of S
    vector<int> I(1<<n);
    I[0] = 1;
    for (int S = 1; S < (1<<n); ++S) {
        int v = __builtin_ctz(S);
        I[S] = I[S & ~(1<<v)] + I[S & ~neighbor[v]];
    }
    int low = 0, high = n;
    while (high - low > 1) {
        int mid = (low + high) >> 1;
        
        // g[S] := #. of "k independent sets" which cover S just
        // f[S] := #. of "k independent sets" which consist of subseta of S
        // then
        //   f[S] = sum_{T in S} g(T)
        //   g[S] = sum_{T in S} (-1)^(|S|-|T|)f[T]
        long long g = 0;
        for (int S = 0; S < (1<<n); ++S) {
            if ((n - __builtin_popcount(S)) & 1) g -= modpow(I[S], mid, MOD);
            else g += modpow(I[S], mid, MOD);
            g = (g % MOD + MOD) % MOD;
        }
        if (g != 0) high = mid;
        else low = mid;
    }
    return high;
}

int main() {
    int N, M;
    cin >> N >> M;
    vector<vector<int>> G(N, vector<int>(N, 1));
    for (int i = 0; i < M; ++i) {
        int a, b; cin >> a >> b; --a, --b;
        G[a][b] = G[b][a] = 0;
    }
    cout << chromatic_number(G) << endl;
}