hogecoder

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

Educational Codeforces Round 87 G: Find a Gift

こういうの典型なんですかね?結構面白い気がしたのでブログに残しておく

問題概要

原文 → Problem - G - Codeforces

 N 個の箱が一列に並んでいる。箱の中身は石またはギフトである。石が入っている箱はすべて同じ重さであり、ギフトが入っているどの箱よりも真に重い。ギフトが入っている箱どうしは重さが異なる可能性がある (注意: 石が入っている箱より軽いことは保証される)

高々 50 回、以下の形式の質問をすることで、最も左に位置する、ギフトが入っている箱の番号 を当てよ。

  • 積集合が空 (つまりお互いに要素に被りがない) であるような、空でない添字集合  A, B をジャッジに投げて質問する。これは、「 A に属する箱の重さの総和と、 B に属する箱の重さの総和を比較したときに、どちらが重いか (どちらも同じであるばあいは同じであること) 」を質問しており、ジャッジからはその回答が得られる。

解説

※この解説は 公式の解説 をベースに書いています。

最も左に位置するものを当てる必要があるので、左に位置する箱から順に情報が得られるのが望ましいだろう、という直感を働かせましょう。まずはじめに、最も左の箱に入っているものが石であるかギフトであるかを特定できるかを考えてみましょう。

質問時に使用する  A, B の要素数を、それぞれいったん  1 に限定しましょう。 A は必ず最も左の要素のために使うことにします。さて、 B はどのように選択すれば良さそうでしょうか?実は、これについて以下のことが言えます。

ランダムに要素を選び、それを  B の元とする戦略を何度か取ると、高い確率で最も左の要素が石であるかどうかがわかる。

制約を見ると分かるとおり、ギフトの数は 高々  N の半分以下 であるため、適当に要素を選んだ際にそれが石である確率は半分以上はあることになります。最も左の要素がギフトであることを特定するためには、 B の要素として石であるものを選ぶ必要がりますが、半分以上の確率で石を選択できるため、だいたい  20 回程度試行しますと石を選べる確率が  1 - \left( \frac{1}{2} \right)^{20} くらいになるため、十分大きい確率で判定できることになります。よって、最も左の箱がギフトである場合には、 20 回程度の質問で答えが得られること・そうでない場合にも「最も左の箱が石である」という情報が得られることがわかりました。

以下、最も左の箱がギフトではない (石である) 状況を考えます。今は最も左の箱、つまり  1 番目の箱のみについて情報が得られていますが、ここから情報を効率的に増やすにはどうすればよいでしょうか?これについては、以下のことを利用して戦略を取ると良いです。

 2^{k} 番目の箱について全て石が含まれている場合、「 2^{k+1} 番目の箱まで全て石が含まれていること」または「 2^{k} + 1 番目から  2^{k+1} 番目の箱の中にギフトが少なくともひとつ含まれること」のいずれかの情報が得られる

ギフトは石よりも軽いため、質問する集合に含まれている要素数が同じで、かつ一方が全て石であるような場合、もう一方が軽いと判定された時点でそこにギフトがあることがわかります。逆にそのような判定をされなかった場合は、もう一方もすべて石であることがわかります。いま、 1 番目が石であるという情報をすでに持っているわけですから、この戦略が利用できるというわけです。

さて、この戦略を利用すると 「添字区間  \left[ l, r \right) の間に少なくともひとつギフトが存在する」ことまでは特定できます。この後にどうすればよいかと言うと、二分探索をすると良いです。というのも、 \left[ 1, l \right) に関しては全て石が含まれているので、片方の集合を  \left[ 1, l \right) から、もう片方の集合を  \left[ l, r \right) から要素数が同じになるように質問すれば、先ほどと同様に「全部石の箱たち」と「一部はギフトかもしれない箱たち」との比較ができ、二分探索が可能です。

ソースコード

#ifdef LOCAL を使ってローカルでもテストできるようにしてあるので、若干冗長です、、、

enum Verdict {
    EQUAL,
    LEFT,
    RIGHT,
};

const int STONE_WEIGHT = 10;
vector<int> ref_vec;

Verdict ask_to_judge(int l1, int r1, int l2, int r2) {
    vector<int> A, B;
    for(int i=l1; i<r1; i++) A.emplace_back(i);
    for(int i=l2; i<r2; i++) B.emplace_back(i);

    printf("? %zu %zu\n", A.size(), B.size());
    for(size_t i=0; i<A.size(); i++) printf("%d%c", A[i]+1, " \n"[i + 1 == A.size()]);
    for(size_t i=0; i<B.size(); i++) printf("%d%c", B[i]+1, " \n"[i + 1 == B.size()]);
    fflush(stdout);

    int verdict;
    
#ifdef LOCAL
    int SA = 0, SB = 0;
    for(auto &a : A) SA += ref_vec[a];
    for(auto &b : B) SB += ref_vec[b];

    if(SA < SB) verdict = RIGHT;
    else if(SA > SB) verdict = LEFT;
    else verdict = EQUAL;

    vector<string> v_to_s = {"EQUAL", "LEFT", "RIGHT"};
    fprintf(stderr, "# verdict: %s\n", v_to_s[verdict].c_str());
    return (Verdict)verdict;
#endif

    string s; cin >> s;
    if(s == "FIRST") verdict = LEFT;
    if(s == "SECOND") verdict = RIGHT;
    if(s == "EQUAL") verdict = EQUAL;
    assert(s != "WASTED");
    return (Verdict)verdict;
}

void answer(int x) {
    printf("! %d\n", x+1);
    fflush(stdout);

#ifdef LOCAL
    assert(ref_vec[x] < STONE_WEIGHT);
    for(int i=0; i<x; i++) assert(ref_vec[i] == STONE_WEIGHT);
    puts("[ OK ]");
#endif
}

void solve() {
    int N, K; scanf("%d%d", &N, &K);
#ifdef LOCAL
    ref_vec.resize(N);
    printf("# Input weights.\n");
    printf("# if you want to choose stone_weight, input '-1'.\n");
    printf("# otherwise, you have to choose the weight less than STONE_WEIGHT (%d).\n", STONE_WEIGHT);
    for(int i=0; i<N; i++) {
        int v; scanf("%d", &v);
        if(v < 0) ref_vec[i] = STONE_WEIGHT;
        else {
            assert(v < STONE_WEIGHT);
            ref_vec[i] = v;
        }
    }
#endif

    Rand rnd(1333);
    for(int i=0; i<25; i++) {
        int x1 = 0, x2 = rnd.NextInt(1, N-1);
        Verdict q = ask_to_judge(x1, x1+1, x2, x2+1);
        if(q == RIGHT) {
            answer(x1);
            return;
        }
    }

    // 1st elem is stone
    int len = 1, lb = -1, ub = -1;
    for(; ; len<<=1) {
        int cnt = min(len, N - len);
        int l1 = 0, r1 = l1 + cnt;
        int l2 = len, r2 = l2 + cnt;
        Verdict q = ask_to_judge(l1, r1, l2, r2);
        if(q != EQUAL) {
            assert(q == LEFT);
            lb = l2, ub = r2;
            break;
        }
    }

    int ll = lb;
    while(ub - lb > 1) {
        int mid = (ub + lb) / 2, w = mid - ll;
        Verdict q = ask_to_judge(0, w, ll, ll+w);
        if(q == EQUAL) lb = mid;
        else ub = mid;
    }
    answer(lb);
}

int main() {
    int T; scanf("%d", &T);
    while(T--) solve();
    return 0;
}

乱択 + うまく選んで  2 ベキに持っていく + 二分探索できる形にする が必要で、けっこう勉強になりました。

分割統治法メモ

はじめに

これは自分の備忘録的に書いているだけであり、体系的に書いているわけではありません。ご了承ください。

ちなみに蟻本 +α くらいの情報をまとめている資料が以下にあります (ダイマ)

https://hcpc-hokudai.github.io/archive/algorithm_divide_and_conquer_001.pdf

オフラインにおける分割統治

Rollback をするテク

いくつかのデータ構造では、そのデータ構造がサポートしている操作の「逆」は難しいという話があります。たとえば UnionFind では辺の追加は普通に行えますが、削除は難しいといった問題があります。では、以下のようなクエリが要求されたときはどのように解けばよいでしょうか?

$$N$$ 個の頂点があり、はじめは辺がひとつも存在しない。以下のクエリを $$Q$$ 回処理せよ。($$N, Q \leq 10^{5}$$)

  • 頂点 $$u, v$$ 間に辺が存在しないなら辺を追加する。
  • 頂点 $$u, v$$ 間に辺が存在するなら辺を削除する。
  • 頂点 $$u, v$$ が同じ連結成分に属するかどうかを判定する。

クエリがオフラインであるとき、これは普通の UnionFind と分割統治を用いて処理できます。なお、これの類題が Educational Codeforces Round #22 F: Bipartite Checking です。

クエリで与えられるそれぞれの辺 $$(u, v)$$ について、その辺がクエリ全体においていつ存在したかを区間として覚えておきます (要するに奇数回目に出てきたときに開いて、偶数回目に出てきたときに閉じるようにして区間を作る、ということです)。こうしてできた区間の数は明らかに高々 $$O(Q)$$ 個です。以降の説明ではこれを「辺区間」と呼ぶことにします。

この区間それぞれを分割統治法で処理することを考えます。クエリのインデックスの区間 $$[L, R)$$ を分割統治するとき、$$[L, R)$$ を完全に覆うような辺区間は UnionFind に反映させて、より細かい区間 $$[L, \mathrm{mid}), [\mathrm{mid}, R)$$ について再帰的に解きます。

再帰的に解くことが終わったら、$$[L, R)$$ を完全に覆っていた辺区間の情報を UnionFind から削除します。こうすることで Rollback の機能を実現できます。ここで、UnionFind から辺区間の情報を削除するときにはどうすればよいのでしょうか?これは、UnionFind における有名な計算量改善テクのひとつである、「小さい連結成分を大きい連結成分にマージ」のみをやることで実現できます (経路圧縮をやると壊れることに注意します)。実は、UnionFind ではひとつ前の状態に戻ることは簡単にできるため、これを活用すると Rollback ありの UnionFind を実現できます。

マージテクのみを用いるとき、UnionFind の中の配列であって unite の前後で値が変化しうるのは、頂点 $$u, v$$ の代表元のみです。なので、unite の前における代表元に関する値を覚えておき、削除したいときにその覚えた値に戻すことで、削除操作が実現できます。

条件の一部を強制的に満たすテク

条件を満たすペアの個数を求める問題で、満たすべき条件がひとつではなく複数存在する場合があります。この場合、分割統治法で処理できる可能性があります。

問題例はいっぱいあるので以下を解いてください (雑)

オンラインにおける分割統治

正直これは分割統治かどうか微妙なんですが・・・

区間全体に対しては簡単に解け、さらにそれぞれの要素が独立に処理できる (ある要素が他のある要素による影響を受けない) 状況で、区間の一部 $$[L, R)$$ に対して問題を解いてほしいというときに、区間をいくつかのより小さい区間に分割することで処理できる場合があります。これはオンラインでもできるため一応「オンラインにおける」分割統治と書いておきます。

以下の問題を解いてみましょう。なお、紹介している問題は Educational Codeforces Round #26 G: Functions On The Segments です。

場合分けありの一次関数 $$f_i(x)$$ が $$N$$ 個与えられ、それぞれの関数は $$1$$ から $$N$$ まで番号付けられています (詳しい一次関数の形は元の問題を読んでください)。$$l_i, r_i, x_i$$ が $$Q$$ 回与えられますので、それぞれについて $$\sum_{k=l}^{r} f_k(x)$$ の値をオンラインで求めてください。

クエリにおいて区間の範囲の制約がなく、全部に対して $$f_k(x)$$ の値を足し合わせる場合、$$x_1$$ 以下であるもの、$$x_2$$ を超えるものを求めて適当に計算すれば良いです。これと同じ発想で、与えられた区間を $$O(\log N)$$ 個の区間に分割し (そのうちのひとつを $$[L, R)$$ とします)、番号インデックスの区間 $$[L, R)$$ について、その区間に含まれる関数のみを考慮して計算する、ということをやって値を合計すれば良いです (Segment Tree と同じような発想で区間を区切ってやります)。

備忘録なので説明が雑ですみません。もし質問があれば聞いてください。

HCPC のホームページを移行した話

この記事は Competitive Programming (1) Advent Calendar 2019 の 12 日目の記事として書かれました。

2019 年 11 月に、北海道大学競技プログラミングサークルのホームページを FC2 から GitHub Pages へと移行しました。移行していろいろと便利になりましたので、今後競技プログラミングサークルでホームページを作ることを検討している方への参考になればと思い、記事を書くことにしました。

TL; DR

GitHub を使って、資料管理・ホームページ作成をするやり方を説明します。資料管理用・ホームページ用リポジトリをそれぞれ作り、GitHub Pages + Jekyll (+ CI) でページをビルドします。

ホームページのコンテンツ

弊サークルのホームページの主なコンテンツは以下が挙げられ、これらを便利に見れるようにすることを重視してホームページを構成しました。

  • ICPC 参加報告
  • スライド
    • 弊サークルでは「勉強会」という、予め決定されたテーマについてスライドを通じて勉強する活動があります。今まで作られたスライドがどこかにまとまっていれば、新しくサークルに興味を持ってくれた方にも勧めやすいですし、勉強もしやすいです。
    • 大学サークル主催の競技プログラミング合宿で出した問題やその解説についても、どこかにまとめる必要があります。
  • 連絡先

従来のホームページの構成、その問題点

従来の構成は以下のようになっていました。

ですが、この構成にはいくつか問題点があります。

  • ホームページを三ヶ月以上更新しないと広告がつく
    • これは FC2 のホームページ側が課している制約です。無料だし仕方ないですが、できればこの制約はなくしたいところです。
  • SlideShare でスライドをアップロードしても日本語が見れない
    • フォントが埋め込みされていない、などが理由の場合もあります。
    • しかしながらフォントを埋め込んでいるのに見れない場合もあります。こういった場合はなぜかフルスクリーンにすると大体は見れるのですが、わざわざフルスクリーンにするのは不便です。
  • SlideShare でスライドを差し替えられない
    • SlideShare にはスライドを編集して再アップロードする機能はありません。いったんスライドを削除して全く同じものをアップロードしても、スライドへの URL は今までとは異なるものになってしまいます。
    • スライドの中身を変更した場合、URL の変更を何らかの形で周知しなければならないことを考えるとこれも非常に不便な点です。

これらの問題を解消するために、以下で説明する構成に変更しました。

移行後のホームページの構成

GitHub Pages では広告が付きません。また、今時 PDF が見れないブラウザを使用する人はそうそういないだろう・・・ということで、資料はリポジトリに直接入れて Web 上で見れるようにしました。ファイルを編集して再アップロードする機能は当然 GitHub に備わっていますので、日本語が見れない問題や再アップロードができない問題は解消されました。

それでは、実際にこの構成でホームページを作成してみましょう。資料管理・ホームページ作成について、どう設定すればよいかを以下で詳しく説明します。

資料管理について

資料管理用に、archive というリポジトリを作ったとします。

まず、master ブランチに各種資料を push します。次に、GitHub Pages を有効にするために以下を設定します。

  • リポジトリのページに飛び、「Settings」タブを押す
  • 「Settings」の中に「GitHub Pages」という項目があるので、Source を「master branch」に設定

f:id:tsutaj:20191207204800p:plain
生成元ブランチ選択画面

これで、資料が Web 上から参照できるようになります。

GitHub Pages について

ここに書くと記事が非常に長くなるので、別途リポジトリ tsutaj/pages-example を用意しました。Jekyll や CI について簡単に知ることができる構成にしたつもりです。

上に示したものを fork するかリンク先のコンテンツを含んだリポジトリを自分で作成するかして、説明を読んだり、ビルドしたものがどのように見えるか試してみたりしてください。

サンプルはこちらにあります: tsutaj.github.io/pages-example/

f:id:tsutaj:20191207205024p:plain
用意した pages-example リポジトリ

まとめ

GitHub Pages + Jekyll は少しの設定で便利にホームページを構成できます。また、CI と組み合わせることで Jekyll への機能追加にも柔軟に対応できます。

この記事に関して質問がありましたら tsutaj までご連絡ください。

宣伝?

現在、kimiyuki さんと beet さんと自分で kmyk/online-judge-verify-helper を開発中です。これは CI と連携して C++ のライブラリリポジトリを自動で verify し、その結果を GitHub Pages で可視化するというものです。まだ現在進行中で開発をしているところですが、よろしければぜひお試しください。

f:id:tsutaj:20191211235923p:plain