sankantsuのブログ

技術メモ・競プロなど

ABC252 G - Pre-Order 解説

atcoder.jp

与えられたDFSの行きがけ順  P_1,\ldots,P_N を達成するような  N 頂点の根付き木の数を求める問題.

部分木の形はその部分木の外にある頂点の置き方にはほとんど影響を与えないので,部分木ごとに数を数えて組み合わせる動的計画法が有力に見える.

そこで次のように定める.

 dp [ l ] [ r ] : 頂点  P_l, \ldots, P_{r-1} からなる行きがけ順  P_l, \ldots, P_{r-1} を満たす部分木の数

指定された行きがけ順を満たすために注意すべき点は,あるノードの子が複数ある場合には番号が小さいほうから訪れるという条件である. この条件より,ある頂点を兄弟ノードとして足す場合には,これから足す頂点の番号が現在ある兄弟ノードの中で最も番号が大きいものよりもさらに大きくなっている必要がある.

このことから,部分木の構成において根に対する直接の子ノードのうち最も頂点番号が大きいものがどれであるかで場合分けを行うという方針が見えてくる. そこで,下図のように頂点  P_l, \ldots, P_{k-1} からなる部分木の根に  P_k, \ldots, P_r からなる部分木を接続することでより大きな部分木を構成することを考える.

部分木の構成

このとき与えられた行きがけ順を満たすためには,すでに  P_l の子になっている中でもっとも頂点番号が大きいもの ( P_{k'} とする) よりも  P_k のほうが大きくなっていなければならない. したがって, P_{k'}, \ldots, P_{k-1} からなる部分木を構成する段階で, P_{k'} \lt P_{k} を満たすかどうかということを覚えておく必要がある.

そこで,次のようにもうひとつ配列を定める.

 dp' [ l ] [ r ] : 頂点  P_l, \ldots, P_{r-1} からなる行きがけ順  P_l, \ldots, P_{r-1} を満たす部分木のうち, P_{k'} \lt P_r を満たすものの数

なお, P_l が子を持たない (部分木が根のみからなる) 場合は行きがけ順の制約に関係なく任意の頂点を子としてつけ加えることができるので,  P_{k'} = -1 と考えてよい.

以上の考察をもとに  dp, dp' が次のように計算できることがわかる.

部分木が根のみからなる場合は,

 \displaystyle dp [ l ] [ r ] = dp' [ l ] [ r ] = 1 \qquad (r - l = 1)

 r - l > 1 のときは,

 \displaystyle \hphantom{'}dp [ l ] [ r ] = \sum_{k \in \{ l+1, \ldots, r-1 \} } dp' [ l ] [ k ] \times dp [ k ] [ r ]

 \displaystyle dp' [ l ] [ r ] = \sum_{ \scriptstyle k \in \{ l+1, \ldots, r-1 \} \atop \scriptstyle P_k \lt P_r } dp' [ l ] [ k ] \times dp [ k ] [ r ]

 dp dp' の更新式はほとんど同じであるが, dp' は部分木の直後にくる頂点を根  P_l に対して子として付け加えることができるかどうか覚えている点が異なる.

 dp [ 1 ] [ N+1 ] が求める値である.

実装例

github.com

g.ccがメモ化再帰による実装,g-2.ccがdpテーブルを直接計算する実装

実装の都合で添字はひとつずらしている.

ABC252 感想・反省

atcoder.jp

結果

atcoder.jp

ABCDEF6完 (72:26+4ペナ) ペナルティ多いのは少しもったいないが,6完は初なので嬉しい.

青パフォも初.

コード (C++)

github.com

各問題の振り返り

A

与えられたASCIIコードに対応する文字を出力する問題

文字コードとか知らないと厳しいかもしれないが,知っていれば実装は一瞬で終わる. 迷わず書くことができた.

1分消費

B

一方の配列の最大値を与えるインデックスがもう一方の配列に含まれるかどうか判定する問題

配列に含まれるかどうか検査するのにsetを使ったが,あまり計算量気にする必要もないので線形探索したほうが素直かもしれない.

添字ずれで1WA. 実装に少し手間取ったり,しょうもないWA出したりするので,低難度ももう少し練習したほうがいいのだろうか...

7分消費

C

スロットを揃えるためにかかる最短秒数を求める問題

秒数が同じタイミングで出る文字が被る場合,文字を揃えるためには一方を押してからもう一方を押すために一周待つ必要がある. ある文字について,タイミングごとに出現回数を数えておけば,最も多くその文字の出現が被っているタイミングで揃えるまでにかかる秒数が決まる. それぞれの文字について揃えるまでにかかる秒数を調べ,もっとも短いものを出力すれば良い.

12分消費

D

異なる数字を3つ選ぶ選び方の数を求める問題

文字の出現回数を数えればそこから計算できそうな気がしたが,すぐにはわからずいったんEに移動.

数え方をよく考えてみると,数字の出現にかぶりがなければ  {}_n C_3 通り. 同じ数字が2回以上出現する場合は,この数字を複数回選ぶような選び方を除外すればいい. ある数字がk回出現するとき,除外するパターンの数は  (n-k){}_k C_2 + {}_k C_3 である.

intのオーバーフローで2WA

24分ぐらい (D+Eで39分)

E

ある頂点からほかの頂点までの距離が短くなるように辺を残して木をつくる問題

ダイクストラで頂点1から他の頂点への最短経路を求め,各最短経路に必要な辺だけを残せば閉路はできていないはずなので,木になるはず. したがって,通常のダイクストラで最短距離を求めながら,使った辺を保存していくことで解ける.

intのオーバーフローで1WA

15分ぐらい

F

パンを切るためのコストが切る前の長さで決まっていて,つくりたいパンの長さが決まっているときに切り出すのにかかる最小コストを求める問題

見てすぐ既視感があり,蟻本にのっている下の問題とほぼ同じとわかる.

3253 -- Fence Repair

最終的にパンを切り終わった状態からはじめ,かけらが小さいほうから切り出すためのコストを確定させていく貪欲法的なアルゴリズムで解ける.

蟻本は偉大

12分ぐらい

G

与えられたDFSの行きがけ順を達成する木構造のパターン数を求める問題

与えられた行きがけ順の前から見ていって,最後のノードをどの深さに足すかでdpとかするのかなとか考えてみるがコンテスト中には解けず.

後日解いて解説を書いた.

sankantsu.hatenablog.com

LaTeXの\@ifnextcharマクロ

概要

LaTeXカーネルで定義されている\@ifnextcharというマクロについて説明する. このマクロを使うとトークンの先読みを行うことができ,オプション引数の実装などに応用できる. カーネルやパッケージのソースにはよく登場するので,これらを読んでみたい場合には役立つ知識になるだろう.

以下,\makeatletterの状態で考える (@が英文字と同等扱い).

動作

\@ifnextcharは,3つの引数をとる.

\@ifnextchar<token>{<true case text>}{<false case text>}...

と書くと,3つ目の引数の次にくるトークン(上で言うと...の部分の先頭)を先読みし,このトークンが<token>に一致していれば<true case text>が展開され,一致しなければ<false case text>が展開される.

使用例

最も重要な使用例は,オプション引数の実装である. LaTeXの多くのコマンドは,[...]で囲んだ部分をオプション引数として渡せるようにしてある.

\defによるマクロで[...]の形の引数を受け取ろうと思うと,次のように書くことになる.

\def\foo[#1]{(option:#1)}

この定義により,\foo[...]のような形で引数を渡すマクロを作れる. 問題はこの定義だと[]の括弧は省略不可能になり,これに沿わない形式の引数を与えるとエラーとなることである.

\@ifnextcharを用いると,この問題点を次のような方法で解消できる.

\def\foo{\@ifnextchar[\@foo\@@foo}
\def\@foo[#1]{(option:#1)}
\def\@@foo{(no option)}

このように定義することで,\fooは次にくるトークンを先読みして[と等しいかどうか調べることができる. 等しい場合にはオプション引数を取るバージョンである\@fooを展開し, 等しくない場合にはオプション引数を取らない\@@fooを展開する.

したがって,オプション引数がある場合とない場合でそれぞれ次のような結果が得られる.

\foo[bar] % -> (option:bar)
\foo      % -> (no option)

実装

LaTeXカーネルのソース(latex.ltx)を読んで実装を追ってみる.

\long\def\@ifnextchar#1#2#3{%
  \let\reserved@d=#1%
  \def\reserved@a{#2}%
  \def\reserved@b{#3}%
  \futurelet\@let@token\@ifnch}

まず,引数をそれぞれ\reserved@d,\reserved@a,\reserved@bというマクロに保存しておく.

その後にくる\futureletというプリミティブが鍵で,これは

\futurelet\cs<token1><token2>...

と書くと\let\cs=<token2>を実行したあとで<token1>を展開するものである. この動作により,\csに先読みトークンを保存した上で<token1>の展開が行えるので,<token1>の定義の中で\csの中身を見ての分岐を行うことができるようになる.

つまり,今回の場合には\@let@tokenという制御綴に先読みトークンを保存しておいてから\@ifnchの展開を行う. この\@ifnchは次のように定義されたマクロである.

\def\@ifnch{%
  \ifx\@let@token\@sptoken
    \let\reserved@c\@xifnch
  \else
    \ifx\@let@token\reserved@d
      \let\reserved@c\reserved@a
    \else
      \let\reserved@c\reserved@b
    \fi
  \fi
  \reserved@c}
\def\:{\let\@sptoken= } \:  % this makes \@sptoken a space token
\def\:{\@xifnch} \expandafter\def\: {\futurelet\@let@token\@ifnch}

\ifxは,\ifx<token1><token2>の形で<token1><token2>の一致判定を行うプリミティブである. 最初の分岐は先読みトーク\@let@tokenが空白文字かどうかを判定して,もし空白文字なら読み飛ばすということをしている.

次の分岐が重要で,先読みトークンが事前に保存しておいた\@ifnextcharの第1引数\reserved@dに等しいかどうかを判定する. 等しければ保存された第2引数\reserved@aを実行するようにセットし, 等しくなければ第3引数\reserved@bを実行するようにセットする.

まとめ

  • \@ifnextcharの動作と応用例を説明した.
  • \futureletを用いた\@ifnextcharの実装を説明した.

TeXでドラゴン曲線を描く

概要

ドラゴン曲線というフラクタル曲線の一種をTeXを使って描いてみる. この題材はTeXブックに載っている有名(?)なもので,今回は元のプログラムを参考にしつつ簡易的なバージョンに書き直した.

出力結果を画像にすると以下のような感じ.

ドラゴン曲線

コード

github.com

pdftexコンパイルできるサンプルを用意した. 以下,実装について説明する.

解説

全体の流れ

まず,線の構成要素をボックスとして用意しておく. ここでは縦/横方向の線を構成要素として使った(線は\vruleを使って描ける). 基本的にはこの縦線または横線を\kern, \raiseといったTeXのプリミティブを使って位置を調整しながら並べることで任意の線を描く.

毎回プリミティブを使って位置制御をするのは大変なので,現在の位置から紙面の上下左右に向かって線を伸ばすという操作をするマクロを定義する (\goEast, \goNorth, \goWest, \goSouth).

また,現在の進行方向に対して相対的に直進,後退,左折,右折ができるようなマクロも用意する (\S,\B,\L,\R). さらに,\S,\B,\L,\Rを並べた命令列を受け取って連続した線を描くようなマクロpathを用意する.

最後に,ドラゴン曲線の再帰的な定義を使って\pathコマンドを使ってドラゴン曲線を描く.

縦線と横線の用意

\newdimen\lineLength
\newdimen\lineWidth
\newbox\vLine
\newbox\hLine

\lineLength=5pt
\lineWidth=0.3pt

\setbox\vLine=\hbox{\vrule height\lineLength width\lineWidth}
\setbox\hLine=\hbox{\vrule height\lineWidth width\lineLength}

\newdimen, \newboxというのはそれぞれレジスタを確保するための命令で,それぞれ寸法 (pt とか cm とか) を保存するレジスタ,ボックス (文字列を組版したまとまりを入れておく箱) を保存するレジスタを確保する.

線の長さ, 幅の適切な寸法をそれぞれ \lineLength, \lineWidth という名前で設定しておく.

設定した寸法を使って \vrule を使って線を描き,縦の線を \vLine, 横の線を \hLine というボックスに入れておく.

上下左右の移動

\newcount\dir
\newdimen\y

\def\goEast{\raise\y\copy\hLine \global\dir=0\relax}
\def\goNorth{\raise\y\copy\vLine \kern-\lineWidth \global\advance\y\lineLength \global\dir=1\relax}
\def\goWest{\kern-\lineLength \raise\y\copy\hLine \kern-\lineLength \global\dir=2\relax}
\def\goSouth{\global\advance\y-\lineLength \raise\y\copy\vLine \kern-\lineWidth \global\dir=3\relax}

\yというのは,現在のy座標を覚えておくためのレジスタである. 線を描く前に \raise\y として \y に入っている値ぶんだけ組版位置を上方向に上げておくことで上下位置の調整を行う.

x方向の調整は,文字の組版によって自動的にx座標が右に文字幅ぶんだけ動くことと,\kern という命令によって明示的に左右方向の位置を動かすことによって行う.

\dir はここまでの分ではなくてもかまわないのだが,現在の進行方向を入れておくためのレジスタで,次に進行方向に対する相対的な移動を実装するために使う.

進行方向に対する相対的な移動

\def\S{\ifcase\dir \goEast \or\goNorth \or\goWest \or\goSouth \fi} % move straight
\def\B{\ifcase\dir \goWest \or\goSouth \or\goEast \or\goNorth \fi} % move back
\def\L{\ifcase\dir \goNorth \or\goWest \or\goSouth \or\goEast \fi} % move left
\def\R{\ifcase\dir \goSouth \or\goEast \or\goNorth \or\goWest \fi} % move right

\def\path#1{\hbox{\dir=0\y=0pt#1}}

このステップは比較的単純で\ifcaseという命令を使って現在の進行方向\dirを参照して紙面での上下左右移動を選択するだけである.

\ifcaseについて簡単に説明しておくと,

\ifcase<number> <case0> \or<case1> \or<case2>... \else<other cases>\fi

という書式で,<number>の値が0なら<case0>を展開,1なら<case1>を展開... という具合に分岐し,どれにも当てはまらなければ <other cases> を展開する.

\pathは単にy座標と向きを初期化したあと渡された命令列を展開してできた組版結果をまとめるだけである.

ドラゴン曲線を描く

\newcount\n
\def\dragon{\ifnum\n>0{\advance\n-1 \dragon\L\nogard}\fi}
\def\nogard{\ifnum\n>0{\advance\n-1 \dragon\R\nogard}\fi}

\centerline{\path{\n=13 \dragon}}

いよいよ最後のステップであるドラゴン曲線の描画である. ドラゴン曲線は次数によって再帰的に定義されていて,

  • 最初 (n=1) はただの直線
  • n=k+1 の曲線は,n=k の曲線を描いたあと左に曲がり,逆向きに n=k の曲線を描いたもの

という手順で構成できる.

Wikipedia にアニメーションが置いてあって,とてもわかりやすい.

Dragon curve - Wikipedia

実装は素直に再帰的定義をなぞったものになっていて,\nを1減らして\dragonを描き,左に曲がって\nogard(\dragonの逆)を描く. ポイントとしては,\n を1減らす \advance\n-1 という命令の効果が {...} で囲んだグループに対してローカルになるところで,\dragonが展開されるたびにローカルに \n を更新することで再帰的な定義を再現できるようになっている.

ARC 140 感想・反省

atcoder.jp

結果

atcoder.jp

A1完 (64:23 + 2ペナ)

初めて実レート減少

最初に参加したARC(138)でも1完でひどいパフォを出し,今回はせめて2完したいと思っていたが撃沈 やっぱり同じ配点の問題でもABCよりARCのほうがずっと難しいと感じる. 苦手意識による思い込みなのか,問題傾向が苦手なのか,それともたまたま解けなかっただけなのか....

コード

github.com

終了後に実装を修正している

各問題の振り返り

A

文字列をできるだけ短い周期で繰り返すパターンに編集する問題

N \leq 2000の制約を見るに,O(N^ 2)ぐらいが目安かなと見積もる.

最初に考えたのは,周期について2分探索できないかという方針. しかし,これはある周期を達成できるならそれより長い周期はかならず達成できるという前提に基づいていて,正しくない.

2分探索はできないが,調べる周期のパターンを少なくしたいというモチベーションは悪くなさそうである. 周期としてありうる数がなんであるかということをもう少し考えてみると,Nの約数だけが周期となりうるということに気づく. 約数の数はそれほど大きくならないと考え,ひとつの周期に対する達成可能であるかの判定にO(N^ 2)で約数について全探索すれば解けるのではないかと考えてみる.

ある周期が達成可能かどうかをO(N^ 2)で調べる方法だが,周期の各位置にあたる文字を一番出現回数の多い文字に合わせるという作業を O(N)で行い,これを文字列の開始位置をずらしてすべて調べればよいだろうと考えた.(実際,文字列の開始位置をずらす作業は意味がない.計算量がO(N^ 2)で良いだろうという思い込みがあったことから無駄に気づけなかった.)

テストケースが通ったので提出してみるとWA. 見直してみると,周期内の各位置について調べるという部分が実は周期内の1点しか調べていないというミスをしていることに気づき,修正.

しかしこれを提出してみるとなぜかRE. よく見てみると,なぜか同時に開いていたB問題のほうに提出してしまっていた. 改めて提出し直すと,1ケースだけTLEになった.

なんとか速くならないかと見直してみるが,なぜか実際文字列の開始位置をずらす作業が無駄であることには気づかず. 代わりに,約数を全探索するのではなく,ある約数Mについてそれを周期とすることができないなら,Mの約数についても周期にすることはできないという点に気づいて修正し,なんとかACになった.

B

文字列に対して特定の操作を行うことができる回数の最大値を求める問題.

操作可能な箇所すべてについて順番をかえて再帰的に探索するのでは間に合いそうもないので,最適な操作順というものを考えてみる. 奇数回目の操作の後,操作箇所のまわりで新たにARCという文字列ができるのは,もともとがAARCCという並びになっていたときである. 偶数回目の操作の後,操作箇所のまわりで新たにARCという文字列ができることはない.

よって,AARCCというのような並びがある部分はできるだけ奇数回目に操作することにするのが最適になる.

最適な操作について,より具体的に考える.  A^ k R C^ kという並びを文字列の中からすべて探し,重複を含むkの集合(multiset)を考える. - 奇数回目の操作ではkが最も大きいものを選び,k\gt1であればkを1減らして集合に戻す.k=1であれば要素を取り除く. - 偶数回目の操作ではkが最も小さいものを選び,要素を取り除く. このようにして行える操作の回数を求めれば答えが求まる.

後は, A^ k R C^ kという並びを与えられた文字列から探すというサブ問題が解ければ良い.

この並びを探す部分について実装がごちゃごちゃになってしまい,原因はよくわからないがバグって通らなかった. コンテスト終了後,ランレングス圧縮をしてから並びを探すと比較的見通しが良いことに気づいて実装し直してみるとACできた.

Bまでは正答したかったが,残念.まあ実力不足だろう.

ABC251 E - Takahashi and Animals 解説

atcoder.jp

以下のN個の行動がある.

  • 行動1: 動物1と動物2A_1円で餌をあげる
  • 行動2: 動物2と動物3A_2円で餌をあげる

    \vdots

  • 行動N: 動物Nと動物1A_N円で餌をあげる

すべての動物に餌をあげるための最小コストを計算したい.

少し観察すると,次の特徴がわかる.

  • 動物iに餌をあげるためには,行動 i\mathord-1 か行動 i の少なくとも一方は行う必要がある.

したがって,まず動物1に対して行動1か行動Nのどちらかによって餌を与えるか決め,さらに動物2,3と順に餌を与える方法を決めていく動的計画法で解けそうとわかる.

  • dp[0] [j] : 行動 Nを使って1,\ldots,j番目の動物に餌を与えるための最小コスト
  • dp[1] [j] : 行動 Nを使わずに1,\ldots,j番目の動物に餌を与えるための最小コスト

と定める.

動物jに餌を与える方法は行動 j\mathord-1 か行動 jのいずれかであるから,

\displaystyle dp [i] [j] = \min \{ dp [i] [j-2] + A[j-1] ,\, dp[i] [j-1] + A[j] \}

によって求められる.

行動 0 を使っていた場合にはすでに動物 N には餌を与えているので, dp [0] [N-1]  dp [1] [N] の小さいほうが答えである.

実装例

github.com

文章の説明と添字がずれているのと,テーブルの更新順が若干違うことに注意.

ABC251 D - At Most 3 (Contestant ver.) 解説

atcoder.jp

数列をうまくつくることで,数列から3個以下の数を選んでW以下の任意の数をつくれるようにせよという問題

 Wが最大値10^ 6であるときに解となる数列さえ求めれば,この数列を使って10^ 6以下の任意の数がつくれるのでW \lt 10^ 6を満たす任意の Wに対する解にもなっていることがわかる. よって, W=10^ 6の場合の解を求めることに注力すれば良い.

与えられた数をいくつかの数の和に分解したいので,このために都合の良い表示を考えたい. このときに桁を分けて考えるというのが有効な考え方になる. 例えば,適当に2563という数を考えると,2563 = 2000 + 563 や,2563 = 2500 + 63 のように桁が独立している数どうしの足し算は考えやすい.

 10^ 6未満の数は10進で6桁の数として表せるので,2桁ずつに分解して各グループについて00から99まですべての数をつくれば,各グループから1つずつ数を選んで組み合わせることで6桁以下の任意の数を表現できる.(00は 0 であり,そのグループからは数を選ばないということによって表現できるので,明示的につくる必要はない)

最後に,1000000という数だけは7桁なのでこれだけ別に数列に加える. 以上で任意の数を構成できた.

実装例

github.com

備考

もちろん10進ではなく2進表示などでも原理的にはよいのだが,たとえば2進で10^ 6以下の数を表現しようとすると20桁いる. 7桁,7桁,6桁に分けて各グループについてすべての数をつくると,2^ 7 + 2^ 7 + 2^ 6 = 320程度の数が必要になるので制約を満たさない. 今回は上限が10^ 6という10進できりの良い数になっているので10進表示で考えるのが一番無駄がないということだろう.