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

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

AtCoder ABC 350 B - Dentist Aoki (6Q, 灰色, 200 点)

「集計処理」の基本問題!

問題概要 (意訳)

 N 個の LED が最初はすべて光っている。

 Q 回の処理を行う。 i 回目の処理では  T_{i} 番目の LED の状態を flip する (光っていたら消して、消えていたら光らせる)。

最終的に何個の LED が光っているかを求めよ。

解法

次の配列を用意しよう。


  • haeteru[v] v 番目の LED が光っているとき 1、そうでないとき 0

このとき、 T_{i} 番目の LED を flip する操作は次のように書ける。

haeteru[v] = 1 - haeteru[v];

この実装によって、0 は 1 になり、1 は 0 になる。 Q 回の処理を終えたあとは、 1 の個数を数えれば OK。

コード

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

int main() {
    int N, Q;
    cin >> N >> Q;
    
    vector<int> haeteru(N, 1);
    for (int i = 0; i < Q; ++i) {
        int T;
        cin >> T;
        --T;
        haeteru[T] = 1 - haeteru[T];
    }
    
    int res = 0;
    for (int i = 0; i < N; ++i) {
        res += haeteru[i];
    }
    cout << res << endl;
}

AtCoder ARC 176 C - Max Permutation (3D, 黄色, 700 点)

これは面白かった。

問題概要

 1, 2, \dots, N の順列であって、以下の  M 個の条件を満たすものの個数を 998244353 で割った余りを求めよ。

  •  j = 1, 2, \dots, M について、 \max(P_{A_{j}}, P_{B_{j}}) = C_{j} である

制約

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

考えたこと

グラフの問題として考えることにした。つまり、0-indexed で表現すると


頂点数   N、辺数  M であるグラフが与えられる。各辺には  0 以上  N-1 以下の重みがついている。

各頂点に  0, 1, \dots, N-1 の値を割り振る方法であって、どの辺についても両端の頂点の値の最大値が辺の重みに一致する方法の個数を求めよ。


重みが大きい方から順に考えることとした。次のように考えることができた。

また、常に「未使用の孤立頂点の個数」を表す値  s を管理しておくこととする。初期状態では

  • res = 1
  • s = (初期グラフの孤立点の個数)

としておく。


 x = N-1, N-2, \dots, 0 について順に考えて

  • 重み  x の辺が存在しないとき:
    • もし s = 0 ならば、答えは問答無用で 0 通りである
    • そうでないならば、res *= s, --s とする
  • 重み  x の辺がちょうど 1 本存在するとき:
    • その時点でのグラフについて、もしその辺の両端の次数がともに 2 以上ならば、答えは問答無用で 0 通りである
    • そうでないとき、重み  x の頂点を以下のように決定し、その頂点を削除する (このとき、孤立点が新たに誕生するならば ++s とする)
      • 片方の頂点の次数が 1、他方の頂点の次数が 2 以上のとき、次数 1 の頂点の値を  x にする
      • 両端の頂点の次数が 1 のとき、いずれかの頂点の値を  x とする (`res *= 2' とする)
  • 重み  v の辺が 2 本以上存在するとき:
    • それらの辺がスターグラフを形成していないならば、問答無用で 0 通りである
    • そうでないとき、スターグラフの中心の頂点を  v として、以下のようにする
      • 頂点  v に重み  v 未満の辺が接続しているならば、問答無用で 0 通りである
      • そうでないとき、頂点  v の重みを  x の頂点として、その頂点  v を削除する (このとき、孤立点が新たに誕生するならば ++s とする)

こうして重みが  v = N-1, N-2, \dots, 0 の頂点を順に決定していく。計算量は  O(N) となる。

コード

#include <bits/stdc++.h>
using namespace std;
using pint = pair<int, int>;

// modint
template<int MOD> struct Fp {
    // inner value
    long long val;
    
    // constructor
    constexpr Fp() : val(0) { }
    constexpr Fp(long long v) : val(v % MOD) {
        if (val < 0) val += MOD;
    }
    constexpr long long get() const { return val; }
    constexpr int get_mod() const { return MOD; }
    
    // arithmetic operators
    constexpr Fp operator + () const { return Fp(*this); }
    constexpr Fp operator - () const { return Fp(0) - Fp(*this); }
    constexpr Fp operator + (const Fp &r) const { return Fp(*this) += r; }
    constexpr Fp operator - (const Fp &r) const { return Fp(*this) -= r; }
    constexpr Fp operator * (const Fp &r) const { return Fp(*this) *= r; }
    constexpr Fp operator / (const Fp &r) const { return Fp(*this) /= r; }
    constexpr Fp& operator += (const Fp &r) {
        val += r.val;
        if (val >= MOD) val -= MOD;
        return *this;
    }
    constexpr Fp& operator -= (const Fp &r) {
        val -= r.val;
        if (val < 0) val += MOD;
        return *this;
    }
    constexpr Fp& operator *= (const Fp &r) {
        val = val * r.val % MOD;
        return *this;
    }
    constexpr Fp& operator /= (const Fp &r) {
        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 Fp pow(long long n) const {
        Fp res(1), mul(*this);
        while (n > 0) {
            if (n & 1) res *= mul;
            mul *= mul;
            n >>= 1;
        }
        return res;
    }
    constexpr Fp inv() const {
        Fp res(1), div(*this);
        return res / div;
    }

    // other operators
    constexpr bool operator == (const Fp &r) const {
        return this->val == r.val;
    }
    constexpr bool operator != (const Fp &r) const {
        return this->val != r.val;
    }
    constexpr Fp& operator ++ () {
        ++val;
        if (val >= MOD) val -= MOD;
        return *this;
    }
    constexpr Fp& operator -- () {
        if (val == 0) val += MOD;
        --val;
        return *this;
    }
    constexpr Fp operator ++ (int) const {
        Fp res = *this;
        ++*this;
        return res;
    }
    constexpr Fp operator -- (int) const {
        Fp res = *this;
        --*this;
        return res;
    }
    friend constexpr istream& operator >> (istream &is, Fp<MOD> &x) {
        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) {
        return os << x.val;
    }
    friend constexpr Fp<MOD> pow(const Fp<MOD> &r, long long n) {
        return r.pow(n);
    }
    friend constexpr Fp<MOD> inv(const Fp<MOD> &r) {
        return r.inv();
    }
};

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

int main() {
    int N, M;
    cin >> N >> M;
    
    // グラフを求める
    vector<vector<pint>> G(N);
    vector<int> deg(N, 0);
    vector<vector<pint>> edges(N);
    for (int i = 0; i < M; ++i) {
        int a, b, x;
        cin >> a >> b >> x;
        --a, --b, --x;
        G[a].emplace_back(b, x), G[b].emplace_back(a, x);
        ++deg[a], ++deg[b];
        edges[x].emplace_back(a, b);
    }
    
    // 孤立点の個数を求める
    int solitude = 0;
    for (int i = 0; i < N; ++i) {
        if (deg[i] == 0) ++solitude;
    }
    
    // 頂点 v を削除する関数
    auto pop = [&](int v) -> void {
        deg[v] = 0;
        for (auto e : G[v]) {
            --deg[e.first];
            if (deg[e.first] == 0) {
                ++solitude;
            }
        }
    };
    
    auto solve = [&]() -> mint {
        mint res = 1;
        
        for (int x = N-1; x >= 0; --x) {
            if (edges[x].empty()) {
                if (solitude == 0) return mint(0);
                res *= solitude;
                --solitude;
            } else {
                const auto &vec = edges[x];
                if (vec.size() == 1) {
                    int a = vec[0].first, b = vec[0].second;
                    if (deg[a] == 1 && deg[b] == 1) {
                        res *= 2;
                        pop(a);
                    } else if (deg[a] == 1) {
                        pop(a);
                    } else if (deg[b] == 1) {
                        pop(b);
                    } else {
                        return mint(0);
                    }
                } else {
                    map<int, int> ma;
                    for (auto [a, b] : vec) {
                        ++ma[a];
                        ++ma[b];
                    }
                    
                    int node = -1;
                    for (auto [val, num] : ma) {
                        if (num != 1) {
                            if (node != -1) return mint(0);
                            node = val;
                        }
                    }
                    
                    if (deg[node] != vec.size()) return mint(0);
                    pop(node);
                }
            }
        }
        return res;
    };
    cout << solve() << endl;
}

AtCoder ARC 176 A - 01 Matrix Again (1D, 水色, 400 点)

大敗してしまったので自戒を込めて。

問題概要

整数  N, M が与えられる。 N \times N のグリッドであって、以下の条件を満たすものを構築せよ。

  • 各マスの値は 0 または 1 である
  •  M 個のマス  (A_{0}, B_{0}), \dots, (A_{M-1}, B_{M-1}) の値はいずれも 1 である
  • 行和はすべて  M である
  • 列和はすべて  M である

制約

  •  N \le 10^{5}
  •  M \le \min(N, 10)

考えたこと

たとえば  N = 5, M = 3 のときを考える。

以下の 5 種類の盤面は、

  • いずれも「行和 = 1」「列和 = 1」である
  • 盤面間で、1 であるマスが disjoint である

という性質を満たすことに着目しよう (赤マスを 1、白マスを 0 とする)。

これら  5 個の盤面の中から、 3 種類選んで結合する (各マスについて和をとる) と、「行和 =  3」「列和 =  3」となるのだ。

ここで、 5 種類の選び方を柔軟に変えれば、どんな入力  (A_{0}, B_{0}), (A_{1}, B_{1}), (A_{2}, B_{2}) にも対応できる。

  • マス  (A_{0}, B_{0}) が赤色になっている盤面
  • マス  (A_{1}, B_{1}) が赤色になっている盤面
  • マス  (A_{2}, B_{2}) が赤色になっている盤面

をそれぞれ選んでくればよいのだ。仮に被ったとしても、他のものを持ってくれば OK。

以上の方法は、一般の  N, M に対しても適用できる。なお、この解法は実は公式解説と同じである。

コード

#include <bits/stdc++.h>
using namespace std;
using pint = pair<int,int>;

int main() {
    int N, M;
    cin >> N >> M;
    
    // M 種類の盤面を選ぶ
    // 具体的にはマス (i, j) に対して、i + j mod N の値で類別する
    set<int> S;
    for (int i = 0; i < M; ++i) {
        int A, B;
        cin >> A >> B;
        --A, --B;
        S.insert((A + B) % N);
    }
    // 被った場合の処理
    for (int i = 0; i < N; ++i) {
        if (S.size() == M) break;
        if (!S.count(i)) S.insert(i);
    }
    
    // M 種類の盤面を結合する
    vector<pint> res;
    for (int i = 0; i < N; ++i) {
        for (auto r : S) {
            res.emplace_back(i, (r - i + N) % N);
        }
    }
    
    // 出力する
    cout << res.size() << endl;
    for (auto [x, y] : res) {
        cout << x+1 << " " << y+1 << endl;
    }
}

AtCoder ABC 350 A - Past ABCs (7Q, 灰色, 100 点)

構文解析の初歩ですね。C++ なら、scanf() 関数を使うと楽ですね。

問題概要

"ABC197" のような長さ 6 の文字列  S が与えられる。

何回目の ABC であるかを判定し、それが 316 回を除く、1〜349 回のいずれかであるかどうかを判定せよ。

解法

C++ の場合、cin よりも scanf() を使う方が楽。int 型変数 N を用意して、

scanf("ABC%d", &N);

というようにして、数値情報のみを取得できる。この値が 316 を除く 1〜349 のいずれかであるかを判定すればよい。

コード

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

int main() {
    int N;
    scanf("ABC%d", &N);
    if (N >= 1 && N <= 349 && N != 316)
        cout << "Yes" << endl;
    else
        cout << "No" << endl;
}

AtCoder ABC 350 G - Mediator (3D, 黄色, 600 点)

本当に色んな解法がある問題っぽい!!

問題概要

頂点  1,2,\dots,N N 頂点からなる無向グラフがあり、最初は辺がない。以下の 2 種類のオンラインクエリに答えよ。

  • クエリタイプ 1:頂点  u, v 間に辺を結ぶ
  • クエリタイプ 2:頂点  u, v の双方に隣接する頂点があるならその番号を答え、無ければ 0 と答える

なお、常にグラフが森であることが保証される。

制約

  •  N, Q \le 10^{5}

解法 (1):マージテク

最初、Union-Find において「経路圧縮」をなくせば所望の処理ができると思い込んでしまった。しかし、Union-Find では「連結成分の根同士が親子関係になる」ように辺を張るのだった。今回は、辺を張るペアが頂点  u, v に固定されていて、これらが各連結成分の根とは限らないので上手くいかない。

しかし、実は次のように少し工夫することで上手く行く。


  1. 適宜、頂点  u, v を入れ替えて、「頂点  u を含む連結成分のサイズ > 頂点  v を含む連結成分のサイズ」となるようにする
  2. 頂点  v を含む連結成分について、頂点  v が根となるように DFS して、頂点  v を根とする根付き木にする
  3. 頂点  v の親を頂点  u にする

このとき、マージテクにより、「どの頂点についても DFS によって探索されるのは高々  O(\log N) 回である」ことが言える。

よって、全体の計算量は  O(Q + N \log N) となる。

コード

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

// Union-Find
struct UnionFind {
    // core member
    vector<int> par, nex;

    // constructor
    UnionFind() { }
    UnionFind(int N) : par(N, -1), nex(N) {
        init(N);
    }
    void init(int N) {
        par.assign(N, -1);
        nex.resize(N);
        for (int i = 0; i < N; ++i) nex[i] = i;
    }
    
    // core methods
    int root(int x) {
        if (par[x] < 0) return x;
        else return par[x] = root(par[x]);
    }
    
    bool same(int x, int y) {
        return root(x) == root(y);
    }
    
    bool merge(int x, int y) {
        x = root(x), y = root(y);
        if (x == y) return false;
        if (par[x] > par[y]) swap(x, y); // merge technique
        par[x] += par[y];
        par[y] = x;
        swap(nex[x], nex[y]);
        return true;
    }
    
    int size(int x) {
        return -par[root(x)];
    }
    
    // get group
    vector<int> group(int x) {
        vector<int> res({x});
        while (nex[res.back()] != x) res.push_back(nex[res.back()]);
        return res;
    }
    vector<vector<int>> groups() {
        vector<vector<int>> member(par.size());
        for (int v = 0; v < (int)par.size(); ++v) {
            member[root(v)].push_back(v);
        }
        vector<vector<int>> res;
        for (int v = 0; v < (int)par.size(); ++v) {
            if (!member[v].empty()) res.push_back(member[v]);
        }
        return res;
    }
    
    // debug
    friend ostream& operator << (ostream &s, UnionFind uf) {
        const vector<vector<int>> &gs = uf.groups();
        for (const vector<int> &g : gs) {
            s << "group: ";
            for (int v : g) s << v << " ";
            s << endl;
        }
        return s;
    }
};


const int MOD = 998244353;

int main() {
    int N, Q;
    cin >> N >> Q;
    
    vector<vector<int>> G(N);
    vector<int> par(N, -1);
    UnionFind uf(N);
    
    auto dfs = [&](auto dfs, int v, int p = -1) -> void {
        if (p != -1) par[v] = p;
        for (auto ch : G[v]) {
            if (ch == p) continue;
            dfs(dfs, ch, v);
        }
    };
    
    long long res = 0;
    while (Q--) {
        long long a, b, c;
        cin >> a >> b >> c;
        
        long long A = (a * (res + 1) % MOD) % 2;
        long long u = (b * (res + 1) % MOD) % N;
        long long v = (c * (res + 1) % MOD) % N;
        
        if (A == 0) {
            if (uf.size(u) < uf.size(v)) swap(u, v);
            dfs(dfs, v);
            par[v] = u;
            uf.merge(u, v);
            G[u].push_back(v);
            G[v].push_back(u);
        } else {
            if (par[u] != -1 && par[par[u]] == v) {
                res = par[u] + 1;
            } else if (par[v] != -1 && par[par[v]] == u) {
                res = par[v] + 1;
            } else if (par[u] != -1 && par[u] == par[v]) {
                res = par[u] + 1;
            } else {
                res = 0;
            }
            cout << res << endl;
        }
    }
}