tag:crieit.net,2005:https://crieit.net/tags/%E7%AB%B6%E6%8A%80%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0/feed 「競技プログラミング」の記事 - Crieit Crieitでタグ「競技プログラミング」に投稿された最近の記事 2020-07-11T14:53:59+09:00 https://crieit.net/tags/%E7%AB%B6%E6%8A%80%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0/feed tag:crieit.net,2005:PublicArticle/16001 2020-07-11T14:53:59+09:00 2020-07-11T14:53:59+09:00 https://crieit.net/posts/l0rzl-ruby-lib-pq 十分にネストした配列は、リストと見分けがつかない <h1 id="十分にネストした配列は、リストと見分けがつかない"><a href="#%E5%8D%81%E5%88%86%E3%81%AB%E3%83%8D%E3%82%B9%E3%83%88%E3%81%97%E3%81%9F%E9%85%8D%E5%88%97%E3%81%AF%E3%80%81%E3%83%AA%E3%82%B9%E3%83%88%E3%81%A8%E8%A6%8B%E5%88%86%E3%81%91%E3%81%8C%E3%81%A4%E3%81%8B%E3%81%AA%E3%81%84">十分にネストした配列は、リストと見分けがつかない</a></h1> <h2 id="簡易な優先度付きキュー(PriorityQueue)の実装"><a href="#%E7%B0%A1%E6%98%93%E3%81%AA%E5%84%AA%E5%85%88%E5%BA%A6%E4%BB%98%E3%81%8D%E3%82%AD%E3%83%A5%E3%83%BC%28PriorityQueue%29%E3%81%AE%E5%AE%9F%E8%A3%85">簡易な優先度付きキュー(PriorityQueue)の実装</a></h2> <p>こんにちは。<br /> へっぽこ茶コーダーの項垂人です。</p> <p>今回は皆さんに私お気に入りの自作ライブラリ、優先度付きキューの実装をご紹介したいと思います。<br /> たいしたものではありませんがこれが初の技術系記事です。<br /> 競プロ愛好家の皆さんも、そうでない方も、よろしければ是非ご一読下さい。</p> <h3 id="説明"><a href="#%E8%AA%AC%E6%98%8E">説明</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://ja.wikipedia.org/wiki/%E5%84%AA%E5%85%88%E5%BA%A6%E4%BB%98%E3%81%8D%E3%82%AD%E3%83%A5%E3%83%BC">優先度付きキュー</a>(PriorityQueue,以下PQ)とは、その名の通り、いくつかの要素を格納した中から最も評価値の高いもの素早く取り出すことのできるデータ構造です。<br /> C++やPythonでは標準ライブラリとして用意されていますが、残念ながらRuby言語にはないので、競プロ用に自作してみました。<br /> ※詳しい解説はリンク先のwikipediaをどうぞ</p> <h4 id="用途"><a href="#%E7%94%A8%E9%80%94">用途</a></h4> <p>まぁ色々あるのでしょうが、代表的なところではスケジューラーあたりですかね。<br /> コンピューターを使う上ではOSがプロセスやI/Oのスケジューリングやロードバランシングをしてくれます。<br /> これはコンピュータの処理性能に大きく関与するので、OS界隈でスケジューラーの刷新といったらちょっとしたトピックだったりしますね。</p> <p>競プロでの利用例といえば、代表的な最短経路探索アルゴリズム、ダイクストラ法を実装するのに有用です。</p> <p>余談ですがこれを作った動機の一旦は、超簡単アルゴリズムと評判の<a target="_blank" rel="nofollow noopener" href="https://ja.wikipedia.org/wiki/%E3%83%99%E3%83%AB%E3%83%9E%E3%83%B3%E2%80%93%E3%83%95%E3%82%A9%E3%83%BC%E3%83%89%E6%B3%95">ベルマンフォード法</a>を実装したら、頂点数400程度のグラフでも実行時間制限2秒を超過してしまった、という苦い経験によるものだったりします。<br /> なおCrystalで提出するとRubyより10倍速くて通るんですね……</p> <h4 id="特徴"><a href="#%E7%89%B9%E5%BE%B4">特徴</a></h4> <p>PQの実装は多々ありますが、本作では次の点が特徴的です</p> <ul> <li>データ構造はほぼ配列そのもの、余分な管理領域や初期化が不要</li> <li>ほんのひと手間で数値以外でも任意の真値(nil,false以外)を収納可能</li> <li>比較演算子一つ実装すれば、任意の比較関数を適用可能</li> <li>コード量が少ない(十数行)</li> <li>速さはそれほどではなく、最悪ケースが存在する(回避策あり)</li> </ul> <p>以下でそれぞれ解説していきます。</p> <h3 id="PQのデータ構造"><a href="#PQ%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E6%A7%8B%E9%80%A0">PQのデータ構造</a></h3> <h4 id="ごく単純な実装:配列"><a href="#%E3%81%94%E3%81%8F%E5%8D%98%E7%B4%94%E3%81%AA%E5%AE%9F%E8%A3%85%EF%BC%9A%E9%85%8D%E5%88%97">ごく単純な実装:配列</a></h4> <p>さて、PQの目的というのは、ある評価基準で順番に値を取り出すこと、でしたね。<br /> 最も単純な実装としては、リストや配列にデータ格納し、min/maxを取り出すとよいですね。<br /> しかしそれでは値を取得するたびにO(<em>N</em>)の探索コストがかかってしまいます。</p> <p>少し効率化してみましょう。<br /> 優先度の高い値に素早く辿り着くには、優先度順にソートしておくのが効果的です。<br /> データが整列済みであれば、先端か末尾の値を見るだけでよく、O(<em>1</em>)で取得できます。</p> <p><code>PQ = [4,5,9,1,8].sort</code> #=> [1,4,5,8,9]</p> <p>この後ただ値を取り出していくだけならこれで十分です。<br /> しかし値を追加するならば、次に要素を取得するまでの間に再び優先度順に並び変えておく必要があり、そうするとソートの回数と重さO(<em>NlogN</em>)が問題になってきます。</p> <p>それでも要素数が少なかったり、なにかしら整列回数を抑える工夫があれば、時には単純なリストや配列でも十分な場面もあるでしょう。<br /> 例えば、<a target="_blank" rel="nofollow noopener" href="https://atcoder.jp/contests/abc141/submissions/7542010">ABC141-D</a> ではソート後に追加された要素の中で最も優先度の高いものを記録しておき、先頭要素の優先度が追加要素の最大値を上回っている間はソートしない、という手法で整列回数を低減しています。</p> <h4 id="一般的な実装:ツリー、というかヒープ"><a href="#%E4%B8%80%E8%88%AC%E7%9A%84%E3%81%AA%E5%AE%9F%E8%A3%85%EF%BC%9A%E3%83%84%E3%83%AA%E3%83%BC%E3%80%81%E3%81%A8%E3%81%84%E3%81%86%E3%81%8B%E3%83%92%E3%83%BC%E3%83%97">一般的な実装:ツリー、というかヒープ</a></h4> <p>データ量が多かったりソート回数が多いなど、全体を整列させるコストが高すぎる場合は、要素を追加するたびに整列済みとなるようなデータ構造、主に木構造などを利用します。<br /> 次のようなアルゴリズムがあるそうです。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://ja.wikipedia.org/wiki/%E4%BA%8C%E5%88%86%E3%83%92%E3%83%BC%E3%83%97">二分ヒープ</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://ja.wikipedia.org/wiki/%E4%BA%8C%E9%A0%85%E3%83%92%E3%83%BC%E3%83%97">二項ヒープ</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://ja.wikipedia.org/wiki/%E3%83%95%E3%82%A3%E3%83%9C%E3%83%8A%E3%83%83%E3%83%81%E3%83%92%E3%83%BC%E3%83%97">フィボナッチヒープ</a></li> </ul> <p>はい、これまたWikipediaの引き写しですが、なんだかややこしそうですね。<br /> 木の高さ調整やら回転やら、なんだかへっぽコーダーが実装するのは気が重いです。<br /> 真面目に読み解いていけば理解できるかもしれませんが、途中で面倒臭くなってしまいそう。<br /> 理解力のある頭脳をお持ちだったり、速度が重要となる場合には、十分に評価・検証されてきたこれらの代表的アルゴリズムを実装された方が良いでしょう。<br /> これらは性能が保障されているうえ、PQの実装に限らず多方面で応用も効きそうですし。</p> <h4 id="本作の実装:ネストした配列"><a href="#%E6%9C%AC%E4%BD%9C%E3%81%AE%E5%AE%9F%E8%A3%85%EF%BC%9A%E3%83%8D%E3%82%B9%E3%83%88%E3%81%97%E3%81%9F%E9%85%8D%E5%88%97">本作の実装:ネストした配列</a></h4> <p>残念なことにあまりおつむのよろしくない私としましては、多少遅くとももう少しシンプルなものを考えてみたいです。</p> <p> 構築が簡単でアクセス性能やメモリ効率の良い配列をベースに、常に整列済みの状態を維持していきます。<br />  でも多量の要素を何度もソートするのは低速なのでできません。<br />  値の挿入や除去が遅すぎても困ります。<br />  さてどうしましょう?</p> <p> ソート済み配列上で位置の特定は二分探索O(<em>logN</em>)できるので十分速いですね。<br />  ただし配列への要素挿入は、後続要素を後ろにずらしてしまうとO(<em>N</em>)になるのでNG。<br />  Rubyの配列はなんでも格納可能なので、挿入先の値を配列に置き換えることにします。<br />  ネストした配列にさらに要素を加えるときは、必要ならば再帰的にさらにネストします。</p> <p>それでどうなるかと言いうと、次のようなネストした配列(多分木)ができます。</p> <blockquote> <p>[1, [2, [3, 4], 5], [6, 7], 8, 9]</p> </blockquote> <p>ネストしていても見た目で完全に整列済みなのが分かりやすいですね。<br /> もう少し詳しくこれを操作する様子を解説します。</p> <h5 id="PQ構築と値の挿入"><a href="#PQ%E6%A7%8B%E7%AF%89%E3%81%A8%E5%80%A4%E3%81%AE%E6%8C%BF%E5%85%A5">PQ構築と値の挿入</a></h5> <p>次のような整列済みの配列があるとします。<br /> 格納されているのは全て値(ここでは整数)です。</p> <blockquote> <p>[1, 2, 6, 8, 9]</p> </blockquote> <p>今回これを、ただこのままの状態でも、考案したPQのデータ構造として全く正当なものとみなします。<br /> 整列済み配列がPQになることは先に書いた通りです。<br /> 構築がカンタンでなんとも都合が良いですね。😉</p> <p>ではここに3を挿入を挿入してみましょう。<br /> 既に整列済みなので、挿入位置は標準ライブラリの二分探索(Array#bsearch_index)で見つかります。<br /> 後続要素を動かさずに値2の位置に値3を挿入するため、値2,3の配列に置き換えます。</p> <blockquote> <p>[1, <em>[2, <strong>3</strong>]</em>, 6, 8, 9]</p> </blockquote> <p>ネスト深度付きで表すとこうです。</p> <blockquote> <p>深さ0: [1, <em>[ ]</em>, 6, 8, 9]<br /> 深さ1: <em>[2, <strong>3</strong>]</em></p> </blockquote> <p>配列がネストして1段深くなるかわりに、深さ0の配列のサイズには変化ありません。<br /> ネストすることで計算量の嵩む後続要素の移動が不要となります。<br /> 一方、配列末尾への要素追加や除去での伸縮にはコストがあまりかからないため、末尾追記はそのまま実行します。<br /> 続いて5を挿入してみましょう。<br /> ※なおRubyにおいては配列先頭でも伸縮が高速です</p> <blockquote> <p>全体 : [1, [2, 3, <strong>5</strong>], 6, 8, 9]<br /> 深さ0: [1, [], 6, 8, 9]<br /> 深さ1: [2, 3, <strong>5</strong>]</p> </blockquote> <p>そしてさらに4を追加。<br /> 深さ1の配列は先頭値1末尾値5なので、中間にもう1段配列をネストして挿入します。<br /> つまり3の値を[3,4]の配列に置き換えます。</p> <blockquote> <p>全体 : [1, [2, <em>[3, <strong>4</strong>]</em>, 5], 6, 8, 9]<br /> 深さ0: [1, [], 6, 8, 9]<br /> 深さ1: [2, <em>[]</em>, 5]<br /> 深さ2: <em>[3, <strong>4</strong>]</em></p> </blockquote> <p>ご覧の通り全体の配列も、どの深さの部分配列においても、全てが整列済みとなるネストした配列ができました。<br /> ただしこのとき配列中には数値と多段にネストした配列が混在しています。<br /> 値を挿入位置を探るとき、数値と配列ではそのまま値の比較ができません。<br /> 配列の中から値をの箇所を探し出す必要がありますが、その位置が不定だと面倒です。</p> <p>値を探す処理が重くならないよう、<strong>配列を構成する際、先頭要素は常に数値とする</strong>と定めることにします。<br /> そうすれば二分探索の比較関数中で、"オブジェクトが値ならそのまま"、"配列ならば先頭要素の値"、を評価値として比較できます。</p> <p><code>pq.bsearch_index {|x| n <= ((x.is_a? Array) ? x[0] : x) }</code></p> <p>返値indexの要素が配列であれば再帰的に同じ探索操作をして、最終的にindex位置の値が数値ならそこが挿入する位置です。<br /> ※ちなみにその後の処理を簡略化するため、どうせなら<strong>先頭だけでなく末尾も常に数値</strong>となるよう操作しておきましょう。</p> <p>このような感じのコードになります<br /> <em>※ <strong>わりと短縮気味なので</strong>、目が潰れないよう注意</em></p> <pre><code class="ruby">def enq(a,x) # a:整列済み配列, x:挿入する値 b,i = a,-1 until a.frozen? # while a.is_a? Array return a << x if (x<=>a[-1]) >= 0 return a.unshift x if (x<=>a[0]) <= 0 a = (b=a)[i = a.bsearch_index{|y| 0 > (x<=>(y.frozen? ? y: y[0]))} - 1] end if a[i] return b.insert(i+1, x) if b[i+2] == nil if i > 0 then b[i] = [a,x] elsif (a=b[1]).frozen? then b[1] = [x,a] else enq a,x end end </code></pre> <h5 id="値の取得・除去"><a href="#%E5%80%A4%E3%81%AE%E5%8F%96%E5%BE%97%E3%83%BB%E9%99%A4%E5%8E%BB">値の取得・除去</a></h5> <p>上述の操作で次のようなネストした配列ができあがりました。</p> <blockquote> <p>[1, [2, [3, 4], 5], [6, 7], 8, 9]</p> </blockquote> <p>構築したPQから最優先の値を参照するには、評価基準に応じて配列の先頭(最小値)または末尾(最大値)を見ればよいわけです。</p> <p>そして不要になった値は、shift(先頭除去)またはpop(末尾除去)で取り除きます。<br /> 次の例では先頭の1を除去します。</p> <blockquote> <p>[[2, [3, 4], 5], [6, 7], 8, 9]</p> </blockquote> <p>たったこれだけですと言いたいところですが、気を付けないと「配列の先頭と末尾は値に限定する」決まりが崩れてしまいます。<br /> 先頭/末尾除去後の次の値が配列だった場合、その配列を1段均して挿入しましょう。<br /> ここでは先頭要素 [2, [3, 4], 5] を取り出し、先端に展開します。</p> <blockquote> <p>[2, [3, 4], 5, [6, 7], 8, 9]</p> </blockquote> <p>すると再びのPQの条件を満たした構造に戻りました。<br /> この状態であれば、続けて値を取り出すことも、別の値を挿入するも自由に可能です。</p> <p>実際のコードがこちら<br /> ※ メソッド名が謎ですが、ただ短くしたかっただけですご容赦ください</p> <pre><code class="ruby">def deq(a) r=a.shift; a[0].frozen? or a.unshift *a.shift; r end def poq(a) r=a.pop; a[-1].frozen? or a.push *a.pop; r end </code></pre> <h5 id="コードの解説"><a href="#%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E8%A7%A3%E8%AA%AC">コードの解説</a></h5> <ul> <li>スプラット演算子(<code>*</code>)で、展開した配列を引数として渡せる <ul> <li>先頭要素の展開:<code>a.unshift *a.shift</code></li> <li>末尾要素の展開:<code>a.push *a.pop</code></li> </ul></li> <li>フリーズしていない要素は配列とみなす <ul> <li><code>x.is_a? Array</code> よりも <code>!x.frozen?</code> の方が短い</li> </ul></li> <li>フリーズしている要素は<strong>何であれ</strong>値とみなす</li> <li>値要素の大小比較には <code><=></code> 演算子を利用する</li> </ul> <h5 id="利点"><a href="#%E5%88%A9%E7%82%B9">利点</a></h5> <ul> <li>整列済みの配列そのものがデータ構造である <ul> <li>特別な初期化や構築の処理ほぼ不要 (sortするだけ)</li> <li>ネストしない1次元の配列に戻すのも簡単 (flattenするだけ)</li> <li>別に専用の管理領域を必要としない</li> </ul></li> <li>コード実装量が少ない (ゴルフ好き) <ul> <li>挿入は<strong>数行</strong>、除去は<strong>1行</strong><br /> ※1行の定義について異論はあるかも</li> </ul></li> <li>フリーズしていれば任意の真値(nil,false以外)を値要素にできる <ul> <li>値は数値(Numeric)に限定されない</li> <li>文字列 <code>-"String"</code> では辞書順で並ぶ</li> <li>配列も格納している子要素の順に並ぶ <ul> <li>評価値と関連付けたいオブジェクトを同時に収容できる</li> </ul></li> <li><code><=></code> 比較演算子さえ実装すれば任意の基準で整列できる <ul> <li>本当は <code><</code> を使った方がシンプルだが、その演算子はArrayが標準で持っていない</li> <li>特異メソッドなどで後付け可能</li> </ul></li> </ul></li> </ul> <h5 id="欠点"><a href="#%E6%AC%A0%E7%82%B9">欠点</a></h5> <ul> <li>ヒープソートなどと比べると多少遅い <ul> <li>だいたい2倍とか、悪い状況ではもっと</li> </ul></li> <li>入力値が最悪パターンの場合、多分木が構成できず連結リストになってしまう <ul> <li><strong>タイトル回収</strong>🙄</li> <li>最悪パターン時の構成:<code>[1,[],9] ⇒ [2,[],8] ⇒ [3,[],7] ⇒ [4,5,6]</code></li> <li>挿入の計算量がO(<em>N</em>)  ※この実装ではネスト深度はN/18程度</li> <li>評価値の差の絶対値が最小の最近傍要素に挿入するとネスト深度を軽減できる <ul> <li>log2(N)程度の深さに収まる?? ※未検証</li> <li>幾分発生しづらい状況なので、対処を入れるかは要検討</li> <li>評価値の差分を計算できる数値でしか使えない</li> </ul></li> </ul></li> </ul> <h3 id="所感"><a href="#%E6%89%80%E6%84%9F">所感</a></h3> <p>いやはや紹介記事を書くというのは、理解を深めるのにとても有用ですね。<br /> 自分で作ったものながら、数値以外も収納できるとか、最悪パターンはどうだとか、考えてもいませんでした。<br /> この手法の他にも、ネストした配列ではなく連結リストのリストか配列を作って、マージソート的な手法が使えないかなども思い付きはしましたが、ツリー然とした構造を作るのなら、その前にヒープやその他の木構造を習得した方が良さそうです。<br /> 余裕があればまた検討してみたいところです。</p> <p>ただ今回初記事とはいっても、想定の10倍くらい時間がかかりました。<br /> 本当ならゴルフ仕様のコードをちゃんと読めるようにするとか、モジュールにまとめてクラス化もするとか、色々やりたかったけれど無限に時間が溶けてゆくのでやめにします。<br /> あまりこだわりすぎると記事を書き上げるのも頓挫しそうなので。<br /> もし何か機会があれば記事をアップデートするなり出来ればとおもいます。<br /> 他にも面白いと思った競プロの問題などネタはまだあるので、もう幾つかはくらいは書きあげたいですね。<br /> とりあえず次はこのPQでAtCoder最強園児に挑んでみましょうか。</p> <p>あと最後に、このような「整列済みのネストした配列」という構造を持つのは、至極単純で自然な発想ではないかと思っています。<br /> もしこれに何かしら名前が付いているのをご存知でしたら、どなたか教えてください。</p> <h3 id="おまけ"><a href="#%E3%81%8A%E3%81%BE%E3%81%91">おまけ</a></h3> <h4 id="実装全体"><a href="#%E5%AE%9F%E8%A3%85%E5%85%A8%E4%BD%93">実装全体</a></h4> <pre><code class="ruby">def deq(a) r=a.shift; a[0].frozen? or a.unshift *a.shift; r end def poq(a) r=a.pop; a[-1].frozen? or a.push *a.pop; r end def enq(a,x) (b=a)[i=-1] && until a.frozen? 0 > (x<=>a[-1]) or return a << x 0 < (x<=>a[0]) or return a.unshift x a = (b=a)[i = a.bsearch_index{|y| 0 > (x<=>(y.frozen? ? y: y[0]))}-1]; end b[i+2] ? (i>0 ? b[i]=[a,x]: ((a=b[1]).frozen? ? b[1]=[x,a]: enq(a,x))) : b.insert(i+1, x) end </code></pre> <h4 id="途中要素検索・削除対応"><a href="#%E9%80%94%E4%B8%AD%E8%A6%81%E7%B4%A0%E6%A4%9C%E7%B4%A2%E3%83%BB%E5%89%8A%E9%99%A4%E5%AF%BE%E5%BF%9C">途中要素検索・削除対応</a></h4> <pre><code class="ruby">def inq(a,x) # 値xを挿入できる位置と、そこにある値yを返す y == a[i] == b[j][i] a[i=0] && (y=a; z=until y.frozen? 0 > c=(x<=>y[-1]) or break [y[c+=y.size-1],c] 0 < c=(x<=>y[0]) or break [(y[c] if c>=0),c] a,b,j,y = y,a,i,y[i=y.bsearch_index{|y| 0>(x<=>(y.frozen? ? y: y[0]))}-1] end) && [*z,y,i,a] or [y,i,a,j,b] end def enq(a,x) y,i,a,j,b = inq a,x a[i+9] or return a[i+1] ? a.insert(i+1,x) : a<<x y or return a.unshift x z = i>0 ? [y,x]: [x,y=a[i=1]] y.frozen? ? a[i]=z: enq(y,x) end def rmq(a,x) # a:PQから値xを除去する (xが無ければ偽を返す) y,i,a,j,b = inq a,x (y<=>x)==0 and (a.delete_at i; a[1] or j && b[j]=a[0]; x) end </code></pre> <h4 id="検索削除対応/スタックトレース版"><a href="#%E6%A4%9C%E7%B4%A2%E5%89%8A%E9%99%A4%E5%AF%BE%E5%BF%9C%2F%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B9%E7%89%88">検索削除対応/スタックトレース版</a></h4> <pre><code class="ruby">def inq(a,x,s=0) # 値xを挿入できる位置の値yと、そこに至る経路sを返す a[i=0] && i=until a.frozen? 0 > c=(x<=>a[-1]) or break a.size-1+c 0 < c=(x<=>a[0]) or break c s << i = a.bsearch_index{|y| 0 > (x<=>(y.frozen? ? y: y[0]))} - 1 a = (b=a)[i]; end i ? (s<< i;a[i] if i>=0) : a end def enq(a,x) # y == a.dig(*s) y = inq a,x,s=[] i = s.pop a = a.dig *s if s[0] a[i+3] or return a[i+1] ? a.insert(i+1,x) : a<< x y or return a.unshift x z = i>0 ? [y,x]: (a[0],x=x,a[0] if i<0; [x,y=a[i=1]]) y.frozen? ? a[i]=z: enq(y,x) end def rmq(a,x) # a:PQから値xを除去する (xが無ければ偽を返す) y = inq a,x,s=[] return if (y<=>x)!=0 i,j = s.pop,s.pop b = s[0] ? a.dig(*s) : a c = j ? b[j] : a c.delete_at i c[1] or j && b[j]=c[0] x end </code></pre> 項垂人 tag:crieit.net,2005:PublicArticle/15986 2020-06-29T02:39:09+09:00 2020-06-29T02:47:22+09:00 https://crieit.net/posts/koya-abc172-d AtCoder Beginner Contest 172 D - Sum of Divisors <h1 id="問題文"><a href="#%E5%95%8F%E9%A1%8C%E6%96%87">問題文</a></h1> <p>要約:<br /> k=1からNまで、<img src="http://chart.apis.google.com/chart?cht=tx&chl=k%20*%20f(k)" alt="k * f(k)" /> を足し上げる。<br /> ただし、<img src="http://chart.apis.google.com/chart?cht=tx&chl=f(k)" alt="f(k)" />とは、正の整数であって、kの約数であるものの個数。</p> <p><a target="_blank" rel="nofollow noopener" href="https://atcoder.jp/contests/abc172/tasks/abc172_d">https://atcoder.jp/contests/abc172/tasks/abc172_d</a></p> <h1 id="O(N^2)"><a href="#O%28N%5E2%29">O(N^2)</a></h1> <p><img src="http://chart.apis.google.com/chart?cht=tx&chl=1%20\le%20j%20\le%20k" alt="1 ≦ j ≦ k" /> なるすべての j について、ループを回して k の約数であるかを調べると <img src="http://chart.apis.google.com/chart?cht=tx&chl=O%28N^2%29" alt="O(N^2)" />になります。<br /> <a href="https://crieit.now.sh/upload_images/5b556d92f0305f87aa876d79c8c86a875ef8bb4e364c6.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/5b556d92f0305f87aa876d79c8c86a875ef8bb4e364c6.png?mw=700" alt="image" /></a></p> <pre><code class="cpp">#include <iostream> using ll = long long; ll f_n2(int N) { ll ans = 0; for (int k = 1; k <= N; ++k) { // 約数カウンタ int c = 0; for (int j = 1; j <= N; ++j) { // 割り切れるなら増やす if (k % j == 0) ++c; } ans += (ll)k * c; } return ans; } int main() { int N; std::cin >> N; ll ans = f_n2(N); std::cout << ans << '\n'; } </code></pre> <p><code>N <= 10^7</code> ですので、当然、3secには間に合いません。</p> <h1 id="O(N√N)"><a href="#O%28N%E2%88%9AN%29">O(N√N)</a></h1> <p>約数を数える部分は<img src="http://chart.apis.google.com/chart?cht=tx&chl=O%28\sqrt%20N%29" alt="O(√N)" /> にできます。</p> <p>たとえば 20 の約数がいくつあるか数えることを考えましょう。<br /> 20 = 1 * 20<br /> 20 = 2 * 10<br /> 20 = 4 * 5</p> <p>このように、割り切れたときには必ず相方がいることが分かります。<br /> 左側は必ず√20 より小さく、右側は必ず√20 より大きくなっているので、<br /> (そうしないと、掛けたときに20になりません)<br /> √20 = 4.47... まで探索して2倍すれば十分であることが分かります。</p> <p>ただし、16のような平方数では、<br /> 16 = 1 * 16<br /> 16 = 2 * 8<br /> 16 = 4 * 4</p> <p>このようになることがあるので、<br /> √k ぴったりのときは1つしか数えないような工夫が必要です。<br /> <a href="https://crieit.now.sh/upload_images/d288b0b0de53bab5a2c736d5d6b63b955ef8bd84e9ace.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/d288b0b0de53bab5a2c736d5d6b63b955ef8bd84e9ace.png?mw=700" alt="image" /></a></p> <pre><code class="cpp">#include <iostream> #include <cmath> using ll = long long; ll f_nsqn(int N) { ll ans = 0; for (int k = 1; k <= N; ++k) { // 約数カウンタ int c = 0; int sqk = (int)sqrt(k); for (int j = 1; j <= sqk; ++j) { if (k % j == 0) { // k = j * j if (k / j == j) c++; // それ以外 else c += 2; } } ans += (ll)k * c; } return ans; } int main() { int N; std::cin >> N; ll ans = f_nsqn(N); std::cout << ans << '\n'; } </code></pre> <p>N = 10^7 のとき、N√N = 3 * 10^10 くらいになるので、これでも間に合いません。</p> <h1 id="O(NlogN)"><a href="#O%28NlogN%29">O(NlogN)</a></h1> <p>k の約数を探しにいくのではなく、k 自身が将来の数の約数である、というふうに考えてみます。</p> <p>たとえば、k=2 とすると、<br /> 2, 4, 6, 8, 10 はすべて 2 の倍数であり、<br /> 言い換えると、これらはすべて 2 を約数にもつ数でもあります。<br /> したがって、f(2), f(4), f(6), f(8), f(10) は 2 が約数であることを知ることができます。<br /> 約数の数がひとつ増えたことになるので、+1 しておきます。<br /> このようにすると、たとえば f(10) は、k=1, 2, 5, 10 のときに +1 されることになり、<br /> これはつまり約数の数になっています。<br /> <a href="https://crieit.now.sh/upload_images/3069b2ecca771b8ee9b57dee6b0b69425ef8c147ace0b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3069b2ecca771b8ee9b57dee6b0b69425ef8c147ace0b.png?mw=700" alt="image" /></a></p> <p>図を見ると分かるように、これのループ回数は<br /> N + N/2 + N/3 + ...... + N/N<br /> になっています。</p> <p>これの計算量を見積もるのは難しそうですが、<br /> 1/1 + 1/2 + 1/3 + ...... + 1/N<br /> の形の和を調和級数と呼び、なんとその値はだいたいlogNになります。<br /> したがって、この解法の計算量は<img src="http://chart.apis.google.com/chart?cht=tx&chl=O(N%5Clog%20N)" alt="O(NlogN)" />です。</p> <p>約数列挙の方針では「約数かどうかを判定してみたものの約数ではなかった」という場合があるのに対して、<br /> こちらでは必ず約数になっています。直感的にも、こちらのほうが効率がよさそうです。</p> <pre><code class="cpp">#include <iostream> #include <vector> using ll = long long; ll f_nlogn_memo(int N) { std::vector<int> p(N+1, 0); for (int k = 1; k <= N; ++k) { for (int j = k; j <= N; j += k) { // k, 2k, 3k, ... の約数カウントを増やす ++p[j]; } } ll ans = 0; for (int k = 1; k <= N; ++k) { ans += (ll)k * p[k]; } return ans; } int main() { int N; std::cin >> N; ll ans = f_nlogn_memo(N); std::cout << ans << '\n'; } </code></pre> <p>C++でギリギリ通ります。<br /> 空間計算量がO(N)なので、10^7 ものメモリを確保するのが遅いのかもしれません。</p> <h1 id="O(NlogN) その2"><a href="#O%28NlogN%29+%E3%81%9D%E3%81%AE2">O(NlogN) その2</a></h1> <p>もとの問題に立ち返ってみると、求めたいものは k * f(k) の総和でした。</p> <p>ここまでの図をじーっと見ていると、<br /> そもそも○を使う必要はなく、数字で埋めてしまっていいことがわかります。<br /> この表中の数字をすべて足しさえすればよいので、<br /> 配列を使わなくても実装できます。<br /> <a href="https://crieit.now.sh/upload_images/dc494292a387e6a735bd8357d917d82a5ef8c6a103de3.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/dc494292a387e6a735bd8357d917d82a5ef8c6a103de3.png?mw=700" alt="image" /></a></p> <pre><code class="cpp">#include <iostream> using ll = long long; ll f_nlogn(int N) { ll ans = 0; for (int k = 1; k <= N; ++k) { for (int j = k; j <= N; j += k) { // 数字を直接足し込む ans += j; } } return ans; } int main() { int N; std::cin >> N; ll ans = f_nlogn(N); std::cout << ans << '\n'; } </code></pre> <p>C++では余裕をもって通ります。<br /> 言語によってはこれでもまだ通らないことがあるようです。<br /> (Rubyで6300ms前後でした)</p> <h1 id="O(N)"><a href="#O%28N%29">O(N)</a></h1> <p>「表中の数字をすべて足し上げる」という方針をもってさらに図を見つめていると、<br /> 横方向の足し算は等差数列の和であり、O(1) で計算できてしまうことが分かります。<br /> 横方向がO(1) で計算できたので、全体ではO(N)になります。<br /> <a href="https://crieit.now.sh/upload_images/b8bcba0c3337ab7fa5a44a20edcdd8a45ef8cc92edc77.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b8bcba0c3337ab7fa5a44a20edcdd8a45ef8cc92edc77.png?mw=700" alt="image" /></a></p> <pre><code class="cpp">#include <iostream> using ll = long long; // 1からnまでの和 ll sn(ll n) { return (ll)n * (n+1) / 2; } ll f_n(int N) { ll ans = 0; for (int k = 1; k <= N; ++k) { ans += sn(N/k) * k; } return ans; } int main() { int N; std::cin >> N; ll ans = f_n(N); std::cout << ans << '\n'; } </code></pre> <h1 id="O(√N)"><a href="#O%28%E2%88%9AN%29">O(√N)</a></h1> <p>表中の数字をすべて足し上げればよいので、もはや見た目の順番通りに足す必要はありません。<br /> 表の数字を左に詰めると、対称性が見てとれます。<br /> √N 個のL字型について、1つのL字は O(1) で計算できるので、<br /> 全体で O(√N) で計算ができます。<br /> <a href="https://crieit.now.sh/upload_images/20514c2e76826449995092ca734237295ef8d09be4d0a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/20514c2e76826449995092ca734237295ef8d09be4d0a.png?mw=700" alt="image" /></a></p> <pre><code class="cpp">#include <iostream> #include <cmath> using ll = long long; ll sn(ll n) { return (ll)n * (n+1) / 2; } ll f_sqn(int N) { int sqN = (int)sqrt(N); ll ans = 0; for (int k = 1; k <= sqN; ++k) { ans += (sn(N/k) - sn(k)) * k * 2 + k * k; } return ans; } int main() { int N; std::cin >> N; ll ans = f_sqn(N); std::cout << ans << '\n'; } </code></pre> <h1 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h1> <p>いろんなオーダーの解法があって、少しずつ改善されていく様子が面白かったです。</p> <p>なお、<img src="http://chart.apis.google.com/chart?cht=tx&chl=O(N%5E%7B%5Cfrac%7B1%7D%7B3%7D%7D)" alt="O(N^1/3)" />の方法もあるらしいです。</p> ドッグ tag:crieit.net,2005:PublicArticle/15872 2020-04-27T12:09:32+09:00 2020-04-27T12:22:31+09:00 https://crieit.net/posts/cpp-overflow-int-abc162-d C++でint型同士の演算結果をlong longで受け取っても、オーバーフローで正しい値にならない場合がある - ABC 162 D - RGB Triplets <p>緊急事態宣言に伴う自粛期間中なので、昨日は家にこもってAtCoder(競技プログラミング)の過去問を練習していました。</p> <p>昨日は「<a target="_blank" rel="nofollow noopener" href="https://atcoder.jp/contests/abc162/tasks/abc162_d">ABC 162 D - RGB Triplets</a>」を解いていたのですが、なぜかtestcase_17とtestcase_18だけACにならず、長時間悩みました。</p> <p>結論から言ってしまうと<strong>int型同士の演算によるオーバーフロー</strong>が原因だったのですが、実際のプログラム例を掲載します。</p> <h2 id="オーバーフローで結果が異なる例"><a href="#%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%95%E3%83%AD%E3%83%BC%E3%81%A7%E7%B5%90%E6%9E%9C%E3%81%8C%E7%95%B0%E3%81%AA%E3%82%8B%E4%BE%8B">オーバーフローで結果が異なる例</a></h2> <p>次のC++プログラムにある「R1,R2」「G1,G2」「B1,B2」の各変数は、それぞれ1333を代入しています。<br /> 1333は小さな数で、int型の範囲内です。</p> <p>ただし計算結果は大きな値となるため、long longで受け取ることにしました。</p> <pre><code class="cpp">#include <bits/stdc++.h> using namespace std; int main() { int R1 = 1333; int G1 = 1333; int B1 = 1333; long long res1 = R1 * G1 * B1; cout << res1 << endl; // -1926374259 long long R2 = 1333; long long G2 = 1333; long long B2 = 1333; long long res2 = R2 * G2 * B2; cout << res2 << endl; // 2368593037 } </code></pre> <p>見てわかるように、計算結果をlong longで受け取ったとしても、1333をint型にしたパターンでは結果が正しくなりません。</p> <h2 id="ハードコードで書き直してみる"><a href="#%E3%83%8F%E3%83%BC%E3%83%89%E3%82%B3%E3%83%BC%E3%83%89%E3%81%A7%E6%9B%B8%E3%81%8D%E7%9B%B4%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">ハードコードで書き直してみる</a></h2> <p>変数に代入するパターンではオーバーフローが起きていたため、次にハードコードで書き直してみました。</p> <pre><code class="cpp">#include <bits/stdc++.h> using namespace std; int main() { // ハードコードのint long long res1 = 1333 * 1333 * 1333; cout << res1 << endl; // -1926374259 // ハードコードのlong long long long res2 = 1333LL * 1333LL * 1333LL; cout << res2 << endl; // 2368593037 } </code></pre> <p>計算結果は相変わらずですが、私の環境(GCC 9.2.0)では次の警告が発生しました。<br /> これにより、オーバーフローが発生していることがわかります。</p> <pre><code class="sh">$ g++-9 test.cpp test.cpp: In function 'int main()': test.cpp:9:34: warning: integer overflow in expression of type 'int' results in '-1926374259' [-Woverflow] 9 | long long res1 = 1333 * 1333 * 1333; | ~~~~~~~~~~<del>^</del>~~~ </code></pre> <h2 id="同じ型同士の演算と、異なる型による演算の違い"><a href="#%E5%90%8C%E3%81%98%E5%9E%8B%E5%90%8C%E5%A3%AB%E3%81%AE%E6%BC%94%E7%AE%97%E3%81%A8%E3%80%81%E7%95%B0%E3%81%AA%E3%82%8B%E5%9E%8B%E3%81%AB%E3%82%88%E3%82%8B%E6%BC%94%E7%AE%97%E3%81%AE%E9%81%95%E3%81%84">同じ型同士の演算と、異なる型による演算の違い</a></h2> <p>発生した警告の内容を見るに、int型同士の演算結果はintになるため、オーバーフローが発生したと推測できます。<br /> <strong>左辺で計算結果をlong longで受け取ったとしても、右辺の演算の時点でダメだったようです。</strong></p> <p>そこで右辺にlong longを組み合わせてみたところ、想定した値が返却されるようになりました。</p> <pre><code class="cpp">#include <bits/stdc++.h> using namespace std; int main() { // ハードコードのint long long res1 = 1333 * 1333 * 1333; cout << res1 << endl; // -1926374259 // ハードコードのlong long long long res2 = 1333LL * 1333LL * 1333LL; cout << res2 << endl; // 2368593037 // intとlong longを組み合わせた例 long long res3 = 1333 * 1333 * 1333LL; cout << res3 << endl; // 2368593037 } </code></pre> <h2 id="さいごに"><a href="#%E3%81%95%E3%81%84%E3%81%94%E3%81%AB">さいごに</a></h2> <p>1333のような小さな値であれば、単にint型へ保存すれば十分だと思っていました。</p> <p>しかし演算に用いるケースでは、演算結果がとり得る値の範囲や、演算結果の型も考慮したプログラミングが必要であると言えそうです。</p> <p>C++は競プロのために入門コンテンツを使って速習したくらいの実力なのですが、奥が深くまだまだ学べることがたくさんありそう。</p> このすみ