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

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

JOIG 2021 B - 巻物 (AOJ 0702, 難易度 2)

文字列の処理を練習する問題ですね

問題概要

 N 文字の文字列  S が与えられます。 S は英文字のみからなります。

 S K 文字目以降について、

  • 大文字ならば小文字に
  • 小文字ならば大文字に

なるように変換してくだだい。

制約

  •  1 \le K \le N \le 100

考えたこと

この問題では、次のような処理をすればよいでしょう。

for (int i = K - 1; i < N; ++i) {
    S[i] を大文字・小文字を入れ替える
}

ここで、i = K から始めるのではなく、i = K - 1 から始めることに注意しましょう。 K は 1-indexed で与えられますが、プログラミングでは 0-indexed で考えることが多いからです。

大文字と小文字の変換方法

最後に、大文字と小文字の変換方法を考えましょう。最初に小文字を大文字に変換する方法を考えます。まず文字 c が小文字かどうかは

if (c >= 'a' && c <= 'z')

というようにして判定できます。その上で大文字にするためには

c += 'A' - 'a';

というようにすることが考えられます。大文字から小文字への変換も同様に行えます。

コード

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

int main() {
    int N, K;
    string S;
    cin >> N >> K >> S;

    for (int i = K-1; i < N; ++i) {
        // 小文字を大文字に変換
        if (S[i] >= 'a' && S[i] <= 'z') {
            S[i] += 'A' - 'a';
        }
        // 大文字を小文字に変換
        else {
            S[i] -= 'A' - 'a';
        }
    }
    cout << S << endl;
}

JOIG 2021 A - 金平糖 (AOJ 0701, 難易度 1)

入出力の練習をしましょう。

問題概要

3 人の生徒がそれぞれ  A 個、 B 個、 C 個の金平糖をもらいました。

これからそれぞれの生徒たちに何個かの金平糖を追加で渡すことで、3 人の生徒がもらった金平糖の個数が等しくなるようにしたいとします。

追加で必要な金平糖の個数の最小値を求めてください。

制約

  •  1 \le A, B, C \le 100

考えたこと

たとえば  (A, B, C) = (4, 9, 6) としてみましょう。このとき、最もたくさん食べたのは真ん中の生徒の 9 個です。

他の 2 人の生徒の量をそれに合わせればよいでしょう。よって

(9 - 4) + (9 - 6) = 8 個

が答えとなります。

さらに整理

さらに整理すると、次のように考えられます。

  • 3 人の最大値を  M = \max(A, B, C) とする
  • 3 人の総和を  S = A + B + C とする

このとき答えは、 M \times 3 - S となります。

コード

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

int main() {
    // 入力
    int A, B, C;
    cin >> A >> B >> C;

    // 最大値を求める
    int M = max({A, B, C});

    // 総和を求める
    int S = A + B + C;

    // 最大値 × 3 - 総和が答え
    cout << M * 3 - S << endl;
}       

square869120Contest #5 B - Emblem

競プロ典型90問の問題 001 の類題。「最大値の最小化」をする二分探索の典型問題ですね。

問題概要

二次元平面上に  N 個の円が与えられます。またそれらとは別に  M 個の点が与えられます。円同士は互いに交わることはなく、点が円に包含されることもありません。

今、 M 個の点それぞれを中心とした円を描いています。ただし、そうしてできる  N+M 個の円のうち、どの 2 つも交差関係にも包含関係にもならないことはないようにします (外接するまでは許容します)。

このとき、 N + M 個の円の半径の最小値として考えられる最大値を求めてください。

なお、許容誤差は  10^{-6} とします。

制約

  •  0 \le N, M \le 100

最小値の最大化

今回の問題は「最小値の最大化」や「最大値の最小化」といえる枠組みになっています。そのような問題では、判定問題に帰着して二分探索に持ち込むことが有効です。今回は次のような問題を考えてみましょう。


 N + M 個の円の半径の最小値を  x 以上にすることは可能か?


この答えが Yes であるような  x の最大値が答えになります。そしてこの問題は次のように言い換えられます。一般に「最小値が  x 以上」は「すべて  x 以上」と言い換えられます。このような言い換えはとても重要な典型です。


 N + M 個の円の半径をすべて  x 以上にすることは可能か?


この判定問題を解いていきましょう。

判定問題

この判定問題を解くためには、実際に、

  • もともとの  N 個の円
  • 新たに  M 個の点を中心とする半径  x の円

を考えて、それら  N+M 個の円が互いに交差関係にも包含関係にもならないかどうかを確認すればよいでしょう (新たな円の半径を  x より大きくする必要はないことに注意しましょう)。

さて、一般に

  • 中心が  (x_{1}, y_{1})、半径が  r_{1} の円
  • 中心が  (x_{2}, y_{2})、半径が  r_{2} の円

が交差関係にも包含関係にもならないようにする条件を考えましょう。まず、2 つの円の中心間の距離を

 D = \sqrt((x_{1} - x_{2})^{2} + (y_{1} - y_{2})^{2})

としましょう。このとき、条件は次のように記述できます。


 D \ge r_{1} + r_{2}


この不等式がイコールになるとき、ちょうど 2 円が外接します。それよりも中心間距離  D が大きくなると、2 円は離れていくことになります。

以上より、2 円が交差関係にも包含関係にもないための条件が求められました。よって  N+M 個の円から 2 つ選ぶすべての組合せに対して、交差関係にも包含関係にもないかどうかを判定していけばよいでしょう。

まとめ

判定問題にして二分探索に帰着します。判定問題は、 O( (N + M)^{2} ) の計算量で解けます。十分間に合います。

なお、もともとの円の半径の最小値よりも大きい答えにすることはできないことに注意しましょう。そこで以下のコードでは、もともとの円の半径の最小値を二分探索の区間の上側 right の初期値としています。

また、二分探索を用いずに解くこともできるようです。

コード

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

int main() {
    // 入力
    int N, M;
    cin >> N >> M;
    double mi = 10000;  // もともとの円の半径の最小値
    vector<double> x(N + M), y(N + M), r(N);
    for (int i = 0; i < N; ++i) {
        cin >> x[i] >> y[i] >> r[i];
        mi = min(mi, r[i]);
    }
    for (int i = 0; i < M; ++i) cin >> x[i + N] >> y[i + N];

    // 二分探索の判定
    auto check = [&](double d) -> bool {
        for (int i = 0; i < N + M; ++i) {
            for (int j = i + 1; j < N + M; ++j) {
                double D = sqrt((x[i] - x[j]) * (x[i] - x[j])
                                + (y[i] - y[j]) * (y[i] - y[j]));
                double r1 = (i < N ? r[i] : d);
                double r2 = (j < N ? r[j] : d);
                if (D < r1 + r2) return false;
            }
        }
        return true;
    };

    // 二分探索
    double left = 0, right = mi;
    for (int _ = 0; _ < 100; ++_) {
        double mid = (left + right) / 2;
        if (check(mid)) left = mid;
        else right = mid;
    }

    cout << fixed << setprecision(10) << left << endl;
}       

yukicoder No.763 Noelちゃんと木遊び

いわゆる「木上の最大安定集合問題」です! 超典型問題です。

問題概要

頂点数  N の木が与えられます。木からいくつかの頂点を選んで削除します。その結果、木はいくつかの連結成分に分かれます。

分かれた連結成分の個数の最大値を求めてください。

制約

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

最大安定集合問題

さて、今回の問題は、実は「木上の最大安定集合問題」に一致します。

安定集合とは、グラフの頂点集合の部分集合であって「どの 2 頂点も隣接しない」という条件を満たすものをいいます。最大安定集合問題は、最大サイズの安定集合を求める問題です。

最大安定集合問題は一般には NP 困難であり、解決は困難です。しかしたとえば二部グラフであれば二部マッチングに帰着して解けることが知られています。

木も二部グラフですから、木上の最大安定集合問題ももちろん二部マッチングで解くこともできます。しかし木であれば、より簡潔な Greedy 解法をとることができます。

葉から考える Greedy

木に関する問題では、葉から考えていく考察によってうまくいくことが多々あります。

drken1215.hatenablog.com

実は木の葉を貪欲に選んでいく方法で解くことができます。

木の葉頂点  v を一つとり、「頂点  v を含まない安定集合  S があったとき、そのサイズを減らすことなく、頂点  v を含む安定集合へと変形できること」を示しましょう。葉頂点  v に隣接する頂点を  u とします。

  •  S u を含まないならば、 S v を追加したものも安定集合となっている
  •  S u を含むならば、 S から  u を除去して代わりに  v を追加したものも安定集合となっている

というようになります。いずれにしても、安定集合のサイズを減らすことなく、葉頂点  v を含むように変形できることがわかりました。

以上より、木の葉を貪欲に選び、その葉と葉に隣接する頂点を除去していく方法によって最大安定集合を求めることができることがわかりました。

コード

実装上は、木 DP の要領で、木の根を 1 つ決めて各部分木について考えていくのが有効です。計算量は  O(N) となります。

#include <iostream>
#include <vector>
using namespace std;
using Graph = vector<vector<int>>;

vector<bool> used;
void rec(const Graph &G, int v, int p = -1) {
    // 子頂点の中に採用済みの頂点があるかどうか
    bool exist = false;
    for (auto ch: G[v]) {
        if (ch == p) continue;

        // 再帰的探索
        rec(G, ch, v);
        if (used[ch]) exist = true;
    }

    // 子頂点の中に採用済みの頂点がなければ採用する
    if (!exist) used[v] = true;
}

int main() {
    // 入力
    int N;
    cin >> N;
    Graph G(N);
    for (int i = 0; i < N - 1; ++i) {
        int u, v;
        cin >> u >> v;
        --u, --v;
        G[u].push_back(v);
        G[v].push_back(u);
    }

    // 頂点 0 を根として探索
    used.assign(N, false);
    rec(G, 0);

    // 数える
    int res = 0;
    for (int v = 0; v < N; ++v) {
        if (used[v]) ++res;
    }
    cout << res << endl;
}

AOJ 1163 カードゲーム (ICPC 国内予選 2009 E) (400 点)

典型的な二部マッチングの問題です。

問題へのリンク

問題概要

 N 枚の赤いカードと、 M 枚の青いカードとの間でマッチングを作っていきます。

各カードには整数値が書かれていて、最大公約数が 1 より大きいカード同士をペアにすることができます。

最大マッチングのサイズを求めてください。

考えたこと

二部グラフを作って、二部マッチングアルゴリズムで解くことができます。ここでは

  • スーパー頂点  s, t を追加して、最大流アルゴリズム Ford-Fulkerson 法のよる実装
  • 二部マッチング専用の高速アルゴリズム Hopcroft-Karp 法による実装

を示します。

コード (Ford-Fulkerson 法による解)

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

// グラフを表す構造体
struct Graph {
    // 辺を表す構造体
    // rev: 逆辺 (to, from) が G[to] の中で何番目の要素か
    // cap: 辺 (from, to) の容量 (この値は変化する)
    // icap: 辺 (from, to) のもともとの容量
    struct Edge {
        int rev, from, to, cap, icap;
        Edge(int r, int f, int t, int c) :
            rev(r), from(f), to(t), cap(c), icap(c) {}
    };

    // 隣接リスト
    vector<vector<Edge>> list;

    // N: 頂点数
    Graph(int N = 0) : list(N) { }

    // グラフの頂点数取得
    size_t size() {
        return list.size();
    }
    
    // Graph インスタンスを G として,
    // G.list[v] を G[v] と書けるようにしておく
    vector<Edge> &operator [] (int i) {
        return list[i];
    }

    // 辺 e = (u, v) の逆辺 (v, u) を取得する
    Edge& redge(const Edge &e) {
        return list[e.to][e.rev];
    }

    // 辺 e = (u, v) に流量 f のフローを流す
    // e = (u, v) の流量が f だけ減少する
    // このとき逆辺 (v, u) の流量を増やす
    void run_flow(Edge &e, int f) {
        e.cap -= f;
        redge(e).cap += f;
    }

    // 頂点 from から頂点 to へ容量 cap の辺を張る
    // このとき to から from へも容量 0 の辺を張っておく
    void addedge(int from, int to, int cap) {
        int fromrev = (int)list[from].size();
        int torev = (int)list[to].size();
        list[from].push_back(Edge(torev, from, to, cap));
        list[to].push_back(Edge(fromrev, to, from, 0));
    }
};

struct FordFulkerson {
    static const int INF = 1 << 30; // 無限大を表す値を適切に
    vector<int> seen;

    FordFulkerson() { }

    // 残余グラフ上で s-t パスを見つける (深さ優先探索)
    // 返り値は s-t パス上の容量の最小値 (見つからなかったら 0)
    // f: s から v へ到達した過程の各辺の容量の最小値
    int fodfs(Graph &G, int v, int t, int f) {
        // 終端 t に到達したらリターン
        if (v == t) return f;

        // 深さ優先探索
        seen[v] = true;
        for (auto &e : G[v]) {
            if (seen[e.to]) continue;

            // 容量 0 の辺は実際には存在しない
            if (e.cap == 0) continue;

            // s-t パスを探す
            // 見つかったら flow はパス上の最小容量
            // 見つからなかったら f = 0
            int flow = fodfs(G, e.to, t, min(f, e.cap));

            // s-t パスが見つからなかったら次辺を試す
            if (flow == 0) continue;

            // 辺 e に容量 flow のフローを流す
            G.run_flow(e, flow);

            // s-t パスを見つけたらパス上最小容量を返す
            return flow;
        }

        // s-t パスが見つからなかったことを示す
        return 0;
    }

    // グラフ G の s-t 間の最大流量を求める
    // ただしリターン時に G は残余グラフになる
    int solve(Graph &G, int s, int t) {
        int res = 0;

        // 残余グラフに s-t パスがなくなるまで反復
        while (true) {
            seen.assign((int)G.size(), 0);
            int flow = fodfs(G, s, t, INF);

            // s-t パスが見つからなかったら終了
            if (flow == 0) return res;

            // 答えを加算
            res += flow;
        }

        // no reach
        return 0;
    }
};

int solve(int M, int N) {
    vector<int> b(M), c(N);
    for (int i = 0; i < M; ++i) cin >> b[i];
    for (int i = 0; i < N; ++i) cin >> c[i];

    Graph G(N + M + 2);
    int s = N + M, t = N + M + 1;

    // スーパーノートとつなぐ
    for (int i = 0; i < M; ++i) {
        G.addedge(s, i, 1);
    }
    for (int i = 0; i < N; ++i) {
        G.addedge(i + M, t, 1);
    }
    
    // 二部グラフを作る
    for (int i = 0; i < M; ++i) {
        for (int j = 0; j < N; ++j) {
            if (gcd(b[i], c[j]) > 1) {
                G.addedge(i, j + M, 1);
            }
        }
    }

    FordFulkerson ff;
    return ff.solve(G, s, t);
}

int main() {
    // 入力
        int M, N;
    while (cin >> M >> N) {
        if (M == 0) break;
        cout << solve(M, N) << endl;
    }
}

コード (Hopcroft-Karp による解)

より高速なアルゴリズムによる解法です。

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


struct HopcroftKarp {
    int sizeL, sizeR;
    vector<vector<int> > list; // left to right
    
    // result
    vector<bool> seen, matched;
    vector<int> level, matching;

    // base
    HopcroftKarp(int l, int r) : sizeL(l), sizeR(r), list(l, vector<int>()) { }
    inline vector<int>& operator [] (int i) { return list[i]; }
    inline void addedge(int from, int to) { list[from].push_back(to); }
    inline friend ostream& operator << (ostream& s, const HopcroftKarp& G) {
        s << endl;
        for (int i = 0; i < G.list.size(); ++i) {
            s << i << " : ";
            for (int j = 0; j < G.list[i].size(); ++j) {
                s << G.list[i][j];
                if (j + 1 != G.list[i].size()) s << ", ";
            }
            s << endl;
        }
        return s;
    }
    
    // methods
    void hobfs() {
        queue<int> que;
        for (int left = 0; left < sizeL; ++left) {
            level[left] = -1;
            if (!matched[left]) {
                que.push(left);
                level[left] = 0;
            }
        }
        level[sizeL] = sizeL;
        while (!que.empty()) {
            int left = que.front();
            que.pop();
            for (int i = 0; i < list[left].size(); ++i) {
                int right = list[left][i];
                int next = matching[right];
                if (level[next] == -1) {
                    level[next] = level[left] + 1;
                    que.push(next);
                }
            }
        }
    }
    bool hodfs(int left) {
        if (left == sizeL) return true;
        if (seen[left]) return false;
        seen[left] = true;
        for (int i = 0; i < list[left].size(); ++i) {
            int right = list[left][i];
            int next = matching[right];
            if (level[next] > level[left] && hodfs(next)) {
                matching[right] = left;
                return true;
            }
        }
        return false;
    }
    int solve() {
        seen.assign(sizeL, false);
        matched.assign(sizeL, false);
        level.assign(sizeL+1, -1);
        matching.assign(sizeR, sizeL);
        int res = 0;
        while (true) {
            hobfs();
            seen.assign(sizeL, false);
            bool finished = true;
            for (int left = 0; left < sizeL; ++left) {
                if (!matched[left] && hodfs(left)) {
                    matched[left] = true;
                    ++res;
                    finished = false;
                }
            }
            if (finished) break;
        }
        return res;
    }
};


int GCD(int a, int b) { return b ? GCD(b, a%b) : a; }

int main() {
    int N, M;
    while (cin >> N >> M, N) {
        HopcroftKarp G(N, M);
        vector<int> left(N), right(M);
        for (int i = 0; i < N; ++i) cin >> left[i];
        for (int i = 0; i < M; ++i) cin >> right[i];
        for (int i = 0; i < N; ++i) {
            for (int j = 0; j < M; ++j) {
                if (GCD(left[i], right[j]) > 1)
                    G.addedge(i, j);
            }
        }
        cout << G.solve() << endl;
    }
}