sweet\nsweet
を検索していたのを修正して sweet\nsweet\ns
を検索するようにした。■B 問題「Grid Walk」。やります。B 問題にしては実装が重め。グリッドのサイズが小さくても実装量が減るわけではないんだよね、当たり前だけど。そこんとこ承知してくれているかな?■C 問題「Minimum Glutton」。C 問題で DP か? と一瞬身構えたけど、最小値を求めるということで、2通りの貪欲法を比較するだけ。大丈夫です、最大値を求める DP 問題は E にあります。■D 問題「K-th Nearest」。二分探索してくださいという問題にしか見えなくて他に方法が思いつかないんだけど、制限時間3秒のところ、(1割増しの 3.3 秒ではなく) 3.22 秒かかって TLE だったので、220 ms ほどの高速化が必要。どうするの?■E 問題「Maximum Glutton」。C 問題の難しい版。甘さとしょっぱさの組み合わせを状態のキーにはできないけど、甘さとしょっぱさのどちらかと個数を組み合わせてキーにすることはできる。甘さをキーの1つにしたら、しょっぱさを最小化する DP をする。これもサンプルが教えてくれたんだけど、A 問題と同じ罠があります。同じ罠に落ちかけました。■F 問題「Range Connect MST」。どういう風に辺を引くことになるのか、イメージがしづらい。木なので本数は N+Q-1 本だと決まっている。それを最大 N×Q の組み合わせからどう選ぶと全域木になるのか。あれこれ考えてようやく納得できたのは、i=1..Q において、Li..Ri のあいだに連結成分が g 個あるなら、g 本の辺を引くのだということ。両手の 5+5 本の指を使って考えると、それで N+Q-1 本の辺が選ばれるようだったのでそう思った。答えが合わなくて時間内に提出できなかったんだけど、原因がしょうもなくて、貼り付けた BIT のイニシャライザにある初期化コードが今回は不要だと思って削除したけど、削除してはいけなかったという、そういう理由で答えが合わなかった。たとえばヒープだと、ソート済みの配列を内部データにする場合、初期化の必要がない。ソート列はそのままでヒープの要件を満たしている。だけど BIT の内部データは違うんだなあ。解ける問題だったなあ。1から数年前の自分なら解いていたなあ。■自分のすべての提出。最近ユーザー名の横に表示されるへの字。ノイズではあるんだけど、水色でもまだ 1500 台を維持しているなという慰めにもなっているもよう。■精進。D 問題。Q のループの中で二分探索をする中で二分探索を2回行って TLE を出していた。二分探索の上限を指定せずに TLE×11。上限を指定して TLE×7。最も内側にある2個目の二分探索を省けるときは省くようにして TLE×1。最内の2個目の二分探索を完全に省いて AC。これが 1765 ms なんだけど、Ruby で 627 ms で解いている人がいるんだよね。気になるけどネタバレは嫌だ。■D 問題。別解。提出 #56088391 (AC / 325 Byte / 340 ms)。log 1つでできると読んだので、k 幅のウィンドウを二分探索で置いてみた。判定条件は、右端の要素が初めて左端の要素よりも b から遠くなる瞬間。そのひとつ手前では逆に、左端の要素が右端の要素より b から遠くなっている。この両者を比較する。二分探索の高速化っていうと尺取りが定番なんだけど、だから昨日はその方面で TLE 回避策を考えたりしてたんだけど、この D 問題はウィンドウの幅 k がクエリごとに可変だから、尺取りはうまくない。今日の提出では同じ二分探索を使っていてあまり違いを感じないんだけど、よく見れば二重の log が一重に減っている。log 1つの差ってたしかにこれくらい微妙なものではあった。Pairs を解いていたときに書いている。「log ひとつの差ってちょっとした違いなんですよ。ちょっと見る角度を変えるだけ」。提出したあとでもういちどさっき読んだブログを読み返すと、まんま自分の提出がやっていることが書いてある。「初めて左端のが遠くなった場所を見つけてその一つ前も(存在すれば)候補なので比較して」。なんかよくわからんなあと思いながら読んでいたけど、実際今も「自分より1離れたもの同士を比較」「長さが1短い区間を考えて」「初めて右のが遠くなったら左のを付け加えてそれがそのまま答え」とかよくわからないんだけど、必要なことは一度読んだだけで頭の中に入ってるんだな! 自分では思い出せないだけで。しじゅうく‐にち【四十九日】〘名〙 人の死後四九日目に当たる日。また、その日に行う法要。」とか書いてあって、これもうわかんねえな。いやホントはわかりますよ。「四十九日」「七七日」の2つと「四九日(目)」とでは使用時期にずれがある。辞典の言葉は収録される言葉よりも新しい。23 日を二三日と書くことの帰結として、10 月が一〇月になっていたのが先々週読んでいた小説。何を今更感しかないけども、そうなんだ、そりゃそうなるよな、だけど気持ち悪いな、その○はなんなんだ、漢字ではないよな、とひっかかってしまったのが今日の日記になっている。十月って書きたいよ。■ATOK で 10 を変換して出した一〇で使われているのは○ではなかった。U+3007 は「漢数字ゼロ IDEOGRAPHIC NUMBER ZERO」であり、U+25CB 「丸印,白丸 WHITE CIRCLE」とは異なる文字だった。〇が比較すると新しい字だけど数百年はさかのぼれるみたいなことが Wikipedia の漢数字のページに書いてある。お前漢数字なのか。漢数字とアラビア数字を区別して一方では位取り記数法を認めたくないっていうのは、単に好みや慣れや、旧弊に囚われているってだけなのか。自分はこれからも区別して用途を分けていきます。■用途を分ける(漢数字では位取り記数法を使わない)ことの別の一面は、「健康第1」とか「3位1体」とか「別の1面」とは書かないということ。算用数字を使うのは1、2、3と数を数える場面だけでいい。X と X+1 が可換な場合だけでいい。七七日 。
A-M$N-Z
と並べたなら、(A,N),(B,O),(C,P),...,(M,Z)
のペアを作る(のだと思う)。ローカリティに注目すればなんとなくそれでいいような気がするし、逆に両端からペアを作っていくのがダメなのもわかる気がするが、いつでも絶対それで OK とはわからない。■自分の解答の後半のステップはプライオリティキューを使わないで、単純に入れ子配列をフラットにして真ん中で切ってペアを作るので良さそう(C.flatten!; C[C.size/2..].zip(C)
)。どの解説もそういうペアの作り方をしてるので。どの頂点集合も過半数に届かないようにしているのだから、たしかにそれで同一集合からペアを作ることはないみたい。それと、DFS での頂点の並べ方は帰りがけ順でも OK と解説に書いてあった。Array#each_cons
がおそらく線形時間を要するせいで TLE を1回出した。たしか以前も同じメソッドで TLE を出している。学習しないな。だけども、配列の dup や shift、pop では配列の要素レベルのコピーが抑制されると知っているから、配列から固定サイズの部分配列を切り出すのに単位時間しかかからないと期待するのは当然じゃない? each_cons が Array#each に基づいて実装されてるとしても、一度の each_cons 呼び出し(と複数回のブロック実行)で線形時間しかかからないなら TLE にはならなかった。尺取りをするなら TLE にはならない。ブロック実行のたびに線形時間かかっていて、each_cons 全体では2乗の時間がかかっているからこそ TLE になる。これが予想外。■D 問題「Go Stone Puzzle」。制約がごく小さいのでメモして BFS をする。実装でいろいろミスをした。たとえば (1,2)、(3,4)、(5,6) みたいな (奇数番目,偶数番目) のペア単位でしか交換できないと思い込んでいたり。そこはちゃんとサンプルで落とされたけども、見るべき要素数が N ではなく N+2 だったりも。15 分かかった。■E 問題「Tree and Hamilton Path 2」。ARC179-D Portable Gate の簡単バージョン。精進記録はこちら>20240602。根を決めて DFS ですべての頂点を訪れてまた根に戻ることを考えると、すべての辺をきっちり2度ずつ通るのでコストの上限がまず決まる。本問題では根に戻る必要がないのでどれだけのコストが節約できるか。直径。自分の提出がすでに求まっている直径を再帰関数で改めて求めているのは、紆余曲折の結果なのです。引き算で求めるのではなく、2種類の再帰関数で積算して答えを求めようと迷走していたなごり。プライオリティキューを貼ったけど、いらなかったらしい。根からすべての頂点への距離を求めているのだし、各頂点へのパスは1つだけなのだから、それはそう。■F 問題「x = a^b」。苦手な苦手な数学問題。指数を3以上に決め打つなら N 以下の全ての立方数、4乗数、5乗数(って呼ぶのか知らないけども)……を列挙して重複を排除できる。じゃああとは最大で 10^9 個ある平方数をどうやって数えるかだけ。すでに数えた立方数、4乗数……が平方数でもあるかどうかを確かめて重複を除外することでクリアした。この問題の提出でも自分は変なことをしている。b を固定したときの a の最大値を Math.log で求めてからすべての a^b を列挙してるんだけど、それだったら a=1 から始めて N<a^b になるまで a+=1 を繰り返せばいい。コンテスト中はそこまで頭が回らないんだなあ。■G 問題「Go Territory」。解けてないよ。サンプルにあるように囲われている格子点を数えたい。縦横斜めに並んだ石のあいだで UnionFind をして、まずは輪っかを作っているかもしれないグループを特定した。次にグループを構成する石の周囲8マスから BFS を開始して孤立している格子点の島を数えようとした。どこで探索を打ち切るか。グループを構成する石の X 座標 Y 座標の最大最小を限度にして BFS を打ち切って孤立していないと判断をした。これでサンプルは合ったけど、WA があるかもそうだけど TLE は間違いないと思うんだよなあ。おおざっぱに2乗の範囲を塗りつぶそうとしてるわけだから。■コンテスト成績証。黄パフォに限りなく近いいいパフォが出たけども、前回(ABC が Unrated 判定だったので配点と Writer から必敗を覚悟して出た ARC)の緑パフォの傷が癒せただけなんだよなあ。F 問題まで4桁人数が解いてるので早解き回だったみたい。それは配点からも読み取れる。簡単な問題に滅法強いという自己評価を裏切らない結果。■G 問題。自分のやり方で WA になる例。ループが2つ左右にくっついていて、右のループの周辺ということで、右のループの外側、左のループから見れば内側から BFS を開始した場合、打ち切るタイミングを誤る。盤面を BFS のたびにクリアすれば解決するけど、ますますもって TLE が加速する。■■■精進。G 問題。提出 #55351016 (AC / 796 Byte / 571 ms)。kotatsugame さんの動画(【競技プログラミング】ABC361【実況】)で解法を聞いたんですよ。UnionFind をするのは石と石ではなく、縦もしくは横方向に連続した格子点列の隣り合ったもの同士だった。やってることは難しくなくて、付加情報として格子点数を持った UnionFind だけど、頂点番号が与えられているわけではないので、格子点列に自分で番号を割り振らないといけないひと手間がやっかい。Y 座標列をソートし忘れたり、ひと手間に色んなミスがからんでくる。グループの管理と格子点数の管理を同時にやろうとしてグループ0と数0を混同したりもした。また、格子点の総数が数えられなくて答えがなかなか合わなかった。X 座標の最大値が 200000 だとして、0 から 200000 のあいだには 200001 個の格子点があるんですよ(あたりまえ……ではなかったんだなあ)。3 2
という入力例がある。サンプル1のように解説がないし、出力例が実数ではなく 554580198 (mod 998244353)
だということで、途方に暮れていたのが昨日。だけどね、自分で計算すればいいんですよ。昨日の方針だけど、場合の数を K 回数え上げていって、最後に N^{2K} で割ることで確率に変換するようにしていた。ということは、3 2
という入力に対しては9通りの出目が2つ続く 81 通りの場合がうまく数えられていれば良かった。81 に足りなければ遷移の式が誤っているということ。そのようにして今日はサンプルの2が合った。そして次のサンプル3の入力が 4 4
。これの答えがまた合わなかったのだけど、同じようにして 4 2
に対する 256 通りの数え漏らしを見つけることでサンプルの3もまた合った。■提出 #55117837 (AC / 362 Byte / 126 ms)。N=1 で WA を出さなかったのはすでに他所で知っていたから。全く警戒していなかった。■この E 問題までささっと解けてまだ不満というのが自分の現状認識なんだけど、また脳みそにクモの巣が張ってきてるみたい。半年後からの挽回に期待!■続けて G 問題 Suitable Edit for LIS にも取り組んで提出したのだけど、#55121924 (WA×8/AC×45)、WA になるケースが全く作れなくてデバッグが進まない。C++ の AC 提出と答えを突き合わせたりしてるんだけど、最初にコピーしてきた提出は長さ6程度のシャッフルした順列に対する答えがおかしかったよ(それでも AC)。2つめにコピーしてきた C++ の AC 提出とは意見の一致を見たので、自分だけが盛大な勘違いをしているということはなさそう。こんなザルなジャッジだったら8個くらいの WA は見逃してくれてもええやろ。■G 問題について現在わかっていること(あるいは勘違いしていること)。操作によってできるのは LIS の長さを +1 することだけ。先頭か末尾の値を含まないで LIS が作れるなら、先頭か末尾の値を操作して +1 できる。では先頭と末尾の値が LIS に不可欠だったら? 適切な位置に LIS に参加しない空き要素があるなら +1 できる。適切とは? 空き要素があってもその前後にある LIS を構成する要素が連番だったら +1 できない。条件はこれだけだと思う。ところで LIS って候補がいくつもあるのが普通。最長の長さを知りたいだけならそれらを区別する必要がないけど、今回は空き要素を挟めるか挟めないかがポイントなので、LIS の個別の列に踏み込んでいかないといけない。添字列に対して LIS を求める操作をして作業配列を上書き更新せずに追記して履歴を残すようにしたらうまくできないかなーと思って書いたのがさっきの提出。なにがうまくないのかわかんない。思いつきでいけるやろって思う方がおかしいか。そうか。■■■通ったー!!! G 問題。提出 #55154669 (AC / 672 Byte / 277 ms)。前の提出が 671 Byte だった。17 行目の <
を <=
にすると WA が AC になった。狭義単調増加だから、値の比較にイコールがないのは一見自然なんだ。でも 17 行目では LIS に含まれない要素をはじいていた。<
の否定が >=
であるように、そこではイコールが必要だった。なんだー、1文字かー。ランダムケースの生成ルールを A = 1,*([2,3,4]*3).shuffle,5; puts A.size,A*' '
に変えたらすぐに見つかった。同値の要素の重複がキーだった。ともあれ、やろうとしたことがちゃんと AC に繋がっていたのは良かった。■LIS の履歴の活用法について書く。作業配列には通常長さ i の増加列の末尾の値を記録する。最終的に i が取り得る最大値が LIS の長さ。今回は値の位置も知りたいので値の代わりに添字を記録し、さらに履歴も知りたいので各長さ i それぞれに添字列を記録していった。入れ子配列の総要素数は N+(LISの長さ) になる。作業配列には最長に届かなかった短いものも含む全ての長さの増加列の情報が記録されている。これを後ろから見ていくことで LIS の一部ではない要素をはじいていくことができる。値を見ることで添字列の先頭から、添字を見ることで添字列の末尾から取り除いていくことができる。たぶんね。あとは前後の添字列を見比べて LIS に中間値を挿入する添字の空きと値の空きがあるかを確かめる。■■■遅れていたレート更新があったのだけど、B 問題で範囲が間違っていた普通の WA コードを投げた人が Unrated に不満を表明していた。テストケースの制約違反とは関係なく WA だった人も Unrated なの? 確かめたら自分も Unrated で草。4完低パフォだから不満はありませんけども。1≦c≦w という範囲を添字にするときに、c を 0 始まりにするのを忘れて各行1文字目を縦読みするケースが漏れていて WA を1回出したんだよね。最終更新: 2024-07-12T00:35+0900
いつもの前置き:提出先は AtCoder だけど問題は AtCoder の作成ではなく JOI で、だけど競プロカテゴリを意図して AtCoder タグを付けています。
しんどかった。BC 問題を読むのは気が向いたときにして、A 問題についてだけ書く。
4種類の論理演算(! & ^ |
)と、かっこと、ある値を境にして真偽が切り替わる関数から成る式の値について、クエリに答える問題。式の長さが 1M 文字以下で、クエリとして与えられる値の数が 200K 個以下。つまりどちらもとてもでかい。
クエリを昇順に並び替えたら、入力値との比較で真偽が決まる関数というのは、一度だけ false から true に値が切り替わる関数になる。更新頻度が (関数の数)×(クエリの数) から (関数の数) に減る。
以上の2つのアイデアというか願望に基づいて最初に書いたのが次の提出
関数を class で置き換えて、そのクラスに演算子を定義して、式全体の評価を eval で行うお手軽な実装。
eval であるか *.rb ファイルを実行しているかに関係なく、大きな式を評価しようとすると Ruby は黙って異常終了する。式の形によっては終了する前に nesting too deep (SyntaxError)
というエラーが出ることもある。p 1^1^1^1^1^...^1
という 1 か 0 を表示するスクリプトをコピペで膨らませて実行すると、Ruby のバージョンと Windows でのスタックサイズに左右されるんだろうけど、手元では 7 KiB を超える前に何も出力されなくなった。
両極端の例を挙げると、1+2+3+4+5+6+7
という式を [+ [+ [+ [+ [+ [+ 1 2] 3] 4] 5] 6] 7]
と解釈するとき、(1+(2+(3+(4+(5+(6+7))))))
という式を [+ 1 [+ 2 [+ 3 [+ 4 [+ 5 [+ 6 7]]]]]]
と解釈するとき、階層が直線的に増えていく。
中置記法とかっこの存在が式の扱いを難しくしている。
変換アルゴリズムがうまく書けなかったので、検索してトップに出てきた「逆ポーランド記法入門2|中置記法から逆ポーランド記法への変換」というページを参考にした。
うまく書けなかった部分、求めていた答えがまさに「演算子を全てpopして」と与えられていて喜んだのだけど、あとになって答えが合わない原因が「全て」にあることがわかった。上のページで扱っている四則演算のように、演算子の優先順位が1位と2位の2つだけなら「全て」で正しい。それより多いなら、「これから入れようとする演算子よりも優先順位が高い演算子を pop できる限り全て」が正しい。日本語の説明と Python の実装例に齟齬はなかったので、日本語だけの問題ではないみたい。その一部分を除けば文章だけでアルゴリズムの全体像がイメージできるわかりやすい説明ではあった。
これで RE は解消するけど TLE はなくならない。むしろ eval を呼び出してインタープリタに丸投げすることができないぶん遅くなる。
例えば [1]^[2]^[3]^[4]^[5]
という1つの式があって、クエリが 10 だったとする。最初は全ての関数が false で、クエリの昇順に true に切り替えていく。10 が最小のクエリなら、1 から 10 までの関数を順番に true に切り替えていくのだけど、その過程でこの式は false>true>false>true>false>true と値をチカチカ切り替えていく。もしこの式の全体が別の式の一部分であるなら、それら外側の式も同時に連鎖的に値を切り替えていくことになる。
更新の伝播を階層ごとに一旦ためておいて、深い階層から足並みを揃えて各項につき1度だけ値を更新しようとした。その過程で更新と更新が重なって更新が消えることも期待できる。ただまあ、これは根本的な解決策ではなく、効果がある入力もあるかもね、というレベルの対策にすぎなかった。TLE はなくならなかった。
演算子の優先順位を考慮して、| ^ &
の順番で、もっとも真ん中にある演算子で分割する。
たとえば [1]&[2]&[3]&[4]&[5]&[6]
という式なら [& ([1]&[2]&[3]) ([4]&[5]&[6])]
と分割する。
たとえば [1]&[2]&[3]&[4]|[5]|[6]
という式なら [| ([1]&[2]&[3]&[4]) ([5]|[6])]
と分割する。
ところでこの分割は同一階層内でしかできないので、[1]&([2]&[3]&[4]&[5]&[6])
の分割結果は [& ([1]) ([2]&[3]&[4]&[5]&[6])]
になる。かっこを無視して分割してしまうとあとでどうくっつけていいかわからないので。
かっこに制約されるということで、更新が影響する範囲を対数レベルに抑えて TLE を解消する助けにはならなかった。
なぜか抵抗があって考えないようにしていたのだけど、TLE 続きでなりふりかまっていられなくなったので、結合法則を使う。実は対策3でももう使っている。
「なぜか」っていうか、左結合だと問題に明記されていたから、それを無視するのはずるであると、理性ではなく感情が訴えるのだ。答えが合いさえすれば何をしてもいいという教育は受けていない。たぶんそういう理由。問題の解釈が曖昧にならないように出題者が左結合だと定義するのはわかる。交換して答えが変わらないのを見抜いて利用するのが解答者だというのもわかる。だけど無意識が抵抗している。まあいいや。
中置記法の式はかっこがあってややこしいので逆ポーランド記法の式を操作する。
たとえば 1+2+3+4+5+6
を逆ポーランド記法に書き換えると、1 2 + 3 + 4 + 5 + 6 +
になるのだけど、演算順序をいじって 1 2 3 4 5 6 + + + + +
に書き換えたい。
そうすると何が嬉しいか。演算子に由来する階層を取り去って [+ ([1]) ([2]) ([3]) ([4]) ([5]) ([6])]
と解釈することが容易になる。次にこれを6項演算として扱うこともできるし、2項演算のまま引数列を半分半分にしていくこともできる、半分半分にするということは、セグメント木のようにきれいで効率的な階層が作れるということ。
正しい方向に向かっていることを予感させるけど、1パスでやろうとして十分にマージできなかった。
たとえば次の式の書き換えを考える。1 2 + 3 + 4 5 6 + + +
。参照範囲が限定できるので予め連続する演算子を結合しておくと便利。1 2 + 3 + 4 5 6 +++
になった。左から読んでいって最初の + 演算子に出合うとき、右の要素が被演算子(3)でその次の要素が + 演算子なら、順番を入れ替えて 1 2 3 ++ 4 5 6 +++
にできる。しかしこれ以上の結合ができない。被演算子もまとめておくとひとつ飛ばすだけで +++
にアクセスできる? だけど 4
という項は 2 2 *
という演算子を含む3項として存在していることもありますね。
対策4とやりたいことは同じなんだけど、今度はうまくできた。演算子に行き当たったとき、スタックから取り出した2つの値(a, b)を即座に [op a b]
としてスタックに戻すのではなく、a
、b
が単独の値か何らかの演算の結果なのかを調べて可能ならマージして、できるだけ階層を増やさないようにした。そしてそれが最後でもなく、スタックから取り出されてまたマージされることもある。
これの 92 行目から 107 行目のあたり。
2K オーバーの長大なスクリプトになってしまった。絶対もっと短く賢く書けるはずでしょ。