hogecoder

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

セグメント木をソラで書きたいあなたに

セグ木にいろいろ生やす問題がてんで解けない私なので、セグ木に慣れようと思い立ちました。そのためにはまずセグ木をもっと知らねばならないと思ったので、ソラで書けないか色々やっていました。

やってくうちにコツを掴んでソラで書けるようになってきたので、忘れないためにも記事を書くことにしました。ということで、何も見ずにセグ木を書くために押さえておきたいポイントを紹介していきます。

注意

この記事は、「セグ木がどんな形をしているかはわかるけど、実装はできない・・・」という方向けで、遅延評価が不要な、区間最小の基本的なセグメント木のみ扱っています。

そもそもセグ木って何という人は他記事を参考にしてください。
おすすめは iwiwi さんのスライド → プログラミングコンテストのためのデータ構造

また、すでにセグ木が書けるプロの方が見ても面白くないかもしれません。

記事では区間最小についてのみ扱っていますが、区間和もほぼ同じコードで実現できます (コード例は後述)。

まずは形を見よう

ノードの数

セグメント木は皆さんご存知のとおり、図で書くとこんな形をしています。

f:id:tsutaj:20170329204251p:plain

元のデータ数以上になる最小の 2 冪の数を  N とします。すると、ノードは全部で  2N - 1あります。なぜこうなるかは、各段にあるノードの数を 2 進で書いて足していくと簡単にわかります。

すなわち、

  • 最下段にはノードが  N 個ある
  • それより上の段のノードは全部で  N-1 個ある

ということになります。この性質は後に大事になるので、しっかり把握しておきましょう。

ノードの関係

セグメント木上で操作をするには、ノードの関係を明らかにする必要があります。ここでは木と同様に、自分のノードの直下にあるノードを「子」、自分のノードの真上にあるノードを「親」と呼ぶことにします。

木に例えるとこれは完全二分木になります。つまり、葉でない限り自分の子は必ず  2 個あるということです。

f:id:tsutaj:20170329204255p:plain

自分のノードの番号を  k をおくと、親や子にアクセスするには以下のようにします。

  • 親にアクセスするには、  \lfloor \frac{k-1}{2} \rfloor 番目にアクセスする
  • 子にアクセスするには、  2k+1 番目・  2k+2 番目にアクセスする

これを各所で使うことになります。

更新・取得クエリ

一般的なセグメント木では、値を更新するクエリと、値を取得するクエリの 2 種類を行うことになります。かなりざっくりとしていますが、各クエリに関してはこのようなイメージを持つと良いです。

  • 値の更新クエリは、最下段から上がっていくようにする
  • 値の取得クエリは、最上段から下がっていくようにする

雑ですが、このようなイメージを持って実装を重ねることで詰まることなく書けるようになると思います。

実際に書いてみよう

初期化

元となる配列があって、それをセグメント木で表してみることをやってみましょう。

まずは、セグメント木のサイズ (ノード数)を決定します。そのためには最下段のサイズを求める必要がありますが、これは先述のとおり、元のサイズ以上になる最小の 2 冪 です。この値を  N としたとき、セグメント木のサイズは  2N-1 です。

次に実際に値を入れていくのですが、最下段から値を入れていき、それ以降は下の段から順番に自分の子を参照することで値を入れていきます。上のノードの値を決めるには下のノードを参照しなければならないことを考えると、最下段から入れるのは自然な流れですね。

最下段のノードのインデックスはどうなるのか?と思うかもしれませんが、これには最下段より上の段のノードは全部で  N-1 個ある ことを利用します。前に  N-1 個の要素があるので、インデックスは元のインデックスに  N-1 を足せばよいですね。

これらをまとめると、次のような実装になります。

struct SegmentTree {
private:
    int n;
    vector<int> node;

public:
    // 元配列 v をセグメント木で表現する
    SegmentTree(vector<int> v) {
        // 最下段のノード数は元配列のサイズ以上になる最小の 2 冪 -> これを n とおく
        // セグメント木全体で必要なノード数は 2n-1 個である
        int sz = v.size();
        n = 1; while(n < sz) n *= 2;
        node.resize(2*n-1, INF);

        // 最下段に値を入れたあとに、下の段から順番に値を入れる
        // 値を入れるには、自分の子の 2 値を参照すれば良い
        for(int i=0; i<sz; i++) node[i+n-1] = v[i];
        for(int i=n-2; i>=0; i--) node[i] = min(node[2*i+1], node[2*i+2]);
    }
};

値の更新

 x 番目の要素を  val に更新する」ことを考えます。

これを行うには、 x 番目の要素が含まれる区間全てを更新する必要があります。上のノードを更新するには下のノードを見なければならないため、下のノードから更新していかなければいけないことがわかります。

最下段から上がっていく イメージで書いていきます。つまり、まずは最下段を更新し、あとはその親を更新することを繰り返せばよいです。

実際に書くとこんな感じになります。

void update(int x, int val) {
    // 最下段のノードにアクセスする
    x += (n - 1);

    // 最下段のノードを更新したら、あとは親に上って更新していく
    node[x] = val;
    while(x > 0) {
        x = (x - 1) / 2;
        node[x] = min(node[2*x+1], node[2*x+2]);
    }
}

値の取得

区間  [a, b) にある要素の最小値を答える」ことを考えます。

説明のため、クエリで与えられた区間 (実際に計算したい区間) を「要求区間」、各ノードがカバーしている区間を「対象区間」と定義します (造語)。

これを扱うには、以下の 3 つの情報が必要になります。

  • 要求区間はどのような区間か?
  • いま自分がいるノードは何番目か?
  • 自分がいるノードはどのような区間か? (対象区間はどのようになっているか?)

要求区間と対象区間の関係によって場合分けをします。

要求区間と対象区間が交わらない場合

この場合は、これ以上操作をすすめても意味がありませんので、答えに影響しない値を適当に返しておきます。

要求区間が対象区間を完全に被覆している場合

この場合、対象区間は要求区間の計算に必要です。そのため、現状の答えと比較して、更新が必要ならば更新をしていきます。

要求区間が対象区間の一部を被覆している場合

この場合、対象区間の子にあたる区間に移動して、完全に被覆するまで操作を行わなければなりません。

子に移動するので、「現在見ているノード」と「対象区間の情報」が変わります。子のノードのインデックスの取得は先述のとおりで、対象区間に関しては、子のノードが現在見ているノードを半分に分割したものであることを利用すると得られます。

最上段から下がっていくイメージでこれをまとめると、このようなコードになります。(ここが一番難しいと思いますので、わからなければ Twitter などで私に質問してください)

// 要求区間 [a, b) 中の要素の最小値を答える
// k := 自分がいるノードのインデックス
// 対象区間は [l, r) にあたる

int getmin(int a, int b, int k=0, int l=0, int r=-1) {
    // 最初に呼び出されたときの対象区間は [0, n)
    if(r < 0) r = n;

    // 要求区間と対象区間が交わらない -> 適当に返す
    if(r <= a || b <= l) return INF;

    // 要求区間が対象区間を完全に被覆 -> 対象区間を答えの計算に使う
    if(a <= l && r <= b) return node[k];

    // 要求区間が対象区間の一部を被覆 -> 子について探索を行う
    // 左側の子を vl ・ 右側の子を vr としている
    // 新しい対象区間は、現在の対象区間を半分に割ったもの
    int vl = getmin(a, b, 2*k+1, l, (l+r)/2);
    int vr = getmin(a, b, 2*k+2, (l+r)/2, r);
    return min(vl, vr);
}

コード例

AOJ にコードを上げていますので、参考までに。

以上です。遅延評価付きのセグ木についても後日記事でまとめます (たぶん)。

RUPC2017 Day3 北大セットのまとめ

この記事は立命合宿の 3 日目に行われた北大セットのまとめ的記事です。勝手にまとめてしまいました。

※ 随時更新します

問題

実際にコンテストで使われたバージョン

A 問題 | B 問題 | C 問題 | D 問題 | E 問題 | F 問題 | G 問題

正式掲載

A 問題 | B 問題 | C 問題 | D 問題 | E 問題 | F 問題 | G 問題

解説

A 問題 | B 問題 | C 問題 | D 問題 | E 問題 | F 問題 | G 問題

テスターやった人並みの一言

解法詳細はスライドを見てください。

  • A は (奇数個ある種類数) / 2。誤読しやすいのは正直申し訳ないし誤読する人が出るだろうなあという予想はしていたけど、問題をちゃんと読むという注意力を問うた部分もある (たとえ誤読していてもサンプル 3 で気づくと思ったんですけどね・・・)
  • B は 「x 点上げるときに 8 位以上になれるか?」で二分探索できる (全探索でも可)。同率の扱いや、得点を上げた時の順位変動に注意。
  • C はパスグラフとしてみるとよくて、端を特定したあとは端と任意の頂点について質問攻め。順列のサイズが 1 のときに注意。
  • D はパラメータが増えたダイクストラで、距離をキーにして priority_queue を使えばよい。N を通過したか否かのフラグを持つと楽に書ける。速度をキーにしてもそこそこうまくいくけど 1 ケースだけ落ちます (後述)
  • E は区間を set に突っ込むときの set のサイズがそのまま答えになる (更新は頑張らなければいけない)
  • F は根つき木の同型性判定を応用して、木の中心を根として同型性判定を行う
  • G は パターン順列の run の数に応じて解法を変えていく。run の数 が 4 以上なら確実に No
    • run の数 = 1 なら LIS
    • run の数 = 2 なら 山 → 谷 のパターンに合うように点を貪欲にとる
    • run の数 = 3 なら 山 → 谷 → 山 のパターンに合うように貪欲に

コード例 (tsutajiro 解)

A 問題 | B 問題 | C 問題 | D 問題 | E 問題 | F 問題 | G 問題

ちょっとした裏話

  • 作問班唯一の学部生・・・ (若人がいない)
  • 情報系学生の底辺なので作問するまで Git の使い方が分からなかった (爆笑)
  • B は最初二分探索が想定解であったため、チーム数が  10^{5} まで、得点上限が  10^{9} まで、といった感じだったが、 B にしては難しすぎるため全探索可能な範囲に落ちた
  • D で始めの自分の解では距離比較でなく速度比較でダイクストラをやっていて、それで一旦全ケース通ってた
    • → のちに Darsein さんがチャレンジケースを生成して見事に落とされる
    • → 理由は最悪計算量がベルマンフォードと同じになり間に合わないため (しかし通常のベルマンフォードよりは速い模様)
    • → tsukasa_diary さんが SPFA を実装したところ爆速で通る (が、最悪ケースを作るのが難しいためこれは許容する流れに)
  • 先輩方はみんなプロ (それはそう) (精進します)

以上です。

立命館大学プログラミング合宿2017 参加記

立命館大学プログラミング合宿2017 に参加してきました。競技プログラミングの合宿に参加したのは初めてです。 帰りの電車ヒマなので参加記をつらつらと書いていこうと思います。

-1 日目 (作問班参加・準備編)

北大では立命合宿で作問を担当しているので、作問班をサークル内で作らなければなりません。大変そうだけど競プロできる先輩たちといろいろ議論しながら作っていったらためになるだろうなあと思って作問班に参加することに。

立命合宿の日程が北大の卒業式と被ってしまい、先輩方が合宿に出られなくなって当日現地に行くのが私だけになってしまったので、必然的に解説を全部やらないといけない感じになりました。これは全部解いてからいかないとまともに解説できなくてヤバいのでは!?と思ったので全問題のテスターをやることにしました (しかし、F の TLE に悩まされて結局 F 以外のテスターになりました)

作問に必要なツールの使い方は今回で一通り勉強できましたし、何に注意すべきかも分かったので勉強になりました。次に作問する機会があったら活かしたいですね。

0 日目 (移動編)

北の果てから移動するので前日から移動。宿は京都でとってました。烏丸おしゃれすぎてマジヤバい (語彙)

1 日目 (立命 & 阪大セット)

いよいよ合宿スタート!

kenkoooo さん、 kawabys さんとチーム ktky で出ました。

  • 最初 A 問題の担当だったのですが難しく考えすぎてハマる (戦犯)
  • B はあんまり読んでない (kawabys さんが AC)
  • C を見たら各頂点の深さみてよしなに足し合わせたらいけるなあとなってので書いて AC。
  • D は最初アルファベットを頂点にして右隣のアルファベットに辺を張るグラフみたいなのを考えていたけど嘘 (戦犯)
  • E は気づいたら kenkoooo さんが AC していた (すごい)
  • F は kawabys さんと kenkoooo さんが考察していたけどつらかったらしい (自分は他の問題やってて参加できなかった)
  • K を見たら DP なのかなあと思って書いていたけど、教科の最高点が複数人いた場合に詰むことが判明しておしまい (戦犯)

結果、4 完 20 位。全体的に戦犯でした。

懇親会ではいろんな人と話せて面白かったです。競プロサークルの事情はどこも同じようで、最初は大量に入ってくるけどその分幽霊部員もたくさん出るよねーみたいな話をしていました。九大の方々が今後の新歓について真面目に議論していてアツかったです。

2 日目 (会津大セット)

yurahuna さん、 odan さんとチーム toy18 で参加しました。

  • A 問題はやばかったらしい (yurahuna さんが AC)
  • B 問題は lower_boundupper_bound 使えばいいだけだったので書いて AC。
  • C 問題は odan さんから方針の相談をされたので、定数項を素因数分解して入る数字の候補を減らしていこうということを言った (でも実際範囲狭かったのでこれをやらなくても全探索で OK) odan さんは構文解析のプロなのですぐ実装してくれて AC。
  • D 問題は DP を考えていて、最初  10^{8} 回くらいの計算になりそうな感じの DP が出来上がって計算量的に不安だったけど書く → ひたすらバグるので 2 人に相談 → DP の方針を変えようということになり、想定解法と同じ DP にたどり着く → 添え字がややこしすぎる → yurahuna さんとひたすらデバッグをする → AC!!!!

この問題だけでコンテスト時間の半分を消費したといっても過言ではないかもしれません。正直チームメンバーがプロなので通った感じで、自分ひとりだったら完全にあきらめてましたね。本当に感謝です。

  • E 問題は平均最大化するだけといって yurahuna さんが早々に AC (ほんまプロ)

結果、 5 完 8 位。最終的に D が通ったのが大きかったですね。

懇親会はなんと RCO さんの援助があり費用が無料に。競プロ er に理解のある RCO さんに感謝です。自分の卓は北大・会津大・名古屋大・九大と津々浦々から来ている感がすごくて楽しかったです。チンチロやったら 3 つとも 2 の目が出たのはビビった (運を使い果たした)

3 日目 (北大セット)

いよいよ北大セットお披露目のときです。実は結構バタバタしていて、AOJ に初めて問題をアップしたのが前日の夜 10 時くらいで、最終稿が載ったのが当日の午前 9 時前後でした。原因としては私が AOJ 側にコンタクトを取る方法を知らなかったことと、準備の段取りを先輩方に聞くのが足りなかったことですね。次回は反省を生かして 3 日前くらいには上げられるようにしたいです。

コンテスト運営を 1 人でやるのは不可能だからと、ジャッジチーム側に tubo28 さんがまわってくださいました。実際 1 人 (しかも作問側未経験) では運営不可能だったので、本当に助けられました。

バタバタしながらもコンテストがスタート。私は総評とスライドの手直し、それから風船運びをしながら過ごしていました。

問題のこといろいろに関しては、長くなったので別記事を見てください。

プロがたくさんいる中で解説をしなければいけなかったので相当緊張しましたが、何とか終わったので良かったです。次に解説するときまでには自信もって解説できるくらいの力をつけたいですね。

まとめ

オンサイトはいいぞ。初参加でしたがとても楽しかったです。やっぱりチーム戦はいいですね。

あと帰りに買った漫画がよかった。以上です。