sankantsuのブログ

技術メモ・競プロなど

ABC257 感想・反省

atcoder.jp

結果

atcoder.jp

ABCD4完(74:55 + 4ペナ)

  • Performance: 1175
  • Rate: 1293 -> 1280 (-13)

しょうもないWAが多く冴えない回だった

コード

github.com

各問題の振り返り

A

特定のパターンの文字列の X文字目を取り出す問題

文字列を実際につくらなくても,割り算して何種類目の文字か数えればいい.

いきなり1WA.原因は大文字を出力しなければいけないのに小文字にしてしまったこと

6:42 (+ 1WA)

B

コマの動きをシミュレーションして最終配置を求める問題

コマが追い越さないことを利用して問題を読み変えると少し楽になる.

ここで3WAも出してしまう.原因は, Qの値だけ制約が少し大きいのを見逃して,メモリの確保が足りていなかったこと. 原因に気づくのに時間がかかりかなりロスしてしまった.

22:24 (+ 3WA)

C

単純なしきい値を用いた判定器の正解率を最大化する問題

一番簡単なのはしきい値で全探索することだが,候補が[tex: 109]程度あるので無理. 実際にはしきい値は与えられた体重のどれか(または体重の最大値より大きい値)だけ考えれば十分なので,これで候補数を O(N)に減らせる. ただし,各しきい値に対して正解数の計算は安直にやると O(N)かかるのでまだ少し工夫がいる. 大人,子供それぞれの体重をソートしておいて,各しきい値の候補に対して正しく判定される大人と子供の人数をそれぞれソート済みの配列に対する2分探索で計算することによって O(N \log N)で解いた.

16:16

D

任意のジャンプ台の間を移動できるようにするための条件を求める問題

  • 訓練の回数を増やすと,飛び移れるジャンプ台の数が増えることはあっても減ることはない
  • 訓練回数を固定すれば,条件を満たすかどうかは比較的簡単に判断できる

という点を考慮すると,訓練の回数について2分探索するのが良さそうである. 実際条件を満たすかの判定については,ジャンプ台 iからジャンプ台 jに飛び移れるときに i\to jの辺を張ったようなグラフを考えて,すべての始点からDFSなりBFSなりして到達可能性を調べることでできる([tex: O(N3)]).

デバッグのためにいれた出力自体が間違っているというしょうもない理由で手間取り,ここでも時間ロスした.

30:33

E

与えられた予算で最大の数字をつくる問題

はじめ,残っている予算に対してその時点でつくれる数字の最大値をDPで求めていけばよさそうと考えた. 一見すると O(N)で良さそうなのだが,整数の桁あふれが問題になる. 多倍長整数を使えばできそうに見えるが,整数の桁が O(N)になるので,整数の計算やコピーに O(N)かかることになり実は全体で[tex: O(N2)]になってしまう.

解決策が思い浮かばず時間切れ.

終了後解説を見て通した. 桁数に注目するという考察ができていなかった. 以前もD - At Most 3 (Contestant ver.)で失敗したが,10進の桁に注目する考察が苦手なのかもしれない.

ABC256 感想・反省

atcoder.jp

結果

atcoder.jp

ABCDE5完(57:45 + 1ペナ) Performance: 1544

大きく詰まるところはなく5完できたのでとりあえず満足.

コード

github.com

各問題の振り返り

A

 2^ N を出力するだけ

cmath の pow 関数は浮動小数点で計算するので,一応整数型で pow を自前で実装. 一応 long 型にしておいたが,制約的には int でも大丈夫なはず.

2分26秒

B

野球っぽいルールに従って得点を数える

累積和で効率的にできそうなことは気づいたが計算量は余裕があるため,問題文どおりに愚直にシミュレーションしたほうが実装ミスは少ないと判断.

5分23秒

C

3x3マスの盤面において,縦横の和が指定されたときのありうる盤面数を数える (俗に言う魔法陣)

基本はありうる数字の埋め方について全探索だが,9マスぶんの数字の埋め方のパターンは  30^ 9 で大きすぎる. 探索する数を減らすため盤面の自由度を考えてみると,左上4マスだけ埋めればあとは自動的に決まることに気づく. もっと盤面が大きければ DFS などで探索したいが,たかが4つなので4重for文の雑なコードで済ませた.

9分58秒

D

たくさんの半開区間が与えられたとき,それらの区間の和を最小個数の区間の和で表す問題.

区間の和だけが重要なので,区間が与えられた順番にはあまり意味がない. そこで, L_i の値でソートして考えると見通しがよくなる.  L_i が小さいほうから区間を調べ,区間が重なっているかぎりつなげてひとつの区間にし,つながらないものが現れたら次の区間を開始する.

区間をつなげるときの処理をミスして1WA

14分35秒 + 1WA

E

与えられた不満度を最小にするような人の並び順を求める問題.

前から順に人の並び  P_1, P_2, \ldots, P_N を決めることを考えてみる.

 i 番目に人  j を置くとき  (P_i = j) ,これにより発生する不満度はそれより後ろにいる人のうち  j が嫌いな人のもつ不満度の和  \sum_{i \lt i', X[P_{i'} ] = j} C [ P_{i'} ] になる.

前から順に各時点で増える不満度が最小になるように順番を決めていけば良さそうである. 厳密な証明はなしで提出したがAC.

公式解説にあるグラフ理論的な考え方を使えばこの貪欲法も正しさを証明できる.

25分23秒

F

配列の各要素を更新しながら,3重の累積和を求めるクエリに答える問題.

紙で計算して3重累積和の一般項を求めるところまではできたが,そこからがどうすれば効率よく計算できるか思い浮かばず時間切れ.

終了後解説AC.  i^ k A_i で整理すればうまく処理できるというところが思い浮かばなかった.

実装にあたって,modint などを使わないと注意深く mod をとるのが結構面倒だった.

AtCoder で水色になった

初めてコンテストに参加してから2ヶ月半,13回目のコンテスト参加で水色レートを達成することができた. AtCoder で当初からひとつの目標としていた水色に到達したので,やってきたことなどをまとめてみたい.

レート遷移

時系列振り返り

参加開始当初の状況

情報系の修士2年生で,授業, 実験, 研究, 趣味などでもともとそこそこのプログラミング経験はあった. 使っていた言語は主に C/C++, Python. 他に Lisp, OCaml あたりも少し触る程度.

Pythonは学部1年のときに授業で習って以降,実験データの整理や作業用のちょっとしたスクリプトを書くのに使っていた.

大学の授業でC言語は勉強していたもののC++は扱わなかったので,学部3年のときに本を借りて少し勉強していたが本格的に使うことはしばらく無かった. 修士研究を進める際,先行研究の実装がC++で書かれていたので,これを読んだり拡張したりする目的で改めてC++を勉強し直した.

競プロについては学部3年のころから存在は認知していて,興味は少しあったものの参加したことはなかったし,どういう問題が出るかも知らなかった. 今年になって,就活で課されるコーディング試験の練習も兼ねてやってみることにした.

開始直後の頃

競プロでは実行時間の速い言語が有利と聞いていたので,基本的にC++を使っていくことにした.

コンテスト参加を本格的に考え始めたのは2月ごろ. とりあえず AtCoder Beginners Selection をやってどんな問題が出るのか見てみることにした.

その後,初参加は少し遅れて3/20のABC244. 結果はA-Dの4完で871の緑パフォ.(あとで確認したところ,この回はD問題まですべて灰diffだったらしい)

参加し始めた当初は実際緑コーダーぐらいの実力だったのではないかと思う.

緑になるまで

コンテストに参加し始めた後は知識不足を埋めるため一気に勉強を進めた. 主に用いたのは次の3つになる.

このころは,まだ典型知識の不足や実装経験の不足がかなり感じられたので,あまり自力で解くことにはこだわらず解説はどんどん見るようにしていた.

まず蟻本を中級編までざっと読んだ. きちんと実装までやっていないものも多くあるが,典型的なアルゴリズムを吸収する上でかなり役にたったと思う.

その後は典型90に取り組み,3/29までで★4以下すべて,4/3までで★5までをすべて埋めた. 典型90もわからない問題が結構あったので,解説ACはかなり多い. 蟻本で読んだアルゴリズムの実践という意味合いと,蟻本でカバーされていない典型知識を身につけるのに大いに役に立った.

その後は典型90の★6以降をやるにはまだ早いと感じていたので,初中級者が解くべき過去問精選 100 問に取り組むことにした. 典型90もこちらも教育的な問題がよく集められていると感じていて,このような題材を準備してくれた E869120 さんには本当に感謝したい.

蟻本や典型90でだいぶ経験を積む中で,過去問精選100の問題はかなり自力で解ける部分が多くなっていた. 復習もかねて適度な難易度の問題をたくさん解けたことはアルゴリズムの定着につながったと思う,

実戦経験の不足もあり結果が出ない回もあったが,レートは徐徐に上がっていき6回目の参加で緑レートに達した.

実は,緑に上がったあとよりも上がる前のほうが精進量は断然多い. すぐに結果が出たわけではないが,このころベースとして身につけた知識はその後レートを伸ばしていく中でかなり大きな推進力になっていたと思う.

水色になるまで

この頃は,修士研究の中間発表の準備や就活の面接等があり,以前ほど多く過去問を解くことはなかった. ときどき時間があるときに精選100問を解いていて,5/15にすべての問題を埋めた.

コンテストには毎週参加していて,時間内に解けなかった問題を1問は解説見てもいいのでできる限り解くようにしていた. この作業を通して青diffぐらいの問題にもいくらか向き合うようになったので,地力の向上につながったと思う.

コンテスト自体への慣れもあって少しずつレートはあがり,6/4のABC254にて水色レートを達成することができた.

競プロをやって良かったこと

まず,単純にコンテストに参加することは面白いと思うので,趣味として楽しめている.

実用的な面としては,競プロへの参加を通してアルゴリズムへの理解や実装力を向上できた点がある. 勉強したアルゴリズムの中には授業などで知っていたものもいくらかあるが,実装経験は不足していた. これらのアルゴリズムについて,競プロの問題を題材に実装することで理解が深まったと思う. また,アルゴリズムに限らずとにかく全体的に実装が速くなった.

今後

次は青色を目指したいと考えている. 水色までは比較的順調に上がってこれたが,現状青パフォは1回しか出せておらず,このまま漫然とやっていて青に上がれるかは微妙な気がしている.

これから強化していきたい部分は次の3つである.

  • 典型知識の強化
  • ライブラリ,ツール整備
  • 速解き
  • 考察力

まず,典型知識について. 基本的な部分はそれなりに身についてきてはいると思うが,まだまだ不足はある. ABCのF問題に出てくるぐらいのレベルのをだいたい網羅しておきたいと思う. まずは典型90の★6を埋めたい.

ライブラリ,ツール整備について. アルゴリズムなどをその場で素早く実装できる力を身につけたいと思っていたこともあり,現状ライブラリはあまり整備していない. ただやはりセグメント木など有名なデータ構造などはライブラリ化することでコンテスト中の時間的なメリットはかなり得られると思うので,頻出のものだけでも少し整備してみたいとは思っている. ライブラリ整備自体で理解が深まるところもあるのではないかと考えている. また,コンテスト中のテストなどを素早く行うためのツールを整備しておきたい.

速解きについて. 最近,コンテスト本番でB,C問題あたりで実装に意外とてこずって時間をかなり消費したりペナルティを食ったりしているケースが多い. 素直な実装をすばやく行うという面で経験値としても意識としても足りない部分があるので改善していかないといけない. 下埋めの精進はあまりしてこなかったが,一度集中的にやってみるのもありかもしれない.

考察力について. これまでABCでは比較的順調に成績を伸ばしてきたが,実はARCでは2回参加して両方わずか1完と惨敗している. AtCoder Problems で diff として同じぐらいとして表示されている問題でもABCとARCだとARCの問題のほうがかなり苦戦する傾向にある気がしている. ABCは知っていれば解けるたぐいの問題が比較的多いが,ARCのほうがアドホックな考察が必要なことが多いからではないかと感じる. 今後はARCの問題なども練習して考察力を身につけ,ARCでも成績を出せるようにしていきたいと考えている.

AHC 011 参加記

atcoder.jp

問題概要

 N \times N の盤面が与えられ,各タイルには4方向のうち1つ以上に線が伸びた図形が描かれている. 空きマスを適当に移動させることでタイルの絵をつなげて,できるだけ大きい木がつくられるようにしたい.

パズルの例
左: 初期盤面, 右: 完成図の一例

結果

プレテストで最終44位

ラソン参加2回目(長期は1回目)のわりには健闘できたのではないかと思う. 今回は大きさ  N^ 2 - 1 の木を完成させられるかどうかという点が大きな分岐点となっているように感じており,完成させるためのアイデアを比較的はやく思いつくことができたところが良かったと考えている.

追記: システムテスト後の最終順位は 42位 (2,321,975,585 点, 1問あたり 773991点) .TLEなどで下がらなくて良かった.

コード

github.com

いくつかのファイルに分けて実装している. 提出したソースは, solve.ccoj-bundle で展開したものである.

方針

今回用いた方針は,おおまかには以下のようになる.

  1. 完成盤面を生成
  2. 現在の盤面から完成盤面に一致させるようにパズルを動かす

まず,完成盤面の生成の方法について簡単に説明する.

はじめに,問題文にある盤面生成と同じ方法でランダムに木をつくる. ここでつくった木は,ほとんどの場合初期盤面と各タイルの数が一致しないので当然そのままでは完成盤面として使うことはできない. そこで,枝を適当に付け替えることによって初期盤面とタイルの数が一致するように変形させる. 枝の付け替えというのは,木に適当に一本辺を追加して閉路をつくったあと閉路内の辺を一本削除してタイルの数が初期盤面に近づくようにするという操作を繰り返すことによって行った. 実質的には初期解をランダムにいじって良くなったら採用するという山登り法になっている.

次に,現在の盤面から完成盤面への移動方法を考える.

各タイルに最終的な配置を事前に割り当て,この配置を実現するような経路を探索すれば良さそうである. タイル配置の割り当ては,とりあえず左上の位置から順に一番マンハッタン距離が近いタイルを割り当てる貪欲法で行った. 割り当てたタイル配置が実際に解ける配置になっているかどうかは配置順の転倒数を用いて判定し,不可能な配置になっている場合は適当にタイルを入れ替えることで解消した.

経路の探索については各タイルの現在位置と目的位置とのマンハッタン距離の和を評価関数としたビームサーチで実装してみたものの,今回の盤面サイズでは時間内に解を見つけられなさそうな様子であり断念. (上位陣の解法はビームサーチが多いようであるので,配置割り当てや評価関数などもっと工夫が必要だったかもしれないと思う.)

確実に時間内に解を見つけるようにしたかったので,とりあえずタイルを端からひとつずつ愚直に運ぶという方針で実装した. ここまでで基本的にすべての入力に対して  N^ 2 - 1 の大きさの木を完成させる手順を構築することができるようになった.

この実装をベースに手数を減らしていく工夫をいくつか行った. 今回実装した手数削減に関する工夫は,主に以下の2点になる.

  • タイル配置の割り当てを動的に行う
  • 同時に複数のタイルを揃える

まず変えたのはタイル配置の割り当て部分である. タイルをひとつずつ運ぶ場合には,最初からすべての配置を割り当てていく必要はなく,次に運ぶタイルさえ決まっていれば構わない. そこでタイル配置は各位置を埋める直前で動的に割り当てることにした.

また,タイルをひとつずつ運ぶのは目に見えて非効率である. タイルを複数同時に運ぶことで手数を削減したい. そこで,揃えるタイル数が少ない場合に特化したソルバをA*で実装し,これを使って2つまたは3つ単位でタイルを揃えた.

基本は上の行から順番にそろえるが,最後の3行は左から順に同時に揃える. また,最後の  3\times 4 領域に残る 11個 のタイルは一度に一気に揃えた.

実行例をひとつ gif で貼っておく (807870点).

今回の解法の実行例
今回の解法の実行例

考察過程

ここからは日記に近いものになる.

序盤

初日,ルールをざっと確認したあとまずは得点計算を注意してみる. 得点計算を観察すると,パズルを全タイル使って完成させられるか否かで壁ができているのでまずは全タイル使ってパズルを完成させることを目指したい. タイル数は  N^ 2 に対して許容手数は  2 N^ 3 と比較的余裕があるように見えるので,手数はあまり気にせずパズル完成の方法を考えることに集中することにする.

1週間という長期のコンテストで比較的時間があるので,まずはビジュアライザで遊びながらいろいろ考察してみる. 手でやってわかったがこのパズル,タイルを全部使い切って完成させるのはかなり難しい. デフォルトで表示される  6\times 6 盤面は何度かやり直して一応手で完成させることはできたが,パーツ数などをかなり注意深く考慮しないと完成は難しく,適当にぐるぐる回していてもパーツが余ってしまって完成しないのではないかという印象を持った. 完成図を先に決めてしまったほうが良さそうである.

どうやったら完成図をつくれるだろうか? 盤面全体をいくつかに分割して,分割どうしのつなぎ目を固定した盤面をつくれないかとか,有名な空間充填曲線 (ヒルベルト曲線など) のパーツをつくってつなぎ合わせるとか,一番上の列から列同士をつなぐ部分のパターン分けの方法をDPなどで考えられないかとか考察してみるもなかなかまとまらない.

最終的に思いついたのが,木を先に適当につくってしまいつなぎ目を適当に組み替えるというものでこれならうまくいきそうである.

とりあえずの方針が定まったので,得点計算やランダムな盤面生成,タイルの移動処理など基本的な部品から実装を進めていく. ランダムな木を生成するのは目標となる盤面の生成にも使うことになるだろう.

実装にも後から手を入れやすいよう慎重に進めていく. 前回初参加したAHC010 では終了後も続けて書いていたが,最初行き当たりばったりで実装を進めていたために後から手を入れにくくなってしまい,結局後から書き直すことになった. 少なくとも1週間は付き合うことになるコードなので,競プロというよりもまともなソフトウェアを書くつもりで進める. とはいえ,あまり実務経験があるわけではないのでコードの質はまだまだだとは思うけれども. AHC010 のコードのリファクタリングを通してクラスをうまく使うことで比較的すっきり書けることを学んでいたので,はじめからクラスを使いながら書いてみることにした.

月曜ぐらいまでで,一通り目標としていた最終盤面の生成や基本部品の実装を済ますことができた.

中盤

最終盤面をつくったら,どのタイルをどこに移動させるかという割り当てを決める必要がある. ここはあまりアイデアがなかったので,とりあえず端から順にタイルを見ていってその時点で一番近いものを貪欲に割り当てるという方法を採用することにした. 後から考えるとタイル配置の割り当てはグラフ上のマッチング問題とみなせるが,このあたりをきちんと整理して考えられなかったのはアルゴ力の不足と感じる.

最終盤面の生成と並行して,初期盤面から最終盤面へと移動させるにはどのようなやり方を使うのかということを考えていた. 自分でこの手のパズルのソルバを書いたことはあまりなかったのでやりかたをいろいろと検索してみることにする. スライドパズルといえば有名な15パズルがあり,これなら解法もいろいろと情報があるだろうと思い検索してみた. 不可能配置の解消なども検索で得た知識をもとに行った.

調べた結果これらのパズルの解法としてはどうやらAやIDAといったアルゴリズムなどを使うのが定番らしいとわかったが,これまで探索アルゴリズムは普通のDFSとBFSぐらいしか実装したことがなかったので,A*などのアルゴリズムをもう少しちゃんと調べることにする. どうやらこれらはゴールまでの距離をヒューリスティクスで予測して見込みのあるものから優先的に探索する手法ということがわかった.

A*を使うならどういうヒューリスティクスで評価関数を求めるかということが重要になる. 各タイルの現在位置からゴールへのマンハッタン距離の和を使うのが定番らしいとわかったので,とりあえずこれで書いてみることにした.

実際動かしてみると,どうも今回の盤面サイズではこれだけではまともに探索するのは少々厳しいようである. 探索時間を減らすためビームサーチ化 (これもはじめて書いた) したりもしてみたが, 6\times 6 ぐらいならときどきうまくいくが  10\times 10 などは手も足も出ないという感じだった.

幸い最大手数には余裕があるので,確実にパズルを完成させられる方針で実装することにする. 採用したのは,タイルを端から順に一個ずつ完成位置に埋めていくという非常に愚直な方法である. これなら複雑な探索を行う必要はないので実行時間は問題にならなさそうである.

次に埋めるタイルを移動させる手順はA*で探索させることにしたが,これは結局空きマスが運びたいタイルに向かうようにして,目標のタイルを動かした結果,最終位置との距離が縮まれば採用するだけなので非常に単純な評価関数で書ける. 各行の最後の2マスは1つだけ埋めてしまうともう一方が埋められなくなるので同時に考える必要がある. ここは1つずつ順に最終位置から1マス離れた固定の場所に運んでおいて固定手順で処理することにした. タイル位置が入れ替わっているような例外ケースも手動で解いた手順を埋め込んで対処した.

ここまでの実装が済んだのが4日目の火曜日. 初の有効な提出で 29,259,891 点を獲得. 33位という経験のない順位につくことができて嬉しかった.

終盤

タイルを1つずつ揃える愚直な方法でもまあまあな結果は出せていたが,改善の余地があることは明らかだった. まず,手動で手順を埋め込んでいるのもあって,右に行った直後に左に行くような明らかに非効率な動作が混ざることがあったので,これを解消する必要があった. また,実行時間にはかなり余裕があったので,制限時間内ランダムに完成図を作り直して経路探索し直すことでより良い解を見つけるだけでもだいぶましになりそうである. これらの簡単な改善を導入して提出した結果,33,255,140 点まで伸ばすことができた.

次に取り組んだのはタイル配置の動的な割り当てである. どうせタイルを一個ずつしか揃えないのなら,わざわざ最初に全部の配置を割り当てる必要はない. タイルを揃える直前に次に動かすタイルだけ都度決めるという方法に変えることにした. 最後タイルが少なくなってきたときに,不可能な配置を防ぐため残っているタイルすべてに配置を割り当てる. この方法でまた多少スコアが改善し,35,255,532 点に到達した. この時点の暫定順位が27位であり,結局このコンテスト中最も良い順位となった.

ここまでで実装としてすぐに改善できる部分は終わってしまった感じがある. 後は,タイルをひとつずつ揃えるかわりに2つや3つ一気に揃えることで手数を削減できるだろうと考えた. これまで使っていた実装だと探索のときに発生する盤面のコピーなどが無駄にコストが大きく非効率に感じていたため,少数の注目するタイルだけの位置を記録するソルバを書き直すことにした. ここでも使ったアルゴリズムはA*である.

盤面の1部分を揃えるコストを調べる方法として事前にパターンデータベースを作成するといった手法も調べていて知ったが,事前に計算した結果を利用するものなので今回のような提出形式には合わないと考え断念した. 小規模なパターンデータベースなら工夫によっては使うことはできるかもしれないが.

A*で少数のタイルを同時に解くにあたって,評価関数に改良を加えた. 具体的には,現在位置と目標位置とのマンハッタン距離に加えて,次の2つを導入した.

  1. 目標位置と同じ行(列)内でタイルの順番が入れ替わっているときにペナルティを課す.
  2. 目標位置に到達していないタイルの中で,現在の空白位置から最も遠いタイルまでの距離を足す.

1つ目は検索していて出会ったもので,いくつかの記事で Linear Conflict という名前で紹介されている. 2つ目は少数のタイルを解くという問題設定を考える上で独自に導入したものであり,タイルが何もない方向に無駄な探索をするのを防ぐ役割ができているのではないかと考えている.

これらのアイデアを使って数個のタイルを同時に解くソルバを書き,既存の1つずつ揃える実装を置き換えた. まずは2個ずつタイルを揃えるようにして,これがうまくいったあと 3個ずつ揃える実装に書き換えた. 3個揃えようとすると時々時間をかなり消費してしまいTLEの危険があったので,探索が長くなったら適当に打ち切ってフォールバックするなど行う必要があった.

揃える順番をジグザグにしてみるなど少しいじってみたりもしたが,大きな改善があったかは微妙.

探索失敗時のフォールバックの実装にやや手間がかかりここでコンテスト終了ぎりぎりに. 最後の提出では 38556229点 を獲得し,プレテスト44位で終了した.

欲を言えば順位表2ページ目以内になんとか入りたかったところだが,マラソン初心者にしては十分健闘したと思うし満足している.

ABC254 感想・反省

atcoder.jp

結果

atcoder.jp

ABCDE 5完 (68:16 + 0ペナ) 久しぶりにペナルティ0で切り抜けられたのは嬉しい. 今回で無事レートも入水することができた.

コード

github.com

各問題の振り返り

A

3桁の整数の下2桁を出力する問題

int よりも string で受け取ったほうが楽

1分22秒

B

問題文は少し読みにくいが,例を見ればパスカルの三角形のこととわかる

無駄に配列を再利用して計算しようとしてしまい,実装に少し手間取る. 入力サイズが小さい2次元配列でまとめて領域を確保して計算したほうが楽だった. こういうのはさらっと書けるようにしたい.

10分ぐらい消費

C

固定間隔のスワップだけでソートできるか判定する問題

要素を入れ替えられるのは  K の倍数間隔で並んだ要素だけなので,インデックスを  K で割ったあまりで分類して各部分列を並び替えることしかできない. 各部分列をソートした結果,数列全体もソートされているならば Yes, そうでないなら No となる.

10分ぐらい消費

D

掛け算して平方数になる数の組を数える問題

まず思いつくものとして  N^ 2 以下の  N 個の平方数を列挙し,条件を満たす約数を数えるという方針がある. 約数の列挙はもとの数の大きさの 1/2 乗程度かかるので,この場合全体の計算量は  O(N \sqrt{N^ 2}) = O(N^ 2)となり間に合わない.

平方数の列挙をベースに考えるのはだめそうなので,約数を直接考えることにしてみる.  a という数と, ab^ 2 という数を組にすれば,掛けた結果が  a^ 2 b^ 2 となって,良さそうである. ただし,これだけだと数え漏れがあって  a = p^ 2 q のように分解できるときは, a qb^ 2 のペアも考える必要がある.

この方法ですべて列挙することができて, O(N^ {3/2}) で解ける. 約数に関する考察が必要で,D問題にしてはやや難しい印象.

イデアを詰めるのに時間がかかり,34分ぐらい消費

E

グラフ上で一定以下の距離にある頂点の番号の合計を求める問題

距離も次数も定数でバウンドされているので,一回のクエリで足すことになる頂点数は定数個にしかならない (具体的には,最大で  1 + 3 + 3^ 2 + 3^ 3 個). したがって,一回のクエリは深さ制限付きの DFS なり BFS なりで適当に処理すれば定数時間なので, O(Q) で解ける.

E問題にしてはやや簡単すぎるような気もする.

13分ぐらい消費

F

長方形領域のGCDを求める問題

 A_i + B_j をすべて保存しようと思うと,それだけで  O(N^ 2) の時間,空間計算量が必要となりアウト.

なんとか  A_i B_j に対する前処理でうまく解けないか考える必要がある. GCDの性質について考察してみると,結局重要なのは領域に含まれる数どうしの差分であるということがわかる. したがって,それぞれの数列の差分を事前に計算し,連続する区間に含まれる差分のGCDを前処理で計算すれば良さそうであると考えられる.

セグメント木を使えば上のような操作が  O(N \log N) で可能であり,あとは1クエリあたり  O(\log N) で解ける. アイデアは終了10分前ぐらいになんとか思いついたが,セグメント木の実装に不慣れだったこともあり実装は間に合わず. 終了後に通した.

ABC253 感想・反省

atcoder.jp

結果

atcoder.jp

ABCDE5完 (91:15+4WA) 5完で1147しかパフォ出ないのは正直しんどい (レート微減)

Eが比較的簡単だったために解答時間でかなり差がつく回だったか.

最近WAが多いのと,そもそも速解きがやや弱い気がするので改善する必要がある.

コード

github.com

各問題の振り返り

A

3つの数の中央値を考える問題

問題自体は簡単だが,なぜか中央値の定義をど忘れして少し引っかかった.

しばらく後からAC

開始からの経過時間 62:29 (D問題より後)

B

2点間の移動距離を求める問題

マンハッタン距離を出せばいいだけなので簡単. 入出力に少し手間取る (cin から char に入力したとき改行が吸収されて変になったりしないか少し心配になった.実際は改行は区切り文字として処理される) stringで入力したほうが迷いがなかったかも?

5分ぐらい消費?

開始からの経過時間 8:36

C

与えられた数を1つ増やす操作と指定された個数消去する操作を繰り返しながら,各時点での最大値と最小値を管理する問題

一見してmultisetらしく見えたので実装してみたがTLE. よく見てみると,指定された個数の消去に最悪 O(N) になっていることに気づく.

追記: 解説を読んで気づいたが,実際の問題は要素の消去自体ではなく count メソッドの計算量だったらしい.これを改善すると通った (c-2.cc)

実際は map で数の個数を管理したほうが書きやすい

multiset を少し調べながら書いたことで時間がかかったのと,TLE 出た後の対応などでやや手間取って21分+1ペナ消費

開始からの経過時間 29:15

D

特定の数の倍数を除いた和を求める問題

高校数学みたいな問題. 愚直に和を計算するのでは間に合わないので,和の公式を使う.

1 から N までの和を求めてから A,B それぞれの倍数の和を引くが,公倍数の和は2度引いてしまうので1回足す.

公倍数は  A,B の最小公倍数の倍数として考えなければいけないが,勘違いして単純に  A\times B を使ってしまい 1WA. 冷静に考えると一番もったいないミスだったかも.

10分消費

開始からの経過時間 39:59

E

となりあう値が K以上離れた数列の個数を求める問題

個数自体は数列の添字と最後の値をテーブルにした dp で求めればよいが,単純にやると更新に  O(M) かかるので更新を累積和を使って高速化する.

方針自体はすぐ思いついたのだが,提出してみると2ケースでWA 怪しそうなところを1つ直してみるも変わらずもう1回WA

原因は  K=0 のコーナーケースの見逃しだった. コンテスト終盤になってようやく気づいて修正.

コーナーケースに気づくのに時間かかりがちな気はするのでこのあたりの注意力やデバッグ力はもっと鍛えなければいけない気がする.

51分ぐらい消費 (途中でA問題の提出を挟む)

開始からの経過時間 91:15

F

行列のいくつかの行にまとめて値を足すクエリと1行の値を特定値にセットするクエリに対し,各要素の値を計算する問題

遅延セグメント木とか使うのかな?と思ったが,時間もなく全然まとまらず.

pgffor のまとめ

概要

pgfforパッケージは,多くのプログラミング言語で用いられるfor文と似た構文として\foreachコマンドを提供する. pgfforパッケージはtikzパッケージを読み込むと自動的に読み込まれるようになっており,tikzによる描画で繰り返しを行うときに用いることが多いが,pgffor自体で単体でも使える独立したパッケージになっている.

この記事ではpgfforの基本的な使い方をまとめる.

公式のマニュアルはtexdoc tikzの89章にまとまっている. 記事内で触れていない機能も一部あるので,詳しく知りたい場合はマニュアルを参照してほしい.

構文

\foreachコマンドの構文を簡単にまとめる.

\foreach <variables> in <list> <commands>

<variables>で宣言された変数に<list>に含まれる値を順に代入し,それぞれの値を使って<commands>を実行する.

  • <variables>で変数(制御綴)を宣言する.

    • /で区切ることで複数の変数を宣言することもできる.
  • <list>{0,1,2,3}のような,{...}で囲まれたカンマ区切りの値のリストである.

    • 変数が複数ある場合は/で区切ってそれぞれの変数に対応する値を書ける.すべての値を明示的に書かない場合は,最後に書いた値が以降の変数について繰り返される.
    • {0,1,...,10}のような省略構文を使うこともできる.
  • <commands>は以下のいずれかである.

    • tikzの1つの描画コマンド (次の;まで)
    • 1つの\foreach文 (多重ループ)
    • {...}で囲まれたグループ

リストの省略記法

連続した数字,英字の列挙について...を使った省略記法を用いることができる.

使用法は比較的直観的に使えるものになっていて,例えば,

  • {0,...,5} → {0,1,2,3,4,5}
  • {a,...,f} → {a,b,c,d,e,f}

のように扱われる.

...の前に2つの値を指定すると,その差分を使って補完する値が計算される. 例えば,

  • {2,4,...,10} → {2,4,6,8,10}
  • {0,0.1,...,0.5001} → {0,0.1,0.20001,0.30002,0.40002,0.50003}

のように使うことができる. 2つ目の例でわかるように,浮動小数点演算の誤差には注意する必要がある.

公式のマニュアルにあるサンプルをメインにいくつか例を見る.

  • 基本的な繰り返し

\x1,2,3,0を順番に代入し,[\x]を出力する.

\foreach \x in {1,2,3,0} {[\x]}
% -> [1][2][3][0]
  • tikz のコマンドを繰り返す

x座標を変えながらいくつかの円を描く.

\tikz
    \foreach \x in {0,1,2,3}
        \draw (\x,0) circle[radius=0.2cm];

  • 二重ループ

2重ループを使って x,y の両方の座標についてループさせる.

\begin{tikzpicture}
    \foreach \x in {0,1,2,3}
        \foreach \y in {0,1,2,3}
        {
            \draw (\x,\y) circle[radius=0.2cm];
            \fill (\x,\y) circle[radius=0.1cm];
        }
\end{tikzpicture}

  • 2つの変数を宣言する

/で区切って\x, \xtext の2つの変数を宣言する. \xtextを明示的に指定していない場合は,\xと同じ値が使われている.

\begin{tikzpicture}
    \foreach \x/\xtext in {0,...,3,2.72/e}
        \draw (\x,0) node {$\xtext$};
\end{tikzpicture}