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

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

AtCoder ABC 128 C - Switches (緑色, 300 点)

bit 全探索

問題へのリンク

問題概要

 N 個のスイッチがある。スイッチによって  M 個の電球が点いたり消えたりする。

  • 電球  i k_i 個のスイッチに繋がっており、スイッチ  s_{i_{1}}, s_{i_{2}}, \dots, s_{i_{k_{i}}} のうち on になっているスイッチの個数を 2 で割った余りが  p_i に等しい時に点灯します。

全ての電球が点灯するようなスイッチの on/off の状態の組み合わせは何通りあるでしょうか。

制約

  •  1 \le N, M \le 10

考えたこと

まず、 N 個のスイッチの on/off の状態は  2^{N} 通りある。それを決めれば、各電球が点灯するかどうかを求めることができる (ちょっと複雑だけども...)

さて問題は、その  2^{N} 通りの場合をどうやって探索し尽くすかだが、以下の記事の bit 全探索のところがぴったりである。

qiita.com

さて、スイッチの on/off がわかれば、

  • 各電球  i について、 s_{i_{1}}, s_{i_{2}}, \dots, s_{i_{k_{i}}} のうち、on のスイッチが何個かを数える
  • それが偶数か奇数かによって、電球  i が点いているかどうかがわかる

という具合である。

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int N, M; cin >> N >> M;
    vector<vector<int> > s(M);
    for (int i = 0; i < M; ++i) {
        int k; cin >> k;
        for (int j = 0; j < k; ++j) {
            int a; cin >> a; --a;
            s[i].push_back(a);
        }
    }
    vector<int> p(M);
    for (int i = 0; i < M; ++i) cin >> p[i];
    
    long long res = 0;
    for (int bit = 0; bit < (1<<N); ++bit) {
        bool ok = true;
        for (int i = 0; i < M; ++i) {
            int con = 0;
            for (auto v : s[i]) {
                if (bit & (1<<v)) ++con;
            }
            if (con % 2 != p[i]) ok = false;
        }
        if (ok) ++res;
    }
    cout << res << endl;
}

番外編の別解: F2 体上の連立一次方程式の解の個数

番外編として、実はこの問題は

  •  1 \le N, M \le 300

とかでも解くことができる。連立一次方程式の解の個数に帰着させるのだ。今回の問題は

  •  x_{i} = 1 or  0 (スイッチ  i が点いているかどうか)

とすると各電球に対する制約条件は

  •  x_{s_{i_{1}}} + x_{s_{i_{2}}} + \dots + x_{s_{i_{k_{i}}}} ≡ p_{i}  \pmod 2

と書くことができるのだ。これによって、 M 本の制約の連立方程式となる。この解の個数を求めればよい。その求め方は以下の記事の通り。

drken1215.hatenablog.com

#include <iostream>
#include <vector>
#include <bitset>
using namespace std;

const int MAX_ROW = 510; // to be set appropriately
const int MAX_COL = 510; // to be set appropriately
struct BitMatrix {
    int H, W;
    bitset<MAX_COL> val[MAX_ROW];
    BitMatrix(int m = 1, int n = 1) : H(m), W(n) {}
    inline bitset<MAX_COL>& operator [] (int i) {return val[i];}
};

int GaussJordan(BitMatrix &A, bool is_extended = false) {
    int rank = 0;
    for (int col = 0; col < A.W; ++col) {
        if (is_extended && col == A.W - 1) break;
        int pivot = -1;
        for (int row = rank; row < A.H; ++row) {
            if (A[row][col]) {
                pivot = row;
                break;
            }
        }
        if (pivot == -1) continue;
        swap(A[pivot], A[rank]);
        for (int row = 0; row < A.H; ++row) {
            if (row != rank && A[row][col]) A[row] ^= A[rank];
        }
        ++rank;
    }
    return rank;
}

int linear_equation(BitMatrix A, vector<int> b, vector<int> &res) {
    int m = A.H, n = A.W;
    BitMatrix M(m, n + 1);
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) M[i][j] = A[i][j];
        M[i][n] = b[i];
    }
    int rank = GaussJordan(M, true);

    // check if it has no solution
    for (int row = rank; row < m; ++row) if (M[row][n]) return -1;

    // answer
    res.assign(n, 0);
    for (int i = 0; i < rank; ++i) res[i] = M[i][n];
    return rank;
}


int main() {
    int N, M; cin >> N >> M;
    BitMatrix A(M, N);
    vector<int> b(M);
    for (int i = 0; i < M; ++i) {
        int k; cin >> k;
        for (int j = 0; j < k; ++j) {
            int s; cin >> s; --s;
            A[i][s] = 1;
        }
    }
    for (int i = 0; i < M; ++i) {
        int p; cin >> p;
        b[i] = p;
    }
    vector<int> res;
    int rank = linear_equation(A, b, res);
    if (rank == -1) cout << 0 << endl;
    else cout << (1<<(N-rank)) << endl;
}

AtCoder ABC 127 F - Absolute Minima (黄色, 600 点)

超絶苦手系。でもこういうのパッとできるようにせな。

問題へのリンク

問題概要

関数  f(x) があります。 はじめ、これは定数関数  f(x)=0 です。

 Q 個のクエリが与えられるので、順番に処理してください。クエリは 2 種類あり、入力形式とクエリの内容は以下の通りです。

  • 更新クエリ 1 a b: 2整数  a, b が与えられるので、 g(x)=f(x)+|x−a|+b として  f(x) g(x) で置き換える。
  • 求値クエリ 2:  f(x) の最小値を与える  x、および  f(x) の最小値を出力する。ただし、そのような  x が複数存在する場合には最小の  x を答える。

制約

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

考えたこと

まず  b は完全に無視してよいね。あとで一律に足せば OK。というわけなので問題は、


 |x - a_{0}| + |x - a_{1}| + \dots + |x - a_{n-1}| の最小値と、最小となる  x を求めよ


という問題になるわけだ。そしてこれは実は有名問題なのだ。実は  a_{0}, a_{1}, \dots, a_{n-1} が小さい順にソートされていると仮定したとき、その中央値 (メディアン) になるのだ。

これは今までなんども出題されて来た有名事実だけど、一応証明してみる。簡単のため  n は奇数とする。

このとき下図 ( n = 7) のように

  •  x が左側に寄っているときは、右へ動かすと  f(x) の値を減らすことができる。例えば下図の上のケースの場合、 x \epsilon だけ増やすと  |x - a_{0}| \epsilon 増えるが、他の項は  \epsilon ずつ減るので、全体で  5\epsilon 減ることになる。

  •  x が右側に寄っているときも同様に、左へ動かすと  f(x) の値を減らすことができる

ということがわかる。

このバランスがとれるのが、ちょうどメディアンの場合だ。

 n が偶数の場合はメディアンは 2 つあって、その間の区間内はすべて最小値になる。

いずれにしても問題は


  • 更新クエリ: 要素  a を追加する

  • 求値クエリ: メディアンを求めよ (とそのときの  f(x) の値)


ということに帰着された。

floating median

更新クエリと、 K 番目の値を求めるクエリとがオンラインに来る状態でも  K が固定ならば 2 つの priority_queue で実現できる話は有名である。

メディアンであっても priority_queue でできる。

  • 小さい側半分を表すキュー (最大をとれるようにしておく)
  • 大きい側半分を表すキュー (最小をとれるようにしておく)

とを用意しておいて、両者の要素数を均衡させながら、値を入れ替えたりしていけば OK。これによって更新クエリを log オーダーでこなせる。

#include <iostream>
#include <vector>
#include <queue>
using namespace std;

const int MAX = 210000;
int Q;
long long type[MAX], a[MAX], b[MAX];

int main() {
    cin >> Q;

    long long sum = 0;
    priority_queue<long long> zen;
    priority_queue<long long, vector<long long>, greater<long long> > kou;
    long long zensum = 0;
    long long kousum = 0;
    
    for (int i = 0; i < Q; ++i) {
        cin >> type[i];
        if (type[i] == 1) {
            cin >> a[i] >> b[i];
            sum += b[i];

            if (zen.size() > kou.size()) {
                // kou に
                int t = zen.top();
                if (a[i] >= t) {
                    kou.push(a[i]); kousum += a[i];
                }
                else {
                    zen.pop(); zensum -= t;
                    zen.push(a[i]); zensum += a[i];
                    kou.push(t); kousum += t;
                }
            }
            else {
                // zen に
                if (zen.empty()) zen.push(a[i]), zensum += a[i];
                else {
                    int t = kou.top();
                    if (a[i] <= t) {
                        zen.push(a[i]); zensum += a[i];
                    }
                    else {
                        kou.pop(); kousum -= t;
                        kou.push(a[i]); kousum += a[i];
                        zen.push(t); zensum += t;
                    }
                }
            }
        }
        else {
            long long x = zen.top();
            long long res = (x * (int)zen.size() - zensum) + (kousum - x * (int)kou.size()) + sum;
            cout << x << " " << res << endl;
        }
    }
}

AtCoder ABC 127 E - Cell Distance (青色, 500 点)

何をしたらよいかがすぐに見えてくる典型だけど、かなり手こずった

問題へのリンク

問題概要

整数  N, M, K があたえられる。
 N \times M のグリッドから  K 個を選んでコマを置く  {}_{NM}{\rm C}_{K} 通りの方法それぞれに対して

  •  {}_{K}{\rm C}_{2} 通りのコマにペアそれぞれについてのマンハッタン距離の総和

を求め、その総和を求めよ (1000000007 で割ったあまりで)。

制約

  •  2 \le NM \le 2 \times 10^{5}

各要素に注目

この手の「A 全体を調べたときのの B の総和」みたいな問題では、やるべきことはもうほぼ決まっている。これを B 全体を動かすのではなく、B の各要素 b ごとに A 全体を調べたときの総和を b ごとに累計するのだ。今回の問題で言えば


 {}_{NM}{\rm C}_{2} 個のマスのペアそれぞれについて、そのマンハッタン距離が何回合計されるかを考える


という風に考えると見通しがよくなる。さて、まずはこの回数自体はすぐにわかる。すなわち  K 個選ぶうちの  2 個が決まっているので、残りを選べばよく

  •  {}_{NM-2}{\rm C}_{K-2}

である。よって問題は、


 NM マスから  2 個選ぶペアそれぞれについてのマンハッタン距離の総和を求めよ


という問題に帰着されたのだ (この答えに  {}_{NM-2}{\rm C}_{K-2} を掛ければよい)

x 軸と y 軸を独立に

しかしまだ、このままでは  O(N^{2}M^{2}) の計算時間がかかってしまう。ここで、次なる方針は

  • x 軸方向と y 軸方向とに分けて考える

というもの。マンハッタン距離に関する問題でしばしば見られる。 {}_{NM}{\rm C}_{2} 通りのうち、

  • x 軸方向の距離が  i
  • y 軸方向の距離が  j

となっているものが何通りあるかを数えることにする。まず

  • x 軸方向が  N-i 通り
  • y 軸方向が  M-j 通り

あり、 i j 0 でない場合にはさらに  2 倍になる。よって

  •  i j 0 でないとき、 2(N-i)(M-j)(i+j)
  • そうでないとき  (N-i)(M-j)(i+j)

を各  i,j に対して合計すれば OK。

#include <iostream>
#include <vector>
using namespace std;


// modint: mod 計算を int を扱うように扱える構造体
template<int MOD> struct Fp {
    long long val;
    constexpr Fp(long long v = 0) noexcept : val(v % MOD) {
        if (val < 0) v += MOD;
    }
    constexpr int getmod() { 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 ostream& operator << (ostream &os, const Fp<MOD>& x) noexcept {
        return os << x.val;
    }
    friend constexpr istream& operator >> (istream &is, Fp<MOD>& x) noexcept {
        return is >> x.val;
    }
    friend constexpr Fp<MOD> modpow(const Fp<MOD> &a, long long n) noexcept {
        if (n == 0) return 1;
        auto t = modpow(a, n / 2);
        t = t * t;
        if (n & 1) t = t * a;
        return t;
    }
};

// 二項係数ライブラリ
template<class T> struct BiCoef {
    vector<T> fact_, inv_, finv_;
    constexpr BiCoef() {}
    constexpr BiCoef(int n) noexcept : fact_(n, 1), inv_(n, 1), finv_(n, 1) {
        init(n);
    }
    constexpr void init(int n) noexcept {
        fact_.assign(n, 1), inv_.assign(n, 1), finv_.assign(n, 1);
        int MOD = fact_[0].getmod();
        for(int i = 2; i < n; i++){
            fact_[i] = fact_[i-1] * i;
            inv_[i] = -inv_[MOD%i] * (MOD/i);
            finv_[i] = finv_[i-1] * inv_[i];
        }
    }
    constexpr T com(int n, int k) const noexcept {
        if (n < k || n < 0 || k < 0) return 0;
        return fact_[n] * finv_[k] * finv_[n-k];
    }
    constexpr T fact(int n) const noexcept {
        if (n < 0) return 0;
        return fact_[n];
    }
    constexpr T inv(int n) const noexcept {
        if (n < 0) return 0;
        return inv_[n];
    }
    constexpr T finv(int n) const noexcept {
        if (n < 0) return 0;
        return finv_[n];
    }
};

const int MOD = 1000000007;
using mint = Fp<MOD>;
BiCoef<mint> bc;


int main() {
    long long N, M, K; cin >> N >> M >> K;
    bc.init(510000);
    mint sum = 0;
    for (int i = 0; i <= N-1; ++i) {
        for (int j = 0; j <= M-1; ++j) {
            mint tmp = mint(N - i) * mint(M - j) * mint(i + j);
            if (i != 0 && j != 0) tmp *= 2;
            sum += tmp;
        }
    }
    cout << sum * bc.com(N*M-2, K-2) << endl;
}

AtCoder ABC 127 D - Integer Cards (緑色, 400 点)

混ぜてソートは賢すぎる!!!惚れた!!!

問題へのリンク

問題概要

 N 枚のカードにそれぞれ  A_1, A_2, \dots, A_N の数値が書かれている。あなたは、 j = 1, 2, \dots, M について順に以下の操作を 1 回ずつ行います。

  • カードを  B_j 枚まで選ぶ(0 枚でもよい)。選んだカードに書かれている整数をそれぞれ  C_j に書き換える。

操作終了後のカードの数値の総和の最大値を求めよ。

制約条件

  •  1 \le N, M \le 10^{5}

考えたこと 1

ややこしいけど、こういうのはちゃんと順を追って考えれば行ける。とりあえず思うのは

  • もともとが小さいやつを優先的に書き換えたい
  • なるべく大きいやつに書き換えたい

このあたりのことをきちんと整理しながら考える。具体的には「変数を固定して考える」という戦略が良さそうに思える。まず、 N 枚のカードのうち  K 枚を書き換えるとすると、

  •  A_{1}, A_{2}, \dots, A_{N} のうち小さい順に  K 枚を書き換える
  •  (C_{1}, \dots, C_{1}), (C_{2}, \dots, C_{2}), \dots, (C_{M}, \dots, C_{M}) のうち大きい順に  K 枚を書き換える

とすればよいことがわかる。まとめると

  •  A_{1}, A_{2}, \dots, A_{N} を小さい順にソート
  •  (C_{1}, \dots, C_{1}), (C_{2}, \dots, C_{2}), \dots, (C_{M}, \dots, C_{M}) を大きい順にソート
  •  i = 0, 1, ..., N-1 に対して、 \max ( A i 番目,  C i 番目) を合計

したものが答えになる。ただし、 C の個数が  N に満たない場合は、足りない部分は単純に  A の方を足していく。

#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
using namespace std;
using pll = pair<long long, long long>;

int main() {
    int N, M; cin >> N >> M;
    vector<long long> A(N), B(M), C(M);
    for (int i = 0; i < N; ++i) cin >> A[i];
    sort(A.begin(), A.end());
    B.resize(M);
    C.resize(M);
    for (int i = 0; i < M; ++i) cin >> B[i] >> C[i];

    // C をソート (B をまとめて)
    vector<int> id(M);
    iota(id.begin(), id.end(), 0);
    sort(id.begin(), id.end(), [&](int i, int j) {
            return C[i] > C[j];});

    // A (小さい順) と C (大きい順) とを比べて大きい方を足していく
    long long sum = 0;
    long long K = 0;
    for (auto i : id) {
        for (int j = 0; j < B[i]; ++j) {
            if (K >= N) break;
            sum += max(A[K++], C[i]);  
        }
    }
    for (int i = K; i < N; ++i) sum += A[i];        
    cout << sum << endl;
}

解法 2

実はすごく簡単に

  •  A_{1}, A_{2}, \dots, A_{N}
  •  (C_{1}, \dots, C_{1}), (C_{2}, \dots, C_{2}), \dots, (C_{M}, \dots, C_{M})

を全部混ぜて大きい順に  N 個とった合計値で OK。このとき全部混ぜたものの個数は  O(NM) になってまともに扱えないが、ランレングス圧縮した世界で考えれば OK。

#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
using namespace std;
using pll = pair<long long, long long>;

int main() {
    int N, M; cin >> N >> M;
    vector<pll> v;
    for (int i = 0; i < N; ++i) {
        int a; cin >> a;
        v.push_back({a, 1});
    }
    for (int i = 0; i < M; ++i) {
        int b, c; cin >> b >> c;
        v.push_back({c, b});
    }
    sort(v.begin(), v.end(), greater<pll>());

    long long num = 0;
    long long res = 0;
    for (auto p : v) {
        if (num + p.second <= N) {
            res += p.first * p.second;
            num += p.second;
        }
        else {
            res += p.first * (N - num);
            num = N;
            break;
        }
    }
    cout << res << endl;
}

AtCoder ABC 127 C - Prison (灰色, 300 点)

条件反射でいもす法をしたけれど、もっと楽にできた

問題へのリンク

そして手前味噌ながら類題

atcoder.jp

問題概要

 M 個の区間  \lbrack l_i, r_i \rbrack があたえられる。 1 \le l_i \le r_i \le N を満たしている。

この区間が  M 重に交わっている部分の長さを求めよ。

制約

 1 \le N, M \le 10^{5}

解法 1:区間の交差

区間  \lbrack a, b \rbrack \lbrack c, d \rbrack の交差は実は簡単にかけて

  •  \lbrack \max(a, c), \min(b, d) \rbrack (左端が  \max(a, c) で右端が  \min(b, d))

になる。面倒な場合分けが要らない!!!
ただし  \max(a, c) \gt \min(b, d) の場合は「交差しない」ということになる。

このことを利用して、 N 個の区間の交差は

  •  \lbrack \max(l_1, l_2, \dots, l_N), \min(r_1, r_2, \dots, r_N) \rbrack

と表せる。これを  \lbrack L, R \rbrack と表したとき、

  •  L \le R なら、 R - L + 1 が答え
  •  L \gt R なら、 0 が答え

となる。

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int N, M; cin >> N >> M;
    int left = 1, right = N;
    for (int i = 0; i < M; ++i) {
        int a, b; cin >> a >> b;
        left = max(left, a);
        right = min(right, b);
    }
    cout << max(right - left + 1, 0) << endl;
}

解法 2:いもす法

この問題の解説とほぼ同じことをすれば OK。

atcoder.jp

そして最後に、「出来上がった配列のうち、値が  M になっている部分を数える」とすればよい。

なお、下のコードで、 l, r については 0-indexed で考えています。一方で、問題文で与えられているのは左閉区間・右閉区間ですが、いもす法を適用するために左閉区間・右開区間にしています。よって、

--l
--r
++r

をすることになって、 r については結果的に入力のままとしています。

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int N, M; cin >> N >> M;
    vector<int> G(N+1, 0);
    for (int i = 0; i < M; ++i) {
        int l, r; cin >> l >> r; --l;
        G[l]++;
        G[r]--;
    }
    for (int i = 0; i < N; ++i) G[i+1] += G[i];
    int res = 0;
    for (int i = 0; i < N; ++i) if (G[i] == M) ++res;
    cout << res << endl;
}