最終更新: 2021-09-14T17:27+0900
AtCoder タグを付けてるけど非公式コンテストです。
この問題はとっかかりがなくて最初から解説を読んだ>解説1、解説2、解説3。
この問題で扱われるグラフは DAG (有向非巡回グラフ) といわれるもので、サイクル(閉路) を含まない有向グラフです。
えっっっ? どこに閉路を含まないって書いてあった? 問題文を3回読み直して気がついた。
辺 i は頂点 Xi から Yi に向かいます。
制約 1≤Xi<Yi≤N
制約が持つ情報量が多すぎて見逃していた。難しい。そして閉路が含まれてるなら含まれてるで、解説3に書かれている強連結成分(SCC)分解を手段として持っておくべきであると。
まだコンテスト期間中だけどソースコード共有の動きがあるくらいなので隠す必要はないかなと>「解説が公開されたら、ソースコードを共有していきましょう」。
N,M,Q = gets.split.map(&:to_i) B = 60 # Bignum に切り替わらないビット数を。 Z = (N-1)/B+2 V = Array.new(N+1){|a| [0]*(Z-a/B) } E = Array.new(N+1){[]}; M.times{ x,y = gets.split E[x.to_i]<<y.to_i } N.downto(1){|a| va = V[a] va[-1] |= 1<<a%B E[a].each{|b| V[b].each_with_index{|v,i| va[i] |= v } if va[a/B-b/B-1][b%B]<1 } } puts$<.map{|ln| a,b = ln.split.map(&:to_i) next %w[No Yes][V[a][a/B-b/B-1][b%B]] }
2枚目の解説画像で満点解答のために「ビット演算で 64 倍高速化」と書かれていることから、Ruby で満点解答が望めないのは明らか。Ruby を使うことによる定数倍のハンデが最初から 100 より大きいので……。手元では V =
の初期化部分だけで2、3秒かかってるもんね。部分点2で満足している。
AtCoder はスクリプト言語に優しいので、定数倍高速化がキーになるのは非公式コンテストならではかな。解説が大いに参考になる。
昨日の ABC204-D - Cooking の解法はどこか(解説1)で見たような形だなあと思いながら書いていた(この日記を書いてるのは月曜だけど、典型90-059の出題は土曜日だったので、日曜の ABC の前に取りかかっていた)。
キーワードからこうだろうと最初に思いついた方法で実装したら解説とちょっと違う。違って良くない。
最初にすべての頂点について到達可能な点を(テキトーにビット演算を使って)調べてからクエリに定数時間で答える、というのがさっき貼り付けたスクリプトだけど、解説ではクエリを 64 並列で調べるようにしていた。クエリが起点にある。
調べたら、重いケースでは頂点数(N=100000)、クエリ数(Q=100000)に対して、実際に到達可能性が参照される頂点の種類が 84000 ちょっとになるものがほとんどだった(例外が 99999)。約 16000 の頂点に関しては到達可能地点を調べたのが無駄だった。まあ、無駄を省こうとするとスタックオーバーフローを避けてごてごてと記述と条件判断が増えるわりに、2割も違わない(それも入力依存)という見かたはできる。
必ずしも良くないばかりでもなくて、頂点数 N に比して辺数 M が大きい場合、想定解法の方が遅くなる。
基本は提出1と同じ。30 ビット 60 ビットでテキトーに区切っていた最大で 10 万ビットになりうるビットフラグを、1つの Bignum にまとめた。
多倍長が許されるのは数百桁までかなと思ってたんだけど、(最大で)10万桁でもすっごく速かった。しかしメモリ制限 1024 MB を超えて 1.24 GB 使ったところで MLE。
N,M,Q = gets.split.map(&:to_i) E = Array.new(N){[]}; M.times{ x,y = gets.split E[N-x.to_i]<<N-y.to_i } V = [nil]*N N.times{|a| va,bs,b = 1<<a,E[a] va |= V[b] if va[b]<1 while b = bs.pop V[a] = va } $<.each{|ln| a,b = ln.split.map(&:to_i) puts %w[No Yes][V[N-a][N-b]] }
頂点番号とビット位置の対応を逆向きにしたのが工夫(提出1でも Z-a/B
という形でやっていた)。たとえば N-1 番目の頂点が N 番目に移動するというのを 0b11 で表現すれば Bignum はいらない。これを素直に 1<<N-1|1<<N で表していたら、ほぼ全ての頂点で N 桁の Bignum が必要になってもおかしくない(頂点1以下全ての頂点が頂点 N に遷移する場合)。しかしあえなく MLE。工夫なしだと 1.29 GB だったから 50 MB だけ減りましたよ。
たぶん Bignum のネックは桁数に比例する部分ではなく、100 桁であれ1万桁であれ必ず必要になる基礎的な部分でのコストなのだろう。その面では 64 ビット版が不利だ。Bignum か非 Bignum かで見れば、上の段落で改善するのは 10 万頂点のうちの 62 頂点程度に過ぎない。桁数部分での寄与は小さい。
これは提出1、2と違って想定解法と同じ方針。ただしクエリ 64 並列ではなく 10 万並列でやっている。
DP がしたいのに Bignum を頂点の数だけ保持すると MLE になるのをどうすれば良いか。TLE を避けたいなら Bignum を使うしかないがどうすれば良いか。頂点番号の小さい順にどんどん処理を進めて、使用済みの情報は辺もクエリも遷移してきたという DP 要素もどんどん捨てていった。一時的には C 配列に処理待ちの Bignum が蓄えられるけど、どこかから遷移して来るまではそれも 0 (Fixnum) だ。
N,M,Q = gets.split.map(&:to_i) E = M.times.map{ gets.split.map(&:to_i) }.sort_by(&:first) A = Array.new(N){[]} B = Array.new(N){[]} Q.times{|q| a,b = gets.split A[-a.to_i]<<q B[-b.to_i]<<q } r = 0 C = [0]*N N.times{ as,bs,c,q = A.pop,B.pop,C.pop c |= 1<<q while q = as.pop r |= c[q]<<q while q = bs.pop C[N-E.shift[1]] |= c while e = E[0] and e[0]+A.size==N } Q.times{|q| puts %w[No Yes][r[q]] }
Ruby で満点がありだったのだなあ。道具のせいにしてあきらめなくて良かった。
メモリもタイムもさらに良くなった。提出3がベースだけど、クエリごとに1ビットを使うのではなく、スタート地点 A が共通するクエリが同じビットを共有する。
こちらを参考にして。
昨日はうしさんのライブラリを参考にして解いたため、(A_i,B_i)というクエリを処理するときにはdp[A_i]|=1<<iとしていた。そうではなくdp[u]|=1<<uとし、すべてのペアについて問題を解くことにすれば、頂点番号uに対しては1..uに対応するbitしか保存しなくてよいため、必要なメモリが\frac{N^2}2くらいになるらしい。
頂点番号が小さい順に処理をするという提出3の流れと、頂点番号が小さいものから0に近いビットを割り当てるというのがマッチして、ビットの同時使用量が節約できる。
整理したからそのまま同じではないけど、だいたいこんな雰囲気>typical90_59_nosub5.rb27
@2021-09-14 結局最後のも提出した>提出 #25841973 (1149 ms)
最終更新: 2021-06-05T05:03+0900
解説画像はこちら>https://github.com/E869120/kyopro_educational_90/blob/main/editorial/057.jpg
この問題はとても良かった。確実に力になった。問題を解く選択肢が増えた。キーワードは「行列の掃き出し法を知ろう」だって。
最初は独力でパネルを順番に見ていく DFS を書いた。それしか知らない。だけどどうしても入力に左右されるし、あるパネルに影響する複数のスイッチの組み合わせを総当たりするので遅くなる。
入力を前処理して扱いやすくするとは考えてもみなかった。
でまあ今後は、こういうことができると知ったはいいけど、これが適用できる対象の性質を見抜けるかどうか、というところに課題が移ったとみていいのではないかな。簡単ではないね。
具体的な実装にも迷いがある。スイッチ数(N)、パネル数(M)の上限が 300 なのに対して 550 ms の時間をかけている。メインループだけど、
ステップ1の処理が N で、ステップ2の処理が N×M。もっとシュッと片付くうまい書き方があっても良さそうでは?
hai!@magrofly「典型057 Ruby https://t.co/Dri1yBSvuK」 / Twitter
「基底を求める」のはよくわからないけど、「解く」過程では順番に適用していくだけでいいようだ。さっきの手順に当てはめると、基底を求めるのがステップ2に相当して、解く部分がステップ3。ステップ1の検索が不要であると。
最初の提出。無駄にソート、検索している(9行目)。
N,M = gets.split.map(&:to_i) A = N.times.map{ gets next gets.split.map{_1.to_i-1} } S = gets.split.map(&:to_i) n,T = 0,[0]*M while as = A.sort_by!(&:first).shift i = as[0] A.map!{|bs| i < bs[0] ? bs : ((as|bs)-(as&bs)).sort }.reject!{|bs| bs.empty? && n+=1 } as.each{|i| T[i] = 1-T[i] } if S[i]!=T[i] end p(S==T ? 2.pow(n,998244353) : 0)
ツイートを参考に改良したが、あまり良くはならない。N,M<=300 だから? 対称差を求める部分(15行目)が富豪的過ぎるから? 前処理の部分でまだちょっと変なやり方をしてる?
N,M = gets.split.map(&:to_i) A = N.times.map{ gets next gets.split.map{_1.to_i-1} } S = gets.split.map(&:to_i) N.times{|i| next if A[i].empty? (i+1...N).each{|j| next if A[j].empty? if A[j][0] < A[i][0] A[i],A[j] = A[j],A[i] elsif A[j][0] == A[i][0] A[j] = ((A[i]|A[j])-(A[i]&A[j])).sort end } } A.reject!(&:empty?) O = N-A.size T,as = [0]*M,nil as.each{|i| T[i] = 1-T[i] } if S[as[0]]!=T[as[0]] while as = A.shift p(S==T ? 2.pow(O,998244353) : 0)
最終更新: 2021-06-24T16:08+0900
★2つだからってこういう素直なスクリプトを投げた。
N,P,Q,*A = $<.read.split.map(&:to_i) p A.combination(5).count{|a5| Q == a5.inject{_1*_2%P} }
結果は TLE×21/AC×4。なんだよ全然ダメじゃんかよ。解説画像はこちら>https://github.com/E869120/kyopro_educational_90/blob/main/editorial/055.jpg。Ruby では解説が役に立たない。★6~7相当のより高速な解法があるというから、そちらを解説してもらわねば。
ABC192-F Potion (20210224p01) を思い出しながら悪あがきをした。
N,P,Q,*A = $<.read.split.map(&:to_i) q = Hash.new 0 S = A.map{|a| q[a%P]+=1; q.dup } (N-1).downto(N-4){|k| S.pop # S.size==k q = Hash.new 0 S.map!.with_index(N-k){|q0,i| a = A[i] q0.each{ q[_1*a%P]+=_2 } next q.dup } } p q[Q]
これで TLE×6/AC×19。0≤Q<P≤10^9
という制約がえぐすぎるんよな。大きすぎるし、素数じゃないし。いったいどういう手が打てるのか。
N が 100 以下というところで何かできないかと考えた。3..100 のある i より左から A×A のペアを作って記録し、また 1..98 のある i より右から A×A のペアを作って記録し、3..98 の 96 通りのある A_i に関して i の左右から条件を満たすペアを引っぱって来られないかと考えた。P で割った余りは 10^9 通りになりうるけど、A×A%P が取り得る値は 10000 通りより少ないから、左側のペア×A_i を総当たりしても高々 1000000 通りであり、これは許される。あとは右側のペアが定数時間で選べれば良い。ここでモジュラ逆数が使えるような、使いたいような気がするんだけど、法が素数でないから「a が m と互いに素でなければならず、さもなくば逆元 x は存在しない。」と Wikipedia に書かれていて困っている。A×A×A%P の値が決まっているとき、A×A%P の値が何であれば A×A×A×A×A%P==Q となるか。たぶん1つに定まらないのではないかと思う。ここで詰まった。
Ruby を使う良さってこういうところだよね。ぬるい解法が許されないところ(許されるとそれ以上考えないので)。「Ruby ならではのお楽しみポイント」「この前の日記(20200907p01)で散々 TLE に苦しめられた問題も、C++ なら変数 r を map に、変数 nmin を multiset にすることで、ある範囲のキーを二分探索で検索することも、小さい方からキーを取り出すことも、STL 任せで妥当な時間で行える。適当に速くて短い提出を選んだけどこんな感じ>「提出 #16578878」。トリックは必要ない。」 そこは問題の本質ではないからかかずらいたくないという考えもあるだろうけど、自分が一番楽しめるのはここ。
Ruby で 365 ms の解答が共有されているなあ。見たい、見たくない、見たくない。
典型 90-005 の解説から>「mod の値が 998244353 のような特殊な値ではないので一工夫必要ですが、それでもたとえば 3 種類くらいmod を用意して中国剰余定理で復元する方法などで上手くいきます」。中国剰余定理は Wikipedia やブログを読む機会が何度かあったけど、まだ早い、と理解をあきらめている状態。
最終更新: 2021-06-08T14:11+0900
競プロ典型 90 問の 043 - Maze Challenge with Lack of Sleep(★4)が普通に解けたので、Pond Skater (青diff1968)も解けるような気がした(既視感>20200826p01。根拠は必要ない)。
All AC になるまで死ぬほどバグらせた。これが時間内に解ける未来が見えない。訪問済みフラグが二値ではない。ループの継続条件とキューへの追加条件が同じではない。ループのあいだ無条件にキューに追加していると rand_20_01.txt と rand_20_03.txt の2つのケースでキューが複製要素で膨れ上がってしまう。それ以外のケースでは AC なのに。
無駄に難しくしてる? そうであってほしい。
比較すると短いし速いし省メモリだと思う。テストケースハック(※テストケースは公開されている)らしき提出を除けば。
訪問済みフラグを水平方向と垂直方向に分けたのが間違いだったろうか。途中からフラグ(二値)ではなくなったし、紆余曲折を経て扱いが同列だし、単にコストとして再定義して書き直せるのでは?
Vh と Vv だったものを C に統合した。さらに短くさらに速くさらに省メモリになった。最初にコストでなく2つのフラグを使って書き始めたのが間違いの始まりだったのだなあ(Maze Challenge with Lack of Sleep(★4)の解き方がそうだったからなんだけど)。
最終更新: 2021-06-08T14:41+0900
解けてしまった問題とまるで歯が立たなかった問題は、日記にならなくて困るという点で共通する。E 問題で Ruby の AC 提出をざっと見たら他の人の解法が異なっていたのでそれを。
ちなみに F 問題の提出(Ruby)はなかった。ゼロだった。まずね、凸包がよくない。それは自分の人生とは一切関わることのない専門用語(だということにしてこれまで無視してきたもの)なので。
話は E 問題。こちらはやること自体は明白で、読めばわかる。木が与えられて、深さを調べるのも、親を辿るのも、基本操作だ。この問題の問題は制約にあって、ノード数もクエリの数も、上限が20万だというところ。定数時間でクエリに答えたい。
実装しなかったのでこれがアリなのかわからない。子ノードのうち底が深くないものから深い方へ、順番に足し合わせていくとどうなるか。
一直線の木で Σk 個の要素を処理するような実装でなければアリなのか。
これはマージしないでいいように1つの深さ記録配列を共有する実装。ただしノードごとに Array#dup した配列をスナップショットとして持っている。配列の要素数の合計がギガ単位になるけどどうかな、と期待して出したけど全然ダメだった。AtCoder の入力マジえげつない。上限は飾りじゃないのよ。
基本形はさっきのと同じ。クエリを先読みして必要なものだけ記録するようにした。TLE から 22 分経過していてコンテスト終了まで残り 10 分。あぶなかった。
これは他の人の AC 提出を読んで知った方法。二分探索を使うらしい。オイラーツアー?
確かなことはこのあたりの提出を読めばわかる。(提出の早い順)
メモリ消費にけっこう差があって、自分の提出は最も多い部類。正直どこが原因なのかわからない。
最終更新: 2021-04-30T21:11+0900
「10分間3問早解き」回だったわけなので4問目(D 問題)は時間中に解けなかった。90 分使っても解けなかった。辺がないケースで TLE が避けられなかった。
AC のきっかけはこのツイート。
chokudai(高橋 直大)ナス@chokudai
D問題、非連結ですって言うだけでDiff400くらい落ちそう
chokudai(高橋 直大)ナス@chokudai
非連結ですって言っても落ちないわ(サンプルにあるし)
連結で出題しないと400落ちなさそう。
「連結で出題しないと落ちない
」がよくわからないけど、ともかく、非連結なら問題を分割できるじゃん、と気がつきました。ツイートを読んで初めて気がつきました。
サンプル4つのうち2つが辺がゼロのケースだったんだけど、極端すぎてそれが全体としては非連結なグラフであること、個々の頂点としては最も簡単な連結成分を構成している、ということがわかりませんでした。そんなことってある?
テストケースが公開されていないので、提出前のテストには一直線の木を使っていた。連結成分の数を増やしても、辺の数を増やしても、探索を助ける制約が増えるだけだと思ったので。
しかしまだ TLE。どういう木が嫌か考えると、この提出は次のような入力に弱い。
だけど次のように番号の付け方を変えただけで問題が問題でなくなる。
たぶん頂点を次数でソートしてから DFS をすれば緩和されたと思う(が、AC 提出では違う方法(先読み)をとった)。
ソートするにせよ先読みするにせよ、2番目の問題に対処するには非連結なグラフを連結成分に分割する必要があったが、それをしないまま DFS の処理量を削減する方法を考えていた、というのが失敗に終わったコンテスト中の時間の使い方だった。
DFS の処理順を次数の降順にしたら悪くなりにくいのか、343 ms からタイムが大いに改善した。このムラが DFS の沼であり抜け出せない楽しみなんだよなあ>20201101p02、20201107p01.05。
最終更新: 2021-06-08T15:01+0900
昨日あった ABC。D 問題は覆面算。たまたま何か月か前に「FDCAJH × IBCFEH = FBAECIIJEGIH」というのを解く機会があったのだけど、時間制限がないせいで雑に総当たりをして済ませてしまっていた。
本番中は TLE で終わってしまった。E 問題を 15 分で片付けて戻ってきたけど、ついに通せなかった。
制限時間が大盤振る舞いの5秒なんだよね。
桁を1つ2つ減らすだけで時間がだいぶ違うだろうという予測はできたけど、減らし方がわからなかった。なんといっても目の前に文字で書かれた式があるわけではなく、色々なケースが入力されるわけなので。
TODO: Array#all? の中のテストは l<=r
より l==r||l+1==r
の方が厳しくて良い。
TODO: 和の先頭の桁が1だとすぐにわかる場合がある。
TODO: 列挙してから弾くより列挙しない方がいい。(確定桁が1つあったとして、未確定桁(=文字種-1)の順列の数だけ弾くのは手間だから)
TODO: ループの中の処理がシンプルになるように入念に事前準備をした方がいい。
現在 Ruby での AC 提出は 20。実行時間が 109 ms から 4845 ms までと幅広い。中央値は3秒台です。
たとえば(4桁 ms では最も速い)約 1.6 秒のこの提出>#21688714
先頭桁が0のケースを弾くと同時に、末尾の桁が一致するかどうかだけ特別にチェックしている。一致しないケースでは文字式の全体を数値化する無駄がスキップできる。このひと手間が効果的なのだと思う。
それと、全くの想定外だったのだけど、文字が 11 種類以上使われている式が入力されるケースがあったのだろうか(上の 1.6 秒の提出がチェックして UNSOLVABLE を出力している)。AtCoder の問題は入力や条件がきれいに整理されていて枝葉の手間が省けるように作られているだろう、という甘えがあるのは否めない。
3つある3桁 ms の提出が何をやっているのかは、さっぱりわかりません。
4つの TODO を意識して書いたけど、妥協した部分もある。
とはいえ、これを深さ優先探索で妥協なく書き換えただけで2桁 ms になる? そう、Ruby で現在最速の提出は 71 ms になっている。
根本的なところで、列挙してから弾くか、可能性のある組み合わせだけを列挙するかという違いがあるのかな。そっち方面で書こうとしたときは、ある桁を見たときに未確定文字が0なのか、1個あるのか2個か3個か、未確定文字があるのはどの項か、繰り上がりはあるのか、ということを考えるのが面倒くさくなって(=脳のキャパシティをオーバーして)、書けなかったんだよね。
書けなかったのをがんばって書いた。時間は申し分ないけども1つの WA。たぶん答えがないケースだと思うんだけど……。
WA の原因は非ゼロチェックが1つ抜けていたこと。それと、想定外だと書いた「文字が 11 種類以上使われているケース」はサンプル4がそうだった。コピペするだけで全然読んでいない。
雑に総当たりしていたのを反省して(TLE は嫌だ!)、数か月ぶりに書き直した。提出 #21743445 をベースにして、掛け算に対応させた。prd の計算が難しかったのですよ。ありえたかもしれないもうひとつの筆算のかたち。すっごく縦長になるけども。
A = 'FDCAJH'.bytes.to_a # to_a is for Ruby 1.8/1.9 B = 'IBCFEH'.bytes.to_a P = 'FBAECIIJEGIH'.bytes.to_a C2D = [nil]*91 D2C = [nil]*10 NZ = [-1]*91; NZ[A[0]] = NZ[B[0]] = NZ[P[0]] = 0 F = lambda{|i,carry,aa,bb,zz| next carry<1 if i<-P.size a = (c = A[i]) ? C2D[c] : 0 b = (c = B[i]) ? C2D[c] : 0 if a next D2C.each_with_index.any?{|e,d| next if e || d==NZ[c] C2D[c],D2C[d] = d,c next F[i,carry,aa,bb,zz] || C2D[c] = D2C[d] = nil } unless b prd = a*bb+b*aa+a*b*zz+carry if p = C2D[c=P[i]] next p==prd%10 && F[i-1,prd/10,a*zz+aa,b*zz+bb,zz*10] else p = prd%10 next if D2C[p] || p==NZ[c] C2D[c],D2C[p] = p,c next F[i-1,prd/10,a*zz+aa,b*zz+bb,zz*10] || C2D[c] = D2C[p] = nil end } raise unless F[-1,0,0,0,1] puts [A,B,P].map{|a|'%*d'%[P.size,a.inject(0){|b,c| b*10+C2D[c] }]}
最終更新: 2021-06-02T21:11+0900
緑がほぼ埋まってきて残っているのは解けなかった問題ばかり。そこで水色下位に手を出すも下位とはいえ水色はぱっぱっと解ける雰囲気ではない。あれもこれも行列の問題で、問題のその操作で何ができるのかさっぱりわからない。
だから青色。難しかったん。1年くらい前に ABC004 を埋めようとしたときは力が及ばず C 問題までしか提出に至っていなかった。
今回も一発 AC とはいかなかった。原因はすぐに推測できて、緑色が原点から離れない想定が誤っていたのだと思った。
たとえば赤か青の片方が極端に多いとき、外側に広がっていくよりも中心にある緑色の全体を移動させてでも中心に向けて移動する方が低コストになる分岐点がある。
しかしそれを想定するとコードにするのがさらに難しくなりそうで困った。
ちなみにこの提出の方針は……。赤と青をそれぞれ -100 と 100 を中心にして原点の左右で平らに並べる。原点は超えない。数が多ければ外側により大きく広がる。そのあとで緑色を原点を中心として配置していく。左右のコストを比較して赤と青を押しのけながら。
提出に至らなかった1年前の方針は、RGB の数から重心を求めて云々という感じ。ひょっとすると緑の配置拠点を原点に限らず適切に移動することで、WA だった方針のまま AC に持って行けた可能性が?
「J - 長い長い文字列」(提出 #19035422) とか、「K - 転倒数」(提出 #18029328)とか、脳みそに余裕がなくなるとクラスや日本語変数がソースに現れる傾向があるみたい。今回は両方出てきた。(クラスのメソッドの並びが不揃いなのが気になる。左を先に書くで統一しておきたかった)
イメージとしてはビー玉をざらざらと流し込んでから、抵抗の強弱を感じ取りつつ右に左に均す感じ。最大で900個程度の広がりしか考えなくていいからなんとかなっている。
Ruby の他の提出を見るとゴルフをしていなくても 300 バイト台の短い提出がいくつもあるし、内容も、候補を並べて最小値を選ぶ、二分探索で解(極小値)を探すなど、特に大層な道具は必要としていない。それは、頭の中で十分に理解して整理できているから書けるんだよなあ。
できないからソースコード上でメソッドと複数のインスタンスに分割して整理しています。結果としてひと味違った解法になったと思う。
たぶん抗力の計算が間違ってるんだよね。
押した力を上限として0以上それ以下の力しか発生しないはずだけど、なんだか負の抗力によって隣の障害物に引っぱられていきそうになってる。それだと引っぱってる方はともかく引っぱられる方は、必ずしも安定した、低いエネルギー状態にあるとはいえなくなる。
これが問題にならない理由もわかるけど、それはクラスの外部、インスタンスの利用方法にあるのであって、クラスの、メソッドの定義としては間違っている。
最終更新: 2021-06-08T15:27+0900
ABC の4問目で 400 点問題。しかし青diffではある。
時間制限を 10 秒にしてくれたらたぶん通る。しかし実際の制限は2秒であり、3秒ですらない。慈悲はないのか。
Ruby の提出一覧を見ると AC していても軒並み1秒越えであり、処理量がしんどい問題なのは間違いないのだけど、その中にあって1秒を切っている提出もある。ということは、己の考えが足りないのである。ぐぬぬ。
入力を正負ゼロに分けて、正負ゼロの積がそれぞれいくつ作られるかをまず求めた。
負の積が K 個かそれより多いならば、正の数と負の数のペアを考える。ゼロは特に考えることがない。K 番目が正の積の中に含まれているなら、負の数同士のペアと正の数同士のペアを考える。
これで考えるべき組み合わせが多少は減ったつもりになるが、入力次第では何の足しにもならない。本質的に計算量を削減する方法がわからなかった。
それでどうしているか。
K 番目の数を -10^{18} から 10^{18} の範囲で二分探索している。
ペアを、ある数とそれに掛け合わせるソート数列として持っている。K 番目の数の候補となる数が与えられたとき、その数以下の積がいくつ作られるかは、これまたソート数列を二分探索することでわかる。
ペアの数が馬鹿にならない。N (≦2×10^5) のオーダーで存在する。だから「ある数」と「ソート数列」に注目して、ペアをソートされた状態で持っている。そうすると K 番目の数の候補となる数が与えられたとき、かすりもしないペアを予め除外して考えることができる。かすらないとは2通りあって、すべての積がある数以下となるか、すべての積がある数より大きくなるか。全か無か。ここで累積和と、三度目になる二分探索を使っている。
とまあ、こんな感じ。(3つだが三重ではない)二重の二分探索のあいだに、範囲を絞っているとはいえちまちまと順番に数え上げる線形時間の処理が挟まっているのがいただけない。一番重たいケースで 10 秒はがんばった方だと思うよ。知的方面でのがんばりではないけども。
ソート列とソート列の組み合わせでペアを作っているのに、そのときに一方のソート列をばらばらにしてしまっているのが悪いのか? (短い方を選んでバラすようにはしている)
この回 は「C 問題が解けなくて大爆死した回の ABC」。その後 C 問題を解いて、F 問題も解いたけど、「F 問題が解けたら D と E も解けたつもりでいいんじゃないかな?」と書いたように、F の後でも D と E が解けていなかった。不思議なもので、D 問題は緑埋めをしていた先月に普通に解いていた(提出 #21267825)。緑がほぼ埋まってきて次なるターゲットは水色下位に移ってきている。E 問題 Logs である。解けない緑より解ける水色なのである。
えー、解けました。解けなかったときは何を考えて行き詰まっていたか。
今日の日記のタイトルは「D 問題 Pairs」です。関連は?
これまで二分探索といえばソート済み配列から特定の閾値をまたぐ値を選び出すのに使用してきたのだけど、実はそれだけではなかった。何もない空中から特定の値(解)を見つけ出すのにも利用できるのだった。順序さえ与えられるなら、解が -10^{18} から 10^{18} の範囲に存在すると判っているなら、たったひとつの意味のある値(解)を二分探索してもいいのである。
という気付きが Pairs を解く過程で(まだ解けてないけど)得られていたので、今度はごく素直に、解を決め打ってから最適な切断をすると切断回数の合計が何回になるかという逆算的な解法を発想することができた。そういうことができるとわかっていた。
二分探索を使った解法でかつて最も衝撃を受けたのは Vacant Seat というインタラクティブ問題に対する提出 #2057817 と #2064531 だった。bsearch メソッドから呼び出されるブロックの中でクエリを行っている。いやね、自分も提出 #7970588 の中で二分探索を使って答えを出してるんだけど、そのことと、対象となる具体的なソート列がないまま空中で二分探索を行う、順序はクエリで動的に決定するということの間に、どれだけの隔たりがあることか。
脳みそが不自由だと存在しない制約で思考が枷をはめられてしまうのだなあ。最も基本的なツールといえる二分探索も、まだまだ使いこなせていないのだった。
ところで 350 ms は Ruby で2番目に速い提出なのだけど、どんぐりの背比べである2番目とそれ以降から頭ひとつ抜けて速いのがこの 提出 #15632506 (sushibon さん / 219 ms)。二分探索は行っていない(ソートはしている)。
二分探索というのは人間が考えることを放棄して機械に試行錯誤させる解法なのだけど、人間が頭を使えば無駄なく速く答えを求めることができるのですね。まあ、何をどう考えればいいのかわかりませんけども。
これも空中二分探索。解を決め打ってから考える。もはやおなじみである。
Ruby では唯一3桁 ms に入った(他は4桁)。log1つ分の差だと思う。Nlog^2 と Nlog。単にソートする方のやり方を思いつかなかっただけなんだけど。
同じ青diffでもこちらのほうが Pairs よりわずかに難しいことになっている。
しかしこれは簡単な Pairs ということでいいんではないか? だって同じように二重の二分探索の真ん中で線形時間の足し合わせを行っていて、TLE にならないんだもん。
概ね 300 ms から 500 ms の間におさまっているから、自分の 1489 ms は最も遅い部類に入る。Pairs を解くヒントが(Pairs の提出一覧はもちろん)ここにもあるのでは?(だったら読むわけにはいかない)
ループの構成は変わらないまま脳筋的努力を重ねた結果、倍近く速くなった。しかし 300 ms にも 500 ms にも及ばない。やっぱり計算量のオーダーを減らす手がどこかにあるのだろう。それがわかれば Pairs が AC できるぞっ。
やったど。246 ms は Ruby では僅差で一番速い。
どこでオーダーが改善できるか。解法の根幹をなす大外の二分探索の log は欠かせない。入力をなめる N もなくせない。なら内部の二分探索を削るしかないのはわかってたんだけど、「log を削らなければいけません」「はい、削りました」ができるなら脳みそはいらないわけで……。
ヒントはこの問題の前に解いた射撃王にあった。log ひとつの差ってちょっとした違いなんですよ。ちょっと見る角度を変えるだけ……でなんとかなるなら脳みそは(略)
実際のところ、二分探索の代わりに shift/pop を繰り返すようにしただけ。
261 ms の提出を読んだ。A 数列の値から添字を得る逆引きインデックスを事前に作成するのがキモであるようだった。A の値の範囲は 10000 以下なので、それが配列のサイズとなったところで大した大きさではない。
言われてみれば、そうだね、という感じ(だけど思いつかなかった)。313 ms の提出も 328 ms の提出も 329 ms の提出も、同じ下拵えをしていた。
やったど! たまたまぶつかった別の問題ばっかり3問片付けてきたけど、とうとう本丸の Pairs をクリアしたぞ! (提出日時を見ればわかるけど、今日は5月の下旬なのだ。日記とは?)
これもやっぱり Handshake と同じように二分探索の代わりに shift/pop を繰り返すようにした。Pairs は Handshake と違って A 数列の値の上限が 10^9 なので、逆引きインデックスを用意しておく方法は使えなかったのではないかと思う。
ところで、ぎりぎり3桁 ms には入ったけど、759 ms には負けました。配列の操作でなく添字の操作をしているところが効いてるのかな?
最終更新: 2021-03-24T16:47+0900
解いたあとで他の人の Ruby での解答を見たらバリエーションがいくつか見られた。
これが一番多かったと思う。公式解説に書かれている通りの手順。
これは Ruby で最速の qib さんの提出 #20369253 (191 ms) の解法。
公式解説にはこう書かれている。
マス i と j の距離を d(i,j) として,マス i の色は d(1,i) ≦ d(N,i) ならば黒,そうでなければ白となる.結論としてマス 1 とマス N の 2 点から幅優先探索や深さ優先探索などを行うことで O(N) でこの問題を解くことが可能である.
解法1はたしかに解説通りの手順ではあるが、解答にあたり具体的な距離まで知りたいわけではなく、距離の大小関係だけ知れれば十分なのだ。
解法2の手順は(スタート地点からの距離を測定する)幅優先探索に則っているのだが、一見すると1手につき1マスしか塗れないゲームのルールに反しているように見えるのが難しい。同じことは解法1にも言えて、「マス i の色は d(1,i) ≦ d(N,i) ならば黒,そうでなければ白となる
」が納得できるかどうかに尽きるのだけど、解法2の手順がなまじゲームに似ているせいで考えてしまう。
フェネックとすぬけくんの行動原理として想定したのは公式解説のものと同じ。見立てだけが異なる。どういう見立てだったか。
フェネック(すぬけくんでもいいが便宜上フェネックを選ぶ)のスタート地点を木の根と定めて、すぬけくんのスタート地点の深さを知る。すぬけくんは移動可能範囲を広げるために根に向かって移動する。フェネックはすぬけくんの移動可能範囲を狭めるためにすぬけくんに向かって移動する。出会うのは中間の深さ。すぬけくんは根に向かって移動できなくなった地点を根としてその子孫ノードだけを塗ることができる(だから一直線に根(フェネックのスタート地点)を目指していた)。
結局のところこの問題は一本の辺を見つけ出す問題だった。頂点集合をフェネック側、すぬけくん側に分ける辺がどれかを見つける問題だった。
その手順として幅優先探索(解法1)とその応用(解法2)と深さ優先探索(解法3)とダイクストラ法(未紹介)と、いろいろな方法があって、実行速度の差があった。同じ線形時間でも1回なめるだけで済ませられるのか、2回か、3回か。
今日@2021-03-23 たまたま取り組んだこの問題が同じ方針で解けそうだった。
2地点から深さ優先探索で陣取りをしていって、中央付近でにらみ合って、それからどれだけ相手陣へ侵攻(自陣へ後退)できるかを数えれば答えになりそうだった。
きっちりと隙を見せない after_contest に撃ち落とされましたとさ。
競技プログラミングをするフレンズ @kyopro_friends
サーバル「ABC148F『Playing tag on tree』にafter_contestを追加したよ! 不等式に等号を入れるか入れないかを間違ってるコードが落ちるようになったはずだから確認してみてね」https://t.co/jcHP4lHFhg
不等号などなかった。先攻後攻を入れ替えたのと、自陣へ逃げ込もうとしてうっかり中立地帯へ迷い込まないように道を塞いだ。
当初方針のまま after_contest に対応したが、どうにも不自然に頑張ったようなコードになってしまった。この問題に関しては、想定解法通りに2通りの距離表を見比べて答えを選び出すのが良かっただろう。
ところで ABC148 はオンタイムで参加していた。A-D まで灰 diff で、E 問題に至ってもギリギリ緑という低難度回。F 問題でやっと水 diff 中位だったらしい。当時1時間を残していながら解けなかったのがこの F 問題。何を考えて解けなかったか。
木の上で追いかけっこをする2人がすれ違うことができない、ということが認識できていなかった。だから偶奇が適切な部分木を選んで逃げ込むことで追跡が躱せるような気がしていた。それじゃあこの但し書きが嘘になるのにね。「なお、ゲームは必ず終了することが証明できます。
」 そんなん考えたら青 diff 上位の「DFS Game」より難しくなるってのにね。
最終更新: 2021-03-15T22:56+0900
本日の ABC。1時間かけて ABCD の4完で、残り40分考えて E 問題が解けずに終わった。ゲーム問題苦手。勝ち筋とか必勝法とか、さっぱり見えない。「自分はこの、先攻後攻が決まった瞬間に勝ち負けが見えるゲームを、きっと楽しくプレイできるんだろうなあ。」
本番中に E 問題が行き詰まっている最中に F 問題をタイトルだけチラ見していた。Coprime の単語が見えた瞬間にあきらめた。別の問題だけど先々月に「Coprime はまた解けなかった。」 完全に苦手意識を持っている。素数とか見たくない。
割と大きめのサンプル3が通ったのでいけると思ったが TLE だった。
考えたことを順番に。
このとき(緑diff精進3問)解いた問題の1つが「ABC 115 D - Christmas」なんだけど、素直に問題の通りに書いた最初の版が明らかに TLE を免れなくて、ださいけど if を使って2回の再帰呼び出しを1回に節約するパスを追加することで AC になっていた。
倍倍ゲームになりうる再帰構造には特別な警戒が必要だということと、それが反転したときに改善効果が劇的だということを学んでいた。今回も最後の lambda F に2行追加して AC。
たぶんグループの作り方が間違っていた。二次ペア三次ペアと芋づる式に相互グループを作るのでなく、それぞれの数ごとに一次ペアのグループを作って、そのサイズでクラス分けをすれば、計算で答えが求まったのではないか。計算の材料にする数字が誤っていたから求まらなかったのではないか。いやでもそのクラスには公倍数の情報が抜けてるのか……。
組み合わせた結果をフィルタリングするよりも、フィルタリングした結果を組み合わせるべきだったのではないか。SQL がそうでしょう? JOIN する前に WHERE で絞るべきなんだ。WHERE に似ていても HAVING では遅いんだ。
全探索がダメでもある種の探索が許されていたあたり、今日の制約には優しさが感じられるなあ。
これに関連した @kyopro_friends さんのツイートを考えていた。
競技プログラミングをするフレンズ @kyopro_friends
アライグマ「F問題は、COLOCON2018C『すぬけそだて――ごはん――』の難しい版なのだ! gcd(x,y)=gcd(x-y,y)≦|x-y|だから、72以下の素数の倍数が重複しないようにすればよくて、どの素数の倍数をもう使ったかでbitDPすればいいのだ!」
「
」ってつまり……gcd(x,y)=gcd(x-y,y)≦|x-y|
というような発見があった。ものがよく見えていないと「新発見」が多い。ユークリッドの互除法まで見つけてしまった。開拓者か研究者に向いているのではないか。
最終更新: 2021-03-23T20:00+0900
解説を読んで ABC をコンプリートしようシリーズの1回目。ABC192 で残っているのは F 問題。いわゆるポーションって portion とはスペルが違ったのね。
2回目があるかはわからない。1回目にして解説を読んでから2日間苦しんだ。DP だったんだけど、人類が扱うには次元が高すぎるのではないかな? 自分には無理。
ソースコードの冒頭にも引用したけど、解説の要諦が次の一文。
dp[i][j][k] = i 番目までから j 個選んだときの和であって、mod C で k に等しいようなものの最大値
自分は最初これを次のように解釈した。
dp[i][j][k] = i 番目までから j 個選んだときの和であって、mod j で k に等しいようなものの最大値
微妙な違いがわかりますか? mod C と mod j の違い。うっかりミスではなく、理解できる範疇を超えていたから、これってこういう意味だよね、と一段次元が低い誤った理解しか生まれなかった。
引き回すデータ配列の構成を教えてもらってさえ遷移が書けるまで一日かかったんだけど、いざ完成したらこの微妙な勘違いのせいで時々答えが合わなかった。時々。答え合わせに使ったのは次のナイーブな解答スクリプト。N が 30 を超えると実行時間が現実的でないので生成する入力の N は小さめに。テストケースはまだ利用できない。
N,X,*A = $<.read.split.map(&:to_i) p (1..N).filter_map{|c| k = X%c m = A.combination(c).map(&:sum).sort.reverse_each.find{|m| m%c == k } (X-m)/c if m }.min
要するに、これを時間制限に収まるように書き直しましょう、という問題だった。それが難しい。
結局一度完成したと思ったスクリプトを囲うようにもうひとつループを重ねた。法が変わると余りは再利用できない。最初から目的地(C)を定めて j を変化させなければいけない。dp 配列の添字 k の上限は j でなく C である。無理だよ、明日にはもう自分でこの文が理解できないよ。
DP であることでナイーブな解答より有利になる点は次の2つ?
j+1 個の組み合わせを生成するのに j 個の組み合わせ結果が利用できる。
その際にキーとなるのが添字 i (「i 番目までから j 個選んだときの和であって
」)。j 個の組み合わせ結果を i (1~N-j)によって分類しておくことで、j+1 個の組み合わせを作るのに利用できる。
たぶんこれって DP のひとつの典型なんだと思うけど、配列の型を示されてさえこの種の遷移(何を残して何を再利用するか)を見つけるのに1日かかった。
見つけた遷移は具体的には、「j を C まで増やしながら、ある j について i 番目の要素(A[i])を i の大きい順に考える。A[i] を採用しないときに dp[j][i] に対応する C 要素の配列は dp[j][i+1] のものと同じ。A[i] を採用するときは dp[j-1][i+1] に記録された C 個の値と組み合わせる」 i と j が解説とは入れ替わってら。
dp[j][i] の値を作るのに dp[j][i+1] (最内ループの直前の値)と dp[j-1][i+1] (中間ループの直前の値の1要素)を再利用している。
勘違いして見えていなかったのは、j=C であり j を 1..N の範囲で変化させる過程で各 j(C) に対応した答えが見つかる……のではなく、C=1..N について j を 1..C の範囲で変化させなければいけないということ。
提出 #20486969 (TLE×11)
主にイテレータを使って書き直したので遅くなるのはわかる。
Array#min の代わりに Array#[] でダイレクトに最小値を取得するようにしたので、special_xx.txt 以外のケースでは改善している。
提出 #20486969 (TLE×11)
同じように Array#min を使うのをやめたのと、イテレータを使わず全て while で書いた。special_xx.txt 以外のケースで上よりさらに少し改善しているが、TLE は TLE。
Ruby って整数演算が足す引く剰余大小比較まで、どれも同じくらい遅い雰囲気。演算コストに差がないなら演算子の数を減らす方がいい?
でもどこに 800 ms も遅くなる要因があった? もう予測できない。
平均すると最初の提出より1割弱タイムが改善しているけど、意味のある差ではない。
ベースはイテレータメインの提出 #20486969 (TLE×11)。
AC と TLE の分かれ目は4重ループの最深部にあった。
初期値を正の無限大ではなく nil にした。
正の無限大は正常値として扱えるので記述が統一できるのだけど、むしろ異常値として nil や -1 や無限大を設定・検知して、ループをスキップするのが良かった。
ところで、想定上限を整数で表現しようとすると 67 か 68 ビットが必要になる気がして採用できなかった。Float::INFINITY と Bignum の、どちらがいいともいえない。打ち切り条件が ×C ではなく ÷C である理由でもある。
k = m%c
や k-=c if c<=k
よりも、「実行されないコードが最速」なのだった。負の添字を使った配列参照は組み込まれた機能でありコストは支払い済みなので、使い倒さなければ損になる。いくつかの C について最小公倍数で余りをとれば、より外側のループで DP 配列が再利用できるのではないか。数列 A の偏りと C の組み合わせを調べれば、k が取り得る値が C 種類より少なくなるのを見抜けるのではないか。結局のところ、TLE の原因はおそらく X%C と A%C(の和) がまったくマッチしないせいで4重ループを最初から最後までフル回転させられるせいだと思うから。
「いくつかの C について最小公倍数で余りをとれば、より外側のループで DP 配列が再利用できるのではないか」を実装してみた。話を単純にするために C が偶数の時に j=C/2; i=0
の DP 配列を C=C/2 の DP 配列として再利用した。
たとえば N が上限の 100 のとき、51..100 は普通に DP をする。1..50 は再利用配列を使用して DP をしない。限界は次の2点。
ケース | X | X (素因数) | A に含まれる 9999999 の数 | 答えが見つかる C |
---|---|---|---|---|
special_01.txt | 52142908377193267 | 103×4703×107642319563 | 0 | 1 |
special_02.txt | 48620189947792921 | 131×2719×18713×7294453 | 1 | 2 |
special_03.txt | 702276810747319237 | 702276810747319237 | 2 | 3 |
special_04.txt | 651020109319638361 | 162011×231599×17350549 | 3 | 4 |
special_05.txt | 611688502818504841 | 82936769××7375359689 | 4 | 5 |
special_06.txt | 85741517196073082 | 2×11257×32587×116867599 | 5 | 6 |
special_07.txt | 794433313787770441 | 101×74910361×105001181 | 6 | 7 |
special_08.txt | 515779426304609041 | 101×5106726993114941 | 7 | 8 |
special_09.txt | 896297933758956951 | 3×22769×13121611749293 | 8 | 9 |
special_10.txt | 90842952249996662 | 2×24335153×1866496427 | 9 | 10 |
N はすべて 100。数列 A の要素はほとんどが 10000000 で、0から9個が 9999999 という構成。
special_xx.txt が入力する数列 A の中に値の種類は1から2個しかなかった。C 個選んだ和の余りがとる値は、限られた 9999999 がいくつ含まれるかでしか違いが出なかった。つまり1から10種類。それでも C が 1..N の範囲で変化するうちに余りの数字(k)自体は変化していくし、X%C も変化するんだけど、どうやったらぎりぎり最後までマッチングしないような X が選べるんですか?
最終更新: 2021-04-06T17:58+0900
昨日あった ABC。今晩には ARC があるので復習が忙しい。
正規表現を乱用する問題だと決めつけて考えた。使える限り最善でなくてもかえって難しくなっても正規表現を使う。
パターンには改良の余地がある。たぶん /^([a-z][A-Z\n])+$/
で良かった。
$ は改行の前でも文字列の末尾でもマッチしたと思ったけど、フラグの影響がどう出るかが不確かだ。そして Ruby のフラグは JavaScript のフラグと比べてあべこべな雰囲気がしてわかりにくい。
入力が英大文字小文字だけだから大小の判別は1ビットを見るだけでいいんだけど、正規表現だから関係ない。
C 問題にしては……と疑いをもったが、テキトーに大きい桁を与えてもいけるみたいだったので問題の通りに関数 f を定義してシミュレーションした。本当はテキトーに大きいだけだとすぐに桁数の少ない値に収束してしまいかねなくて、そうではない嫌らしい値が与えられるかもという疑いがまだあったのだけど、とりあえず投げてみるスタイル。
最近誰かがツイートで Integer#digits メソッドに言及していたので初めて使ってみた。適所では? そういえば D 問題でも使っていた。
やってきました因縁の D 問題。前回の虐殺劇が記憶に新しい>20210206p01.02。今回も E 問題が緑色なのに対して D 問題が水色だったりして、正答数に逆転があったもよう。
あれ? やるだけ? という感想はあまりに素直。たしかに優秀な人は目をつぶっていても答えにたどり着けるのかもしれないが、凡人は周到に落とし穴を探し出さなければいけない。
それは1の位についてだけ当てはまらない。基数が3でも4でも5でも、数1は0より大きい1番目の数で変わらない。
そしてこれが、基数の種類を答える問題でないことの傍証になる(そういう誤読が多かったらしい)。無限大の答え方が問題文中にないからだ。
二分探索の下限に d+1 を、上限に M+1 を設定していたのだけど、M+1 の方が d+1 より小さいことがあるから、答えを導く引き算の結果が負の数になるケースがあった。
手で計算しているときは自然と自然数の範囲でものごとを考えてしまって例外ケースを無視してしまいがち。
早期に AC を得ていた複数の人が上限を定めない二分探索を行っていたようだ。kotatsugame さんがこの奇妙な二分探索の振る舞いについてツイートしていたので存在は知っていたが、自分で使えるほどには知らないし思い出さない。
7 WA のあとの AC。どちらの落とし穴もテキトーな入力を与えて出力を見るデバッグで発見した。1桁のケースはタイプするのも簡単だし、それでいて境界に近くてバグが潜みがち。嗅覚を働かせよ。
えびま @evima0
(D 実はもともと「9 1」っていうサンプルがあったんですが、出題の意義が 1/3 くらい消滅する (「1 9」だと 2/3 消滅) ので消してもらいました) https://t.co/NNcbAu6GjF
このツイートはもっともで、そうでなければ D 問題としては易しすぎて出題されなかったと思う。といってもこれだけいくつも罠があって目配りが要求されるなら、AGC の A 問題といった風情もある。
自分より上位の人は仮に D 問題の罠にはまったとしても、さくっと E 問題を片付けてから帰ってきて、結局 E も D も通してしまうというムーブができてしまう。(そもそも罠にはまらないか)それができるからこそそのレイティングなのだ。自分がそれをやろうとすると虻蜂取らずになるのが目に見えているので、1時間かけて D 問題を通しました。今回の成績はABCDの4完最遅レベルでレートは横ばい。
競技プログラミングをするフレンズ @kyopro_friends
フェネック「もともとD問題でXが1文字のケースを、アライさんは2個か3個しか用意してなくて、それだとWAのケース数でコーナーケースがバレそうだからたくさん増やすようにアドバイスしてみたんだけど、どうだったかな?」https://t.co/FxcvbhUJNL
AC が出るまでは WA の数を見て方針を疑ったり挫けそうになったりしたけど、1アイディアで 7 WA が 1 WA にまで減ったりしたから、まあこういうことなんだろうと予想はしていた。まんまと手のひらころころ。
プライオリティキューを書くだけの問題。まあその「書くだけ」ができなくて 2 WA するのが自分なのだけど。
……だと思ったら、Ruby で最も速い複数の提出が Hash を待ち行列に使っていた。keys.min で最小値を都度取り出す使い方で、それでいて速い。えええ?
あと、久しぶりにプライオリティキューを書いたから速度改善テクニックを1つ忘れていた。ヒープを整理するときに都度都度要素を交換しながら上昇(下降)するのでなくて、ローテーションする感じで、いくつかの要素を順番にスライドさせてできた空きに追加要素を置くのがいい。
最終更新: 2021-05-07T14:00+0900
先週末の ARC。ABの2完でレートは横ばい。ちょっと背伸びして C 問題が今やっと解けたので日記にする(べつに考え続けていたわけではなくて、オラクルが降ってくるのを待っていたのです)。
一応制約は掛け算していたんだけど、まずは素直に数えて確実に答えを……TLE。見れば一次 の k のΣなので機械的に変形して……AC。
間違った式の変形に5分以上の時間をかけてもしゃーないので、TLE は避けられない。今は(ARC の1問目に対しても)ステップを刻まなければ、答えにたどりつくことさえ覚束ない。
どれだけ1円を払っても数の種類は1しか増えないので、基本となる操作は何回2円を払って絶対値を変化させられるか。±B が境界として存在していて、|B| から 0 へ向かう変化と -|B| から負の無限大へ向かう変化が考えられる。反対側の値は1円余らせておくだけでいい。0 を挟んで -B から B の範囲を数えるのが面倒か。親切にも B=0 となるコーナーケースがサンプルのひとつになっている。もうひとつのコーナーケースが C=1
2 WA のあとの AC。±B と 0 と、それらで区切られた4つの区間を愚直に数えた。
翌日になって機械的に式を整理したもの。できれば if による分岐を消したかったのだが。
問題の見通しは難しくない。表のスコアと裏のスコアと、手番を渡すか否かのフラグ(子孫ノード数(=スコア計)の偶奇)があって、それらを葉からボトムアップで積み上げていけば先手、後手のスコアが即座に解る。
制約の 1≤
の解釈に一瞬詰まったけど、p_v
<vp_v
の上限が v であることで、逆向きにスキャンするだけで子から親へ順序よく処理できる親切設計だとわかった。
最後まで解らなかったのは青木君高橋君が採用する最適な行動がいかなるものであるか。二人が何を指標にしてどの子を選ぶのか、それが解らないでどうしてコーディングができる? 何をコードにする? 自分はこの、先攻後攻が決まった瞬間に勝ち負けが見えるゲームを、きっと楽しくプレイできるんだろうなあ。
odd.sort_by!{ _2-_1 }
と even.each(...)
がキモ。これが二人の戦術。
最後まで見えなかった even.each についてもう少し。
even は潜って戻ってきたときに手番が入れ替わらない子ノードを集めた配列。表のスコアが裏のスコアより高いものは手番(※広辞苑にはテツガイの見出ししかない。テバンは業界用語か?)を持っている方がさっさと潜って表のスコアを得てしまえばいい(裏のスコアは相手に渡る)。では裏のスコアの方が高いものは?
裏のスコアの方が高いものは、できることなら相手の手番で相手に選ばせたい。そうすれば表のスコア(低い)が相手のものに、裏のスコア(高い)が自分のものになる。それが可能になるのは、潜って戻ってきたときに相手に手番が渡る子ノード(odd 配列)が奇数個ある場合。手番というババの押し付け合いに勝てる。
Ruby の他の AC 提出(今のところ2つ)と比べて遅かったので出し直し。100 ms 縮んで遜色がなくなった。省メモリを目論んだが結果的に増えている。配列の配列の配列がよくない。
ところで、再帰呼び出しを行っている解答を手元で実行してみたらいくつかのケースで stack level too deep (SystemStackError)
が出て速度比較ができなかった。PC が貧弱なんだな(環境変数か? その解決法はドーピングぽい)。
最終更新: 2021-05-07T14:21+0900
今日の ABC の C と D はちょっと傾向が違ったよね(E と F は時間切れで読んでいない)。C はむしろ復古的かもしれないけど。
どこに着目すれば数えられるのか、わかりますか? わかりません。
テキトーに注目して数えて、(÷2では)ダメだとわかって(÷3で)やり直して、31 分かけて鬼の羅列である。
(構造の)理解に頭が必要ないという意味で、これも可読性に優れた読みやすいソースコードの例なんですよ。似たような例にすべての繰り返しを for ループで書くなんてありますね。for さえ解れば鬼に金棒、馬鹿の手にハンマー。目に入るすべては釘。打つべし打つべし。
だけどプログラムは構造化と抽象化を(必要である限り)繰り返して、人間はよりハイレベルな意味を読み取らなければいけないんです。あれをどーしたこーしたなんて作業手順を(人間に向けて)仰々しく並べ立てることに意味はありません。それはソースコードの役割であって、人間に向けたコメントには意味のあることを書いてください。
これって、黒のマスが1つの塊(ドーナツではない)であって、黒の内部に白のマスが島になっていることがない(逆に黒のマスはたった1つの島になっている)ってことだと読んだ。そのあとで補足的に「白に塗られた任意の 2 マスは、辺を共有するマスへの移動を繰り返し、白に塗られたマスのみを通って互いに到達可能である(マス目の一番外側のマスは全て白に塗られていることに注意してください)
」ともあるし、問題文は慎重に
書かれていたと思う。
多角形の解釈についても、色が塗られたグリッドであって座標空間上の点列ではないのだから、書かれていないものを見ようとして見るべき角が見えていなかっただけでしょう。
数学の言葉で書かれた制約の読解に普段苦労するので(20201122p01)、今回の問題文に文句はない。
すごくいい。そうか、ドット絵師と 3DCGモデラーがいただけなのか。
図形です。
制約が 10^5 だからどうかなーと思ったけど、普通に数えられる範囲だったみたい。手元ではサンプルに2秒以上かかってたんだけど、ジャッジサーバーは速かった。
1時間かけて、コンテスト終了1分前の提出。よかった……よかった……。
格子点を数える問題で、入力を小数で受け取るのはやっぱり怖い。小数点以下第4位までって書いてあるので、(文字列のまま) 10000 の下駄をはかせて、ついでに諸々の座標が正の範囲に収まるように平行移動した。負の数が混じると整数除算の丸め方向が期待と異なっていて面倒くさい。
# 0 を足すと答えが変わります。難しすぎるでしょ? -1/2 #=> -1 0-1/2 #=> 0 # 1 (イチ)が変数 l (エル)で、中身が正の数だったり負の数だったりすると、もう予測できないでしょ?
上記は Ruby の挙動。仲間はずれらしい>「整数同士の除算演算子の挙動 (C言語、C++、Scala、Java、Rust、Go言語、PHP、JavaScript、Perl、Python、Ruby、Elixir) - Qiita」 Python の整数除算(//
)も同じく負の無限大方向に丸められるとか。
他の人の Ruby での提出を見ると、入力を to_r するものが多かった。r は Rational の r. 使ったことがないと使えないし、思いつきもしないのだ。
二分探索の探索範囲をちまちま限定したところで、倍の違いが試行1回の違いにしかならないのだから、2つのループは1つで十分でしたね。これは円を4分割して数えられないか考えていたのが尾を引いている。
競技プログラミングをするフレンズ @kyopro_friends
アライグマ「D問題は、円の中の格子点のx座標としてありえる値の範囲がX-R~X+Rだから、x座標を決めたときの格子点の個数が求められればいいのだ! 誤差が大変だから整数で計算して、負の数の切り上げや切り捨ての計算に気をつけて……、罠がいっぱいあって大変なのだ」https://t.co/6z8erFU3Ym
実は二分探索がいらなかった>画像。三平方の定理! 中学生!
三平方の定理。速い! 短い!
Integer#sqrt なんてニッチなメソッドを使ってみた。
ところで、やっぱり **2
は遅いみたい。引き算を2回評価することになっても覆らないくらいに。
Ruby での提出を早い順に見てるんだけど、どの人もどの人も平方根をとって計算で格子点の数を求めていた。10行以下のスクリプトで。それが間違いなくすごいんだけど(だって開始後30分ぐらいでの無駄なく短い提出だ)、それらをことごとく撃墜した3つの入力(handmade_marginal_{01|03|05}.txt)が、今回はいい仕事をしていたなと。単に to_f を to_r にしたところで、三羽烏のひとつしか超えられないみたいですよ。
Rational だけでなく BigDecimal の存在も忘れていた。これは「任意の精度で 10 進表現された浮動小数点数を扱えます。
」 to_d の d は (big)decimal の d. to_f を to_d にしてもやっぱり3つのうち2つが WA になるようなのは、BigDecimal#sqrt を使わないで Math.sqrt を使っているのが良くないんでしょうか。Math モジュールは、標準とはいえ require が必要な添付ライブラリである BigDecimal を知らないのが普通だと思う。
提出して確かめようとしてわかった。BigDecimal#sqrt を使うとサンプル3で既に TLE が避けられない。
入力は Rational で受け取っている。Math.sqrt の結果を検算して条件を満たす限り±1の微調整を施し続けている。そして大事なことは、±1した結果の正当性も確かめている。
単純に±1するだけ、しっぱなし、では、handmade_marginal_{00|04}.txt に捕まってしまうようだ。
書き方を洗練させた結果がたぶんこの提出 #20009989 (kyoshida さん)。find メソッドと count に加算する前の nil チェック。
左右の点のうち1点が二分探索で見つかりました。左右の点の中間座標は円の中心に由来して明らかです。ではもう1点は? a1 = x*2 - a0
違いは1行だけ。Math.sqrt の結果を(floor ではなく)小数点以下第5位あたりで丸めていたらどちらも AC だったんだろうかダメです。
これが Integer.sqrt の実装らしい。
def isqrt(n): x, y = n, (n + 1) // 2 while y < x: x, y = y, (y + n // y) // 2 return x
Math.sqrt とは別に用意する価値があるからこそ存在しているのかな。ニッチとか言ってしまったが、こちらが使い所を知らないだけなのか。
自分は今回も Sqrt Inequqlity のとき(20200316p01)も、浮動小数点数を単純に嫌ったり怖がったりして難を逃れたけど、こういう風に限界を見極めて対応できるの、かっこいいよなあ。
B 問題の出力例はスペース区切りだけど、問題文は「A′ の要素を空白区切りで順に出力せよ。
」という表現になっている。
ここを参照すれば間違いないという定義があるわけではないけど、空白が white spaces の意味なら、ここに改行もタブも含まれると考えるのが普通(※要注意ワード)。自分は「スペース」(ASCII 0x20)と「空白文字」を使い分けているし、AtCoder にもそのように期待している。
というわけで、わざわざ .join(' ')
はしない>提出 #19962733。
ダメです handmade_marginal_{00|04}.txt に捕まってしまう。Math.sqrt のアルゴリズムに起因して誤差が蓄積するらしい?