2020年5月28日木曜日

JOI2009 春合宿 - Pyramid

ピラミッドを高さの降順にソートしてBFSのイメージで埋めていく。具体的にはH=3000H=3000から降りながら次を繰り返せばよい:

  1. 頂上の高さがHHのすべてのピラミッドについてHHを答えに足し、その座標(x,y)(x, y)を(スタックなりキューなりに)積んでおく。
  2. 前のループで高さH+1H+1として積んでおいた頂点の八方を調べて、それぞれ訪問済みでなければHHを答えに足して座標を積んでおく。

メモリ制約が厳しいのでいろいろ使いまわす必要がある。

JOI2010 春合宿 - Sengoku

(i,j)(i, j)を通る傾き11の直線の切片はjij-iで、傾き1-1の直線の切片はi+ji+jになる。

とりあえず重複のことは忘れて番兵毎に数えると、傾き11の直線の通るマスはLijL - |i-j|個あり、傾き1-1の直線の通るマスはi+j<Li+j < Lのときi+j+1i+j+1個、i+jLi+j \ge Lのとき2L(i+j+1)2L - (i+j+1)個あることがわかるので、まずはこれらの総和が基本になる。

次に、重複を除くことを考える。まず、切片と傾きが同じ直線は完全に重なるので、登場した切片を記録しておいて、前に見たものは無視することで重複を避けられる。

最後に傾き11の直線と傾き1-1の直線の交点の数を引く必要がある。まず、交点が格子点になるのは切片が偶数同士、奇数同士の場合なので、切片が偶数の直線についてのみ考えることにする。(奇数のものの交点も同様に数えられる。) 傾き11で切片がCCの直線と傾き1-1の直線が与えられた領域内で交わるのは、後者の切片が[C,2(L1)C[|C|, 2(L-1) - |C|]に入っているとき、またその時に限るので、ソートしておいてそれぞれ二分探索で数えられる。

2020年5月25日月曜日

CodeChef May Cook-Off - Chefina on a Trip

無向辺を2つの有向辺と見て、スコアが下がる向きに重み1-1、上がる向きに+1+1、変わらないなら00を付与すると、ある有向パスが上りになっているmin\Leftrightarrow \minクエリが11である、下りになっているmax\Leftrightarrow \maxクエリが1-1である、と判定できる。

ダブリングLCAの構築をする時に、同時にmin/maxについてもダブリングのテーブルを作っておけばパスuvu - vがbeautifulかどうかは適当に判定できる:

  1. uuから根に向けてスコアが上がる限りダブリングで上る。LCA(u,v)\operatorname{LCA}(u, v)の深さより上(inclusive)に行けたら3に行く。
  2. さらに根に向けてスコアが下がる限り上る。LCA(u,v)\operatorname{LCA}(u, v)の深さより上に行けなかったらfalseを返す。LCA(u,v)\operatorname{LCA}(u, v)の深さより上に行けたら頂上がuLCAu- \operatorname{LCA}の側にあると記録しておく。
  3. vvから根に向けてスコアが上がる限り上る。LCA(u,v)\operatorname{LCA}(u, v)の深さより上に行けたらtrueを返す。頂上がuLCAu-\operatorname{LCA}の側にあるのにLCA(u,v)\operatorname{LCA}(u, v)の深さより上に行けなかったらfalse。
  4. さらに根に向けてスコアが下がる限り上る。LCA(u,v)\operatorname{LCA}(u, v)の深さより上に行けたらtrueを返す。
  5. falseを返す。

山のイメージに従って書くだけなので特に難しいとは感じなかった。でも、振り返ってみると、uuvvを完全に対称に扱ったほうがいい気がする。

2020年5月24日日曜日

パ研コンペティション3日目 E - 美しい和音

f(i,x):=A1,...,Aif(i, x) := A_1, ..., A_iを使って作れる総和xxの美しい和音の総数、とする。ffの漸化式はAiA_iを使う/使わないの二択の和でf(i,x)=f(i1,x)+f(i2,xAi)f(i, x) = f(i-1, x) + f(i-2, x-A_i)と書ける。これでDPすれば部分点が取れる。

また、DPのほとんどの枝で不可能な状態の遷移をしていることに注目すると高速化できる。例えば、A1,...,AiA_1, ..., A_iから作れる美しい和音の最大スコアg(i)g(i)がDPで求まるので、これを事前に求めておいて、g(i)g(i)xxより小さくなったら枝刈りしてよい。これで満点が取れた。


でも、計算量がよくわからない。

2020年5月21日木曜日

ふか杯 5th contest E - すごろく

f(i):=f(i) := マスiiからゴールするまでにサイコロを振る回数の期待値、として降順にDPしたいが、振り出しに戻る場合の扱いを考えなくてはならない。

通常、ループを含む確率DPは左辺と右辺の両方にf(i)f(i)が登場する式が立式できて、f(i)=...f(i) = ...となるように整理すると遷移がわかるはずだが、この場合は紙の上で式変形するのは難しそうに見える。そこで、f(1)f(1)の係数も別にDPで求めることにする。つまり、f(i)=a(i)+b(i)f(1)f(i) = a(i) + b(i)f(1)と表して、a,ba, bを降順で求める。

xi>0x_i>0なら

a(i)=a(i+xi) a(i) = a(i + x_i) b(i)=b(i+xi) b(i) = b(i + x_i)

xi=0x_i = 0なら

a(i)=16(a(i+1)+...+a(i+6))+1 a(i) = \frac{1}{6}(a(i+1) + ... + a(i+6)) + 1 b(i)=16(b(i+1)+...+b(i+6)) b(i) = \frac{1}{6}(b(i+1) + ... + b(i+6))

xi=1x_i = -1なら

a(i)=0 a(i) = 0 b(i)=1 b(i) = 1

これでa(1),b(1)a(1), b(1)が得られるので、答えはf(1)=a(1)/(1b(1))f(1) = a(1)/(1-b(1))と求まる。

2020年5月18日月曜日

第一回日本最強プログラマー学生選手権予選 C - Cell Inversion

区間の組み合わせの総数が求まれば、操作の順番の総数はN!N!を掛けて得られるので、前者を求めればよい。

SSを左から走査して、閉じていない区間を保持しながらDPすることを考える。つまり、

dp[x][y]:=x\operatorname{dp}[x][y] := x文字目までみて、閉じていない区間がyy個あるような場合の数

というDPをする。xx文字目がWWyyが偶数、もしくはxx文字目がBByyが奇数なら、ここでyy個の(閉じていない)区間のどれかを閉じるのが正しいので、

dp[x+1][y1]+=ydp[x][y] \operatorname{dp}[x+1][y-1] \operatorname{+=} y \cdot \operatorname{dp}[x][y]

xx文字目がBByyが偶数、もしくはxx文字目がWWyyが奇数なら、さらに新しい区間を開始して反転を1増やすべきなので

dp[x+1][y+1]+=dp[x][y] \operatorname{dp}[x+1][y+1] \operatorname{+=} \operatorname{dp}[x][y]

このDPはO(N2){O}(N^2)の形になっているが、実際にやってみると各xxについて有効なyyは高々1つしかないことがわかるので、線形で書ける。


コンテスト当時にはまったくわからなくて、解説を読んでも天才解法に見えた問題だった。今見ると、いわゆる箱根駅伝DPを書けばアドホックな考察をしなくても解けてしまうことがわかる。

ただ、O(N2){O}(N^2)のDPが見えているときに、それの延長として想定解が導ける場合とかなり難しい場合があって、コンテスト中にそちらにどこまで頭を使うかは微妙なところとは思う。今出ても、ムーブ次第ではまりかねない気がする。

ABC 168 F - . (Single Dot)

縦線と横線で分割される(N+1)(M+1)(N+1)(M+1)個くらいの矩形について、隣接している矩形との連結性を調べながらBFSすればよい……のだが、なかなか通らなかった。

Ci=CjC_i = C_jのような場合の扱いが問題だった。そういうケースがありえることは承知していて、間に面積0の矩形があると考えて問題ないと思っていたのだが、(3,1)(5,1),(4,1)(6,1)(3, 1) - (5, 1), (4, 1) - (6, 1)のように縦線が重なっている場合(AiBjAjBi\Leftrightarrow A_i \le B_j \land A_j \le B_i)に、前者の左にある矩形→中間の(面積0の)矩形→右の矩形、のように本来移動できないところを迂回して移動できてしまう可能性があることに気づいた。結局、前処理の時点でつながっている線分は1本につなげてしまうことで解決した。

2020年5月17日日曜日

Google Code Jam 2020でのSBCLのトラブル その2

GCJ Round 2に出たが、Round 1に引き続いてSBCL絡みのトラブルにあってしまった。

以下の(特に意味のない)コードをSBCL 1.3.8 ~ 1.4.5に打ち込むとコンパイルが終了しない。

(defun test (n graph)
  (let ((vector (make-array n :initial-element 1)))
    (setf (aref vector 0) 0)
    (let ((value 1))
      (dotimes (i n)
        (dolist (v (list 1))
          (setf (aref vector v) value)
          (dolist (y (aref graph v))
            (print y)))))))

1.3.7→1.3.8と、1.4.5→1.4.6の変化を眺めた感じでは、make-arrayの最適化に関連して低レベルの問題が起こっていそうで、(declare (fixnum n))(declare (notinline make-array))を宣言すると問題が解消する。詳細はわかっていない。

Security Updateのvisibleをとりあえず通しておこうとして起きた現象で、この対処に時間を費やしてしまった。自分の準備にも問題があって、ローカルではバージョン1.4.14を使っていたので単純に問題の把握が遅れたというのがある。(GCJの環境は1.3.14だが、Windowsリリースはないし、CodeChefの1.3.13で問題が起きたことがないので気にしていなかった。) 本格的にWSL環境に移ることを考えたほうがよいのかもしれない。

しかし、こんな基本的なコードが落ちるのはさすがに驚く……


コンテスト後にはさすがに凹んだけれど、去年よりは確実に強くなっている。来年も頑張ろう。

2020年5月11日月曜日

CodeChef May Challenge 2020 - Chef and Bitwise Product

いったんLLRRを無視して考えると、XXYYの少なくとも一方が立っているビットはZZも1にしたほうがよいし、そうでないビットは0にするべきである。

LLRRを導入する場合にもこの考え方が使える。桁DPのように「LLにはりついている」フラグと「RRにはりついている」フラグを持ちながら、上位の桁からZZを全探索することを考える。両フラグがオフになった枝は残りをどう選んでもLLより大きくRRより小さいことが確定するので、残りの桁は上の考え方によって一通りに決めることができる。探索する枝は桁数に対して高々線形になる。

CodeChef May Challenge 2020 - Not a Real World Problem

部分点について。N15N \le 15なら全探索でよい。H=2H = 2なら縦2マスの符号の状態(4通り)を持ちながら左から右にDPすればよさそう。

一般の場合について。燃やす埋めるでできたら話が早いと思ってyosupoさんの記事を眺めると、できそうだ。異なる色に塗ると罰金がかかるタイプの問題であることがポイントか。

以下、yosupoさんのフレームワークに従って考える。粒子iiを正に塗る、負に塗るという言い方を使う。また、粒子iiの載っているマスに書かれている数をhih_iとする。

  • hi0h_i \ge 0の場合: iiを正に塗るとpihip_ih_i円得られる。負に塗るとpihip_ih_i円失う。→ 最初からpihip_ih_i円持っているとする。iiを負に塗ると2pihi2p_ih_i円失う。
  • hi<0h_i < 0の場合: iiを正に塗るとpihip_ih_i円失う。負に塗るとpihip_ih_i円得られる。→ 最初からpihip_ih_i円持っているとする。iiを正に塗ると2pihi2p_ih_i円失う。
  • 粒子i,ji, jが隣り合っているとき、iiを正(負)に、jjを正(負)に塗るとpipjp_ip_j円得られる。iiを正(負)に、jjを負(正)に塗るとpipjp_ip_j円失う。→ 最初からpipjp_ip_j円持っているとする。iiを正(負)に、jjを負(正)に塗ると2pipj2p_ip_j円失う。

これをそのままグラフにして、最大フローを流せばよい。


ほとんどのhy,xh_{y, x}に意味がないのはなんだったんだろう…… 誤読を疑って何度も確認してしまった。

CodeChef May Challenge 2020 - Triple Sort

3つの数のシフト操作は2つの互換でできていて、つまり偶置換なので、与えられた置換が奇置換だとどうやっても1,2,...,N1, 2, ..., Nに戻すことはできない。

偶置換の場合について具体的な手順を与えるために、与えられた置換を巡回置換に分解して考える。長さk(3)k(\ge 3)の巡回置換(p1p2...pk)(p_1 p_2 ... p_k)があるとき、p1,p2,p3p_1, p_2, p_3に操作を適用すると先頭の2つが正しい場所に収まって長さk2k-2の巡回置換(p3...pk)(p_3 ... p_k)が残る。k=3k=3の場合はこの操作ですべての数が正しい場所に戻るので、結局kkが奇数ならこの操作を最後まで繰り返せることになる。いっぽうkkが偶数の場合は、長さ22の巡回置換、つまり互換が残るので、これらの処理を考える。2組の互換(p1p2),(p3p4)(p_1 p_2), (p_3 p_4)がある時、操作p1,p2,p3p_1, p_2, p_3p2,p3,p4p_2, p_3, p_4を順に適用することで、4つの数がすべて正しい位置に戻る。こうして2組ずつ処理していったとき、偶置換であれば最後に端(=1つの互換)が残ることもないので、けっきょく常に1,2,...,N1, 2, ..., Nに戻せることになる。また、操作1回につき2つの数を正しい位置に戻せるので、操作回数の上限も満たせることがわかる。

CodeChef May Challenge 2020 - Sorting Vases

M=0M=0なら巡回置換に分解して数える典型問題。また、N=5N=5程度なら各順列を頂点として0-1BFSで探索すればよい。

満点について。まず、2つのロボットが与えられたときに、「少なくともいっぽうのインデックスが等しい」という関係が同値関係になっていることに注目する。このため、NN個のインデックスはノーコストで自由に行き来できるまとまりに分割できる。

具体例を挙げて考える:

1
9 5
5 8 2 4 6 7 9 3 1
2 5
5 6
3 4
7 8
8 9

この入力をグラフで図示すると次のような感じになる:

sortvs1

各頂点は、コスト0で相互に行き来できるインデックスを1つにまとめたものである。また、辺ABA \rightarrow BAAに含まれる場所からBBに含まれる場所にちょうど1つの数を移動させなければならないことを表す。たとえば55は最初インデックス11にあって、インデックス55に移動させたいので、頂点11から頂点5(,2,6)5(, 2, 6)に辺を張っている。

こうしてグラフにしてみると、M=0M=0の場合と考え方はあまり変わらないことに気づく。つまり、このグラフ上の長さkkの閉路はk1k-1回のスワップで解消できるので、グラフをいくつかの(辺素な)閉路に分割する問題であることがわかる。また、必要なスワップの回数は閉路の総数のみによって決まることも変わらない。

さて、閉路の総数だが、下のグラフのように分割方法によって異なる場合がある。

sortvs2

このグラフでは、長さ2の閉路を3つ選ぶことも長さ3の閉路を2つ選ぶこともできる。したがって、スワップの回数を減らすために、できるだけ多くの閉路に分割する方法を考えなければならない。ここまで考察を進めるとbitDPの形が見えた:

dp[S][x][y]:=\operatorname{dp}[S][x][y] := 辺の集合SSを使用済みで、いま作りかけている閉路(まだ閉じていない)の始点がxx、終点がyyである時の、今までに作った閉路の総数の最大値。

遷移がO(N){O}(N)なので計算量はO(N32N){O}(N^32^N)になる。オーダー的には微妙なところだが、大半の状態は無効だし、各NNがすべて1818ということもありえないので大丈夫だろう。割と余裕で通った。(計算量の評価がtightじゃないかも) 辺数がO(N){O}(N)だから隣接リストで持てばO(N22N){O}(N^2 2^N)だった。

CodeChef May Challenge 2020 - Chef and Rainbow Road

要約: a1,...,aNa_1, ..., a_Nから (1) 重複を許して選んで (2) どれも少なくとも1個以上選んで (3) 合わせてN+K1N+K-1個になるように選んで、積を取るとする。それらの総和を求めよ。

最低11個以上という条件を外してK1K-1個選ぶことにして、求まった値にa1...aNa_1...a_Nを掛ければよい。

多項式で考えるんだろうなという検討はつくが、ほとんど何も知らないのでmaspyさんの記事などを見て式を考える。とりあえず

(1+a1x+a12x2+...)(1+a2x+a22x2+...)...(1+aNx+aN2x2+...)(1+a_1x+a_1^2x^2 + ...)(1+a_2x+a_2^2x^2 + ...) ... (1+a_Nx+a_N^2x^2 + ...)

の第K1K-1項(0-based)を見ればよいことがわかる。この式は((1a1x)(1a2x)...(1aNx))1((1-a_1x)(1-a_2x)...(1-a_Nx))^{-1}と表せて、(1a1x)(1a2x)...(1aNx)(1-a_1x)(1-a_2x)...(1-a_Nx)は分割統治してFFTすればよく、逆元もFFTで求まるらしいので、K105K \le 10^5の部分点1, 2はそれで取れる。

部分点3からはこの方針では無理そう。ところで、この間のlong challengeのまったく歯が立たなかった問題についてNyaanさんが記事を書いていたのを思い出して見に行ったら、部分分数分解というワードが出てきて使えそうと思う。

1(1a1x)(1a2x)=a1(a1a2)(1a1x)+a2(a2a1)(1a2x) \frac{1}{(1-a_1x)(1-a_2x)} = \frac{a_1}{(a_1-a_2)(1-a_1x)} + \frac{a_2}{(a_2-a_1)(1-a_2x)}

と分解できるので、全部掛けると1/(1a1x)1/(1-a_1 x)の係数はa1N1/j1(a1aj)a_1^{N-1}/\prod_{j \neq 1} (a_1 - a_j)になる。対称性により1/(1aix)1/(1-a_i x)の係数はaiN1/ji(aiaj)a_i^{N-1}/\prod_{j \neq i} (a_i - a_j)であり、これが1+aix+ai2x2+...1+a_i x + a_i^2 x^2 + ...に掛かっているのでxK1x^{K-1}の係数はaiK+N2/ji(aiaj)a_i^{K+N-2}/\prod_{j \neq i} (a_i - a_j)とわかる。この値をすべてのiiについて加算すれば答えが得られる。部分点3が取れた。

満点について。aiK+N2/ji(aiaj)a_i^{K+N-2}/\prod_{j \neq i} (a_i - a_j)の分母の形を見るとヴァンデルモンド行列式やラグランジュ補間などが思い浮かぶので、ラグランジュ補間の解説を読んでいて(xa1)...(xaN)(x-a_1)...(x-a_N)を微分したものに代入すればいいと気づく。多項式の値をNN個の点について求める必要があるが、ググるとO(Nlog2N){O}(N\log^2 N)でできるらしい → 間に合った。


最終日に満点に気づいた。時間がなさすぎたのでbeetさんのライブラリを大急ぎで写経…… 長期コンテストでも終了直前に方針が立つと結局時間に追われることになるな。

CodeChef May Challenge 2020 - Binary Land

11が書いてあるマスから11が書いてあるマスへのパスの求め方だけ検討すればよい。

2地点間のパスの総数自体は単純なDPで求まる。DPの遷移はN×NN \times Nの行列で書けるので、行列積をセグメント木などに乗せればO(N3QlogQ){O}(N^3Q\log Q)で解ける。また、クエリが単調なことを利用するとSWAGが使えてO(N3Q){O}(N^3Q)になる。

満点を取るためにはもう少し頑張る必要がある。まず、遷移の行列が三重対角行列になっていることに注目すると、行列積はO(N2){O}(N^2)になる。したがって、SWAGへのpushとpopはこの計算量でできる……が、三重対角行列同士の積が三重対角行列になるわけではないので、SWAGをfoldする時の掛け算はどうしてもO(N3){O}(N^3)になってしまう。しかし、pathクエリに対して必要なのは行列そのものではなく一点の値なので、そこだけ求めるのはO(N){O}(N)で可能だった。これでO(N2Q){O}(N^2Q)になった。

これでもなかなかACできなかった。行列をなるべく陽に持たずに省略できる部分は省略して、定数倍高速化を徹底するとようやく通った。

CodeChef May Challenge 2020 - Buying a New String

たくさん解かれているのに部分点すらわからないので、書いたことのない文字列アルゴリズムだろうなと検討をつける。Aho-Corasickがいちばんそれっぽいので、貼るととりあえず部分点が取れた。

AB|A||B|個の文字列間で遷移の共通部分が多いことを利用できれば満点が取れそう。そして、そのためにはAho-Corasickをちゃんと理解する必要がありそう。しかし、ei1333さんのコードを見ていても理屈がよくわからないので、明らかな性質だけでなんとかすることにした:

  • Aho-Corasickのマッチは部分文字列の一致をひとつずつカウントする。つまり、まとめてカウントしたりはできないので、素朴にやるとマッチする文字列の長さに対してO(N2){O}(N^2)になる。とりあえず、トライ木を作ったらノード毎に重みの和をまとめておいたほうがよさそう。たぶんそれでオーダーも落ちる。
  • Aho-Corasickの状態は今いるノードだけで決まる。つまり、ノードが同じで以降の文字列が同じならその後の遷移はすべて同じ。

以上に基づくと、次の配列を作っておけばよいはず:

cumulA[i]:=A\operatorname{cumul}_A[i] := Aの区間[0,i)[0, i)を走査した後のスコア
nodeA[i]:=A\operatorname{node}_A[i] := Aの区間[0,i)[0, i)を走査した後のノード
cumulB[i]:=B\operatorname{cumul}_B[i] := Bの区間[0,i)[0, i)を走査した後のスコアを00として、そこから最後まで走査して得られるスコア。
nodeB[i]:=B\operatorname{node}_B[i] := Bの区間[0,i)[0, i)を走査した後のノード

A,BA, Bをそれぞれ[0,i1,),[i2,length(B))[0, i_1,), [i_2, \operatorname{length}(B))で切った時のスコアはcumulA[i1]\operatorname{cumul}_A[i_1]nodeA[i1]\operatorname{node}_A[i_1]からB[i2,length(B))B[i2, \operatorname{length}(B))を走査して得られるスコアを足したものになるが、この途中で、BBの位置iiを走査する際にノードがnodeB[i]\operatorname{node}_B[i]と一致していることが確認できたらcumulB[i]\operatorname{cumul}_B[i]を足して打ち切ってよい。単語の長さが高々26なので、i1i_1からそれだけ遷移すれば一致するはず。計算量はたぶんアルファベットの数をα\alphaとしてO(αAB+iSi){O}(\alpha|A||B| + \sum_i S_i)


そしてまだAho-Corasickを理解していない……

2020年5月2日土曜日

第二回 アルゴリズム実技検定

リアルタイム受験した。4時間40分かけて全完。前回に比べるとかなり苦戦した。

F

別解っぽいの:

BiB_iの降順に埋められるできるだけ早い日を埋めていく貪欲が成立する。(交換しても損しないことに注目すればよい。) このとき、next[i]:=\operatorname{next}[i] := ii日目以降(inclusive)でまだ埋めていない最も早い日、として空きを管理する。next\operatorname{next}を辿る際にpath compressionするとたぶんO(NlogN){O}(N\log N)

K

(を見るとネストの深さが+1され、)を見ると深さが-1されると考えると、文字を削除(=無視)するのは深さを変えないで次に進むことと解釈できる。したがって、次のようなDPが可能である:

dp[x][y]:=\operatorname{dp}[x][y] := 1,...,x1, ..., x文字目まで見て、深さがyyの場合の最小コスト

答えはdp[N][0]\operatorname{dp}[N][0]に等しい。O(N2){O}(N^2)

L

KK個の要素を選ぶ際、先頭からxx番目(0-based)の数の元のインデックスii(0-based)はi<ND(Kx1)i < N-D(K-x-1)を満たす必要があり、その中で最小(タイブレークはもっとも左)の数を選べばよい。優先度付きキューでできる。

M

今回いちばん大変だった問題で、最後にACした。

数列(Ci)(C_i)をいくつかつなげたものを走査して、各食事xxについての情報を累積していく。具体的には、table[x]\operatorname{table}[x]に(日付, この日付までに食べた料理xxの数, この日付までに食べたxx以外の料理の数)の日付に関する昇順配列を作って、料理xxが出るたびに情報を追加していった。こうすると、ある日付から一巡したときに食べる好みの料理の数と好みでない料理の数は二分探索でわかるので、何周するか計算すればよい。ただし、端の処理をするのがかなりつらかった……

N

平面走査する。xx座標を圧縮しておいてyy座標に関して走査した。

O

問題の条件を無視して最小全域木TTを得たとする。TTTT外の任意の辺e:=(u,v)e := (u, v)を追加すると閉路ができるが、この閉路上でもっとも重みの大きな(ee以外の)辺を除けば求める最小全域木TeT_eが得られる。(正当性は最適性条件により明らか……と言いたいところだが、そんなに簡単な証明を思いつかなかった。下に書いた。)この「辺の重みの最大値」はTT上のパスuvu - vに関するmax\maxクエリを処理すれば得られる。

やることはわかったのだが、そもそも木上のパスクエリを処理した経験があまりない。単純なオイラーツアーではmin/maxの処理ができないように見えるし、HL分解やLC木は持っていないし、Mo's algorithmではO(1){O}(1)の遷移が思いつかないし、しばらく困っていた。HL分解を調べて書こうと決めたところで、そもそも更新がないのでLCAのbinary liftingがそのままmin/maxクエリにも使えることに気づいた。やっぱり経験がないとこういうことがぱっとわからないな。

証明: eeのコストをwwとし、このコストを00に変えたグラフをG0G_0とする。また、G0G_0のMST T0T_0が与えられたとして、そのコストの総和をW0W_0とする。(path optimality conditionより)T0T_0eeを必ず含むので、T0T_0GG上でコストW0+wW_0 + wの全域木になっている。GGeeを含む全域木 TT'でこれより総コストの低いものが存在すると仮定して、総コストをW(<W0+w)W'(< W_0 + w)とする。TT'G0G_0の全域木でもあり、コストの総和はWwW'-wだが、Ww<W0+ww=W0W'-w < W_0 + w -w = W_0なので、T0T_0がMSTであるという仮定に反する。したがって、求める全域木はG0G_0のMSTであることがわかった。

あとは、上の作り方でできた全域木TeT_eG0G_0上でMSTであることを示せばよい。TeT_e外の任意の辺ee'を追加すると閉路ができるが、この閉路に含まれるee以外の辺は(TTGGにおけるpath optimality conditionより)ee'以下の重みを持つし、ee自身は重み00なので、やはりee'以下の重みを持つ。したがって、TeT_eG0G_0上でpath optimality conditionを満たす。\square

後半は有名事実だと思うけど若干雑…… 書き直すかも