hogecoder

tsutaj 競技プログラミングの記録

Educational Codeforces Round 8 F: Bear and Fair Set

問題概要

原文 → Problem - F - Codeforces

 5 の倍数  n と、整数  b, q が与えられるので、以下を全て満たす正の整数集合を構築できるかどうか判定せよ。

  • 素数 n で、値は全て  b 以下で相異なる
  •  \left\{ 0, 1, 2, 3, 4 \right\} の各元  r について、要素を  5 で割ったあまりが  r になるような、集合内の要素の数はちょうど  \frac{n}{5} 個である
  • 以下の  q 個の条件を全て満たす
    • 集合内の  1 から  \mathrm{upto}_i までの整数を全て数えた結果が  \mathrm{quantity}_i である

解説

まず、各条件を  \mathrm{upto}_i の値でソートします。すると、 \left[ 1, b \right] はいくつかの区間に分かれることになり、各区間内で数をいくつ選べば良いかもわかります。

以下のようにグラフを作り、最大フローが  n であるかどうかで判定可能です。

  • source に「余りが  r」を表す頂点をそれぞれつなげ、その頂点集合を  A とする。容量はすべて  \frac{n}{5}
  • sink に、各区間に対応するような頂点をつなげ、その頂点集合を  B とする。容量はその区間内で選ぶべき個数になる。
  •  A に属する頂点と  B に属する頂点との間は、対応する区間内にあまりが  r である要素がいくつあるかに応じて容量を設定してつなげる

f:id:tsutaj:20190126171705j:plain

また、Hall の結婚定理を使っても解くことができます。 A の頂点部分集合  A' 全てに対して、 A' B を結ぶ辺として使用される本数の最小値・最大値を求め、これをそれぞれ  E_{\min}, E_{\max} とします (下限が 0 でないことに注意)。条件を満たすような集合が存在する、つまり適切なマッチングが存在するための必要十分条件は、 E_{\min} \leq \frac{n}{5} \times |A'| \leq E_{max} になります (詳しくは Hall の結婚定理 を調べてください)。この方針でやると  O(2^{K} b) (この問題において  K = 5) で解けます。

ソースコード

以下の実装は普通にフローを流す方の解法です。

// フロー略
const int K = 5;
signed main() {
    int N, B, Q; scanf("%lld%lld%lld", &N, &B, &Q);

    vector< tuple<int, int> > queries;
    for(int i=0; i<Q; i++) {
        int upto, quan; scanf("%lld%lld", &upto, &quan);
        queries.emplace_back(upto, quan);
    }
    queries.emplace_back(B, N);
    sort(queries.begin(), queries.end());

    Flow<int> fl(K + queries.size() + 2);
    int source = K + queries.size(), sink = source + 1;

    // source <-> A
    for(int i=0; i<K; i++) {
        fl.add_edge(source, i, N / K);
    }

    bool ok = true;
    int pre_num = 0, pre_quan = 0;
    for(size_t x=0; x<queries.size(); x++) {
        auto q = queries[x];
        int upto, quan; tie(upto, quan) = q;
        if(pre_quan > quan) ok = false;
        int cap = quan - pre_quan;
        
        vector<int> cnt(K);
        for(int i=pre_num+1; i<=upto; i++) {
            int mod = i % K; cnt[mod]++;
        }

        // A <-> B
        for(int i=0; i<K; i++) {
            fl.add_edge(i, K + x, cnt[i]);
        }
        // B <-> sink
        fl.add_edge(K + x, sink, cap);

        pre_num = upto, pre_quan = quan;
    }

    int res = ok ? fl.max_flow(source, sink) : -1;
    printf("%s\n", res == N ? "fair" : "unfair");
    return 0;
}

Hall の結婚定理を用いた解法は公式の実装がわかりやすいです。

Educational Codeforces Round 8 E: Zbazi in Zeydabad

問題概要

原文 → Problem - E - Codeforces

'.' または 'z' のマスのみからなる  N \times M の盤面が与えられる。この盤面内に存在する「Z 型」の正方形領域がいくつ存在するか求めよ。

Z 型の正方形領域の定義

  • 最も上と最も下の行は全て 'z'
  • 反対角成分は全て 'z'
  • それ以外は 'z' でも '.' でもよい

解説

以下の説明では、座標の上下方向を  x と、左右方向を  y として、 x y 列目の座標を  (x, y) と表しています。

まず、前処理をして以下の値を簡単に得られるようにしておきます。

  •  zl_{ij}: 座標  (i, j) の左に 'z' が何連続しているか
  •  zr_{ij}: 座標  (i, j) の右に 'z' が何連続しているか
  •  zld_{ij}: 座標  (i, j) の左下 (反対角成分に対応) に 'z' が何連続しているか

各座標  (i, j) について、その座標を正方形領域の右上としたときにどれくらいの大きさの Z 型が作れるかを考えます。まず、大きさの最大値は  s = \min(zl_{ij}, zld_{ij}) 以下であることがわかります。あとは距離  s-1 以内に存在する左下の候補を高速に求められれば、この問題が解けたことになります。

では、左下の候補はどのように求めればよいでしょうか?反対角線ごとに独立に処理する、つまりある  A という整数を決めた際に、 i + j = A を満たす座標についてまとめて処理することにします。以下で扱う座標  (i, j) は、全て  i + j = A を満たす (同じ反対角線上に属する) ものとします。

反対角線ごとに、右の列から左の列に向けて平面走査することを考えます。各座標  (i, j) について、その座標を左下として利用するときの、正方形領域の右上の座標としてありえる  y 座標最大  j_{\max} を求めてみましょう。これはすでに「座標の右に 'z' が何連続しているか」を求めていますので、 j_{\max} = j + zr_{ij} - 1 と求められます。また、右上の座標としてありえる  y 座標最小  j_{\min} は明らかに  j です。よって、 j \leq y \leq j + zr_{ij} - 1 の範囲で、座標  (i, j) が左下の座標として用いられる可能性があります。

あとはこれを考慮しつつ SegmentTree などによる高速化を考えます。左下の座標として使用できるときはセグ木に要素を追加しておいて、使用できなくなった場合は削除します。追加してから削除するまでの間に、右上の座標の候補が来る場合があるため、その場合は左下としてありえる候補を区間加算で得ます。

言語化が難しすぎるのでソースコード見てください・・・。

ソースコード

// BIT 略

int lft[3010][3010], rgh[3010][3010], dig[3010][3010];

signed main() {
    int H, W; cin >> H >> W;
    vector<string> vs(H, string(W, '.'));

    for(int i=0; i<H; i++) {
        cin >> vs[i];
    }

    for(int i=H-1; i>=0; i--) {
        for(int j=0; j<W; j++) {
            if(vs[i][j] == '.') lft[i][j] = dig[i][j] = 0;
            else {
                if(j == 0) lft[i][j] = 1;
                else lft[i][j] = lft[i][j-1] + 1;

                if(i == H-1 or j == 0) dig[i][j] = 1;
                else dig[i][j] = dig[i+1][j-1] + 1;
            }
        }
        for(int j=W-1; j>=0; j--) {
            if(vs[i][j] == 'z') rgh[i][j] = (j == W-1) ? 1 : rgh[i][j+1] + 1;
        }
    }

    int ans = 0;
    for(int A=0; A<=H+W; A++) {
        BIT<int> bit(H+W);

        vector< tuple<int, int, int> > tups;
        for(int x=0; x<H; x++) {
            int y = A - x;
            if(y < 0 or y >= W) continue;

            tups.emplace_back(-(y + rgh[x][y] - 1), 0, x);
            tups.emplace_back(- y                 , 1, x);
            tups.emplace_back(- y                 , 2, x);
        }

        sort(tups.begin(), tups.end());
        for(auto e : tups) {
            int x = get<2>(e), y = A - x, query = get<1>(e);
            if(query == 0) bit.add(x+1,  1);
            if(query == 1) ans += bit.sum(x+min(dig[x][y], lft[x][y])) - bit.sum(x);
            if(query == 2) bit.add(x+1, -1);
        }
    }
    cout << ans << endl;
    return 0;
}

"Competitive Programming Team Maker" の beta version を公開しました

tsutaj です。今回は競技プログラミングの解説記事ではなく、競技プログラミングに関するアプリケーションのリリースノートを書きます。

Competitive Programming Team Maker

ユーザーのレーティングと所属を元に、以下の条件をできるだけ満たしつつチーム分けを行うアプリケーション "Competitive Programming Team Maker" を開発しました。まだ機能が不十分なので beta 版として公開しています。

  • チーム間の実力差をなるべく小さくする
  • 同一のチーム内で所属の重複がないようにする (同じチームは、できるだけ異なる所属同士で組む)

ユーザー情報を記載した CSV ファイルをインポートすることによって、簡単にチーム分けを実行することができます。また CSV ファイルを持っていなくてもブラウザ上でテーブルを編集・保存することができ、さらにチーム分けの結果も保存することができます。

簡単に使い方を見ていきましょう。例えば、以下のように CSV をインポートします。インポートした CSV はブラウザ上で簡易的に編集が可能ですし、その状態をエクスポートすることもできます。実質 CSV エディタ

f:id:tsutaj:20190103211326p:plain

これでチーム分けを実行すると、所属が重複する場合もありますが・・・

f:id:tsutaj:20190103211332p:plain

何度か繰り返すと、所属が重複しない実力差が少ない割当が得られます*1

f:id:tsutaj:20190103211336p:plain

有志で開催されている競技プログラミングの合宿である ACPC や RUPC では、その場に集まったメンバーから 3 人を基本としたチームを編成する必要があります。この際に上の条件をできるだけ満たしながらチーム分けをする必要があるのですが、今までは人力でいい感じに行っていました。最近では参加人数が増えてきて人力で行うのが厳しくなってきたため、自動化できないかなあと思い開発した次第です。

使用しているアルゴリズム

詳しくは アプリケーションの About ページ を参考にしていただきたいのですが、ざっくりというと「チーム内のメンバーのレーティング値の二乗和平均」の分散を最小化するような焼きなまし法を行っています。私自身が焼きなまし法初心者であるため、現状のアルゴリズムは改善の余地が割とあると思っていますので、もしも改善案があれば私までご連絡いただければと思っております。

今後実装する予定の機能

詳しくは GitHub の Issue を参照していただきたいのですが、今後実装する予定の主な機能は以下の通りです。GitHub の Issue に記載されていない問題であって、バグや脆弱性・機能の改善点がございましたらぜひ私の Twitter までご連絡ください。

  • 焼きなまし法のステップ数やチームの基本人数など、パラメータをユーザー側が調整できるようにする
  • 条件を満たさないような書式で入力フォームが埋められていた場合、「チーム分けを実行」ボタンを押せないようにする
  • 実行結果を保存した JSON ファイルをインポートすることによって、今まで行ったチーム分けの中で同じチームに配属されたメンバー同士を異なるチームに配属するような仕組みを作る (これは、例えば合宿の Day1 と Day2 で同じチームに属してしまう問題の解消のために実装する予定です)

実装に使用した言語

ここからは開発の裏話的なお話になりますが、実装に使用した言語などを紹介したいと思います。なぜかはわかりませんがこういう情報を公開している開発者が少ないなあと感じるので、せっかくなので書いてみます。

実装には Javascript (jQuery) と PHP を使用しました。理由としてはサーバーサイドで実行される言語を一度も書いたことがなかったためその経験を得たかったことと、API を叩くために PHP が有効であることを聞いたのでそれをつかいたかったからです*2。個人差はあると思いますが、PHP はかなり C++ と似た感覚でかけると思います。むしろ、おそらく C++ よりも便利です。なので競技プログラミングC++ を日常的に使用している方でしたら特に違和感なく開発できるかと思います。

PHP を勉強するために使用した書籍は プログラミング PHP 第 3 版 (オライリー・ジャパン) ですが、正直に言ってそこまで使用頻度は高くありませんでした。英語のキーワードで Google 検索して Stack Overflow や php.net あたりで情報を得たほうが速いので、適当にネットを検索するほうがストレスなく開発できると思います。ただ、さすがに何も知らない状態で Google 検索するのは不可能だと思うので、最低限の知識は予め身につける必要があると思います。私の場合も GET や POST・セキュリティ周りを何も知らなかったため、このあたりは流石に書籍を参考にしてある程度まで体系的に勉強しました。まだ不十分ではあるのですが・・・

お問い合わせ先

Contact ページ にも記載しましたが、問い合わせたい内容がありましたら tsutaj の Twitter までご連絡ください。開発初心者なのでどのような内容でも大歓迎です!よろしくお願いします。

*1:アルゴリズムの改善案がありましたらぜひご連絡ください!

*2:実際にはスクレイピングで事足りたため、API はどこにおいても使用していません・・・