問題
頂点 $1$ が地面に固定された、頂点数 $N$ の根付き木がある。頂点 $i\ (2\le i\le N)$ の親は $p_i$ で、辺 $(p_i, i)$ の色は文字列 $C$ の $i-1$ 文字目(B は青、R は赤)である。
Blue と Red が交互に手番を持つ。Blue は青い辺を 1 本、Red は赤い辺を 1 本選んで切る。切った辺より上(根から遠い側)の辺は地面から離れるので、色を問わずすべて取り除かれる。手番で切れる辺が無いプレイヤーの負けである。
この局面の値(超現実数。必ず分母が 2 冪の有理数になる)を既約分数で出力し、2 行目に「Blue が先手のときの勝者」と「Red が先手のときの勝者」を空白区切りで出力せよ。値が整数ならそのまま、そうでなければ p/q($q$ は 2 冪、$p$ は奇数、負なら $p$ に符号)の形にする。
入力形式
N
p_2 p_3 ... p_N
C
制約
| # | 条件 |
|---|---|
| 1 | $2\le N\le 5\times10^4$ |
| 2 | $1\le p_i<i$ |
| 3 | $C$ は B と R からなる長さ $N-1$ の文字列 |
入出力例
入力例1
3
1 2
BR出力例1
1/2
Blue Blue(茎 BR。Day171-Q1 の規則でも $1-\frac12=\frac12$)
入力例2
5
1 2 2 1
BRRB出力例2
5/4
Blue Blue(頂点 2 への青い辺の上に、赤い辺が 2 本枝分かれしている。上の値 $-2$ に青い辺を足すと $\frac14$。もう 1 本の青い辺 $+1$ と合わせて $\frac54$。青と赤は 2 本ずつで、本数の差で決めると「後手勝ち」と誤る)
入力例3
7
1 2 2 1 5 5
BRRRBB出力例3
0
Red Blue(入力例 2 の左の木 $\frac14$ と、その色を反転した木 $-\frac14$ を並べた。値 0 は後手勝ち。3 例とも全状態探索で値と勝者を確認)
背景と出典
パルチザンゲームの値の理論(Conway の On Numbers and Games、Berlekamp・Conway・Guy の Winning Ways)では、赤青 Hackenbush の局面は数になり、和の値は各成分の値の和になる。木については次の規則があると私は記憶している(本セッションでは原典を参照していない。規則は全状態探索との照合で確かめた)。
- 部分木の値が $x$ のとき、その下に青い辺を 1 本足した値 $f_B(x)$ は
- $x\ge0$ なら $x+1$
- $x<0$ なら、$x+k>1$ となる最小の正整数 $k$ を取って $\dfrac{x+k}{2^{k-1}}$
- 赤い辺は色を反転して $f_R(x)=-f_B(-x)$
- 頂点から上に複数の辺が出ていれば、それぞれの「辺+部分木」の値を足す
例: $f_B(-1)=\frac{-1+3}{2^2}=\frac12$(茎 BR)、$f_B(-2)=\frac{-2+4}{2^3}=\frac14$(入力例 2)。
本問は自作。値の分母は辺が深いほど大きくなり、$2^{N}$ 程度の桁を扱う。浮動小数点は使えず、任意精度整数で「分子 / 2 の指数」を持つことが本体になる。
概念図
ヒント(クリックで段階的に開示)
ヒント1(方向性)
Grundy 数は使えない(手の集合が手番で違う)。局面の値は $\{\text{Blue の行き先の値}\mid\text{Red の行き先の値}\}$ の間で「一番簡単な数」。辺 1 本だけの木、B の上に R が 2 本の木、で行き先を書き出し、値がどうなるか確かめる。
ヒント2(アプローチ)
木の値は「根から出る辺ごとの値の和」。辺 $(p,v)$ の値は、$v$ より上の部分木の値 $x$ だけで決まる(辺の色を $c$ として $f_c(x)$)。下から(葉から根へ)$x$ を合成すればよい。$f_B$ の形は、$x$ を $-3,-2,-1,-\frac12,0,\frac12$ と動かして全探索の値を並べると見えてくる。
ヒント3(誘導)
値を「整数 $a$ と指数 $e$ で $a/2^e$」と持つ。$f_B$ は
if a >= 0: a += 1 << e # x + 1
else:
k = ((1 << e) - a >> e) + 1 # x + k > 1 となる最小の k
a += k << e; e += k - 1 # (x + k) / 2^(k-1)
親へ足すときは指数を大きい方にそろえ、分子が偶数なら末尾の 0 を落として既約にする。BFS 順の逆にたどれば再帰は要らない。
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
def main():
sys.set_int_max_str_digits(0) # 分母 2^e の 10 進表記は 4300 桁を超えうる
data = sys.stdin.buffer.read().split()
n = int(data[0])
par = [0, 0] + [int(x) for x in data[1:n]]
col = data[n] if n > 1 else b''
children = [[] for _ in range(n + 1)]
for v in range(2, n + 1):
children[par[v]].append(v)
order = [1] # BFS 順。逆順にたどると子が親より先に終わる
for v in order:
order.extend(children[v])
num = [0] * (n + 1) # 頂点 v の上に載っている値 = num[v] / 2^ex[v](既約)
ex = [0] * (n + 1)
for v in reversed(order):
if v == 1:
break
a, e = num[v], ex[v]
blue = col[v - 2] == 66 # 66 は b'B'
if not blue:
a = -a # 赤い辺は「色を反転した青い辺」として計算する
if a >= 0:
a += 1 << e # x >= 0 なら青い辺を足すと x + 1
else:
if a.bit_length() <= e: # -1 < x < 0 なら k = 2(大きな整数を作らない近道)
k = 2
else:
k = ((1 << e) - a >> e) + 1 # x + k > 1 となる最小の正整数 k
a += k << e # (x + k) / 2^(k-1)
e += k - 1
if not a & 1: # 偶数のときだけ約分(奇数なら既約)
t = min((a & -a).bit_length() - 1, e)
a >>= t
e -= t
if not blue:
a = -a
p = par[v] # 親の上の値に足し込む(指数を大きい方にそろえる)
pa, pe = num[p], ex[p]
if pa == 0:
num[p], ex[p] = a, e
continue
if pe >= e:
pa += a << (pe - e)
else:
pa = (pa << (e - pe)) + a
pe = e
if pa == 0:
pe = 0
elif not pa & 1 and pe:
t = min((pa & -pa).bit_length() - 1, pe)
pa >>= t
pe -= t
num[p], ex[p] = pa, pe
a, e = num[1], ex[1]
print(str(a) if e == 0 else f"{a}/{1 << e}")
print("Blue Blue" if a > 0 else "Red Red" if a < 0 else "Red Blue")
main()
計算量: 辺 1 本ごとに、その時点の値の桁数 $O(e/64)$ 語の整数演算をする。分母の指数 $e$ は最大で辺の本数程度まで伸びるので、最悪 $O(N^2/64)$ 語(深い道で色が交互の木)。$N=5\times10^4$ の最悪ケースで 0.43 s、$N=2\times10^5$ では 7.67 s(Python 3.14.7、M1 MacBook Pro、実測)。制約を $5\times10^4$ にしたのはこの差による。
Step-by-Step 解説
Blue(Left)が動いた先の値の集合を $G^L$、Red(Right)を $G^R$ として $G=\{G^L\mid G^R\}$。Hackenbush の赤青だけの局面ではすべての $G^L<$ すべての $G^R$ が成り立ち、値は「$\max G^L$ と $\min G^R$ の間で一番簡単な数」になる(間に整数があれば絶対値が最小の整数、無ければ分母の 2 冪が最小の数)。
例(入力例 2 の左の木): Blue が青い辺を切ると $0$。Red が赤い辺を 1 本切ると「B の上に R 1 本」$=\frac12$。$\{0\mid\frac12\}=\frac14$。
根から出る辺どうしは、どちらを切っても他方に影響しない。独立なゲームの和なので値は和になる。頂点 $v$ の上の部分も同じで、$v$ から出る辺ごとの値を足したものが「$v$ の上の値 $x$」になる。
$x\ge0$ のとき、Blue はその辺を切って 0 にでき、Red の手はすべて上の部分での手なので、値は $x+1$。$x<0$ のときは、上の部分で Red が強いぶん、下の青い辺の価値が半分ずつ削られる。$x=-1$ で $\frac12$、$x=-2$ で $\frac14$、$x=-\frac12$ で $\frac34$。一般形が $\frac{x+k}{2^{k-1}}$($k$ は $x+k>1$ の最小の正整数)。この式を、辺 2〜12 本のランダムな木 856 局面で全状態探索の値と照合し、不一致 0(実測)。
$f_B$ は $x<0$ のたびに分母を $2^{k-1}$ 倍する。深さ $5\times10^4$ の道で色が交互だと、分母は約 $2^{5\times10^4}$(10 進で 1.5 万桁)になる。値を $(a,e)$=$a/2^e$ で持ち、Python の任意精度整数でそのまま計算する。約分は「分子が偶数のときだけ」末尾の 0 を落とせば足りる(奇数なら既約)。
分母が 1 なら整数、そうでなければ a/2^e。10 進文字列化が 4300 桁を超えるので sys.set_int_max_str_digits(0) が要る。勝者は値の符号: 正なら Blue、負なら Red、0 なら後手。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 局面の全状態探索(部分集合でメモ化し「一番簡単な数」を計算) | 状態数が指数 | 検算用のみ | 辺 12 本程度まで。規則の確認に使用(856 局面) |
fractions.Fraction で $f_B$ を計算 | 演算ごとに gcd | 不採用 | $N=5\times10^4$ の交互の道で 4.36 s、毛虫型で 3.37 s(実測。本解は 0.43 s・0.35 s) |
倍精度 float | $O(N)$ | 不採用 | $2^{-60}$ の差が消えて勝者を誤る(下の「よくあるミス」) |
| 整数 2 つ $(a,e)$ で持ち、偶数のときだけ約分(本解) | 最悪 $O(N^2/64)$ 語 | 採用 | 最悪 0.43 s($N=5\times10^4$) |
毎回 a & -a で約分する素直な版 | 同じ | 不採用 | $N=2\times10^5$ の交互の道で 24.07 s。偶数のときだけ約分+$k=2$ の近道で 7.67 s(実測) |
前提が変わったら:
- $N=2\times10^5$ が必要なら、値を 2 進の符号展開(sign expansion)のまま持ち、$f_B$ を「先頭に + を足して以降を書き換える」操作として表す方向がある。本セッションでは実装・計測していない
- 緑の辺(どちらも切れる)が入ると値は数にならない($*$ が出る)。数+ニンバーの組や、一般には温度理論が要り、本問の関数は使えない
- 勝者だけでよいなら、値そのものは要らないが、符号を決めるには結局ほぼ同じ精度が要る(入力例の $\frac14-\frac14$ のように打ち消し合う)
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 青と赤の本数の差で勝者を決める | 辺の重みが同じだと思う | 856 局面中 163 局面で勝者を誤答(実測)。入力例 2 が反例 |
| $x<0$ のとき 1 回だけ半分にする($\frac{x+k}{2}$) | 茎の「上に行くほど半分」を 1 段で済ませる | 値は 856 局面中 395 局面、勝者は 45 局面で誤答(実測)。分母は $2^{k-1}$ |
| 茎の規則(2 進展開を読む)を木に使う | 枝分かれを無視 | 枝分かれすると「一番簡単な数」の再計算が要る。入力例 2 は茎に分けて読めない |
float で計算 | 53 ビットを超える分母が消える | 根に R・B・「B の上に R が 60 本」を並べた木(値 $2^{-60}$)で、float は 0.0 になり Red Blue(後手勝ち)と誤る(実測) |
10 進出力で ValueError | Python 3.11 以降の整数→文字列の桁数制限(既定 4300 桁) | sys.set_int_max_str_digits(0) |
次のステップ
発展1: $N=2\times10^5$ を、符号展開(sign expansion)による表現で通す
- 発展2: 緑の辺を混ぜた木(数+ニンバー)。先手必勝の局面が現れることを全探索で確かめる
- 発展3: 原典(Winning Ways の Hackenbush の章、木の規則)で $f_B$ の式と証明を確かめる(本セッションでは未確認)
検証ログ(本セッションで実行した数値)
全状態探索との照合(実測): 辺 1〜12 本のランダムな木(茎・浅い木・一般の木、40% は「木とその色反転を根に並べた」値 0 狙い)856 局面で、出力した値・勝者とも不一致 0。勝者の内訳は Blue 278・Red 307・後手勝ち 271、非整数の値 392 局面。勝者は値を使わない直接のミニマックスでも求め、「値の符号で勝者が決まる」こと自体も 856 局面で確認
- 誤った方法(実測、同じ 856 局面): 本数の差 163 局面誤答、$\frac{x+k}{2}$ は値 395・勝者 45 局面誤答、float は上記の 64 頂点の木で誤答
- 入力例(実測): 例 1
1/2 Blue Blue、例 25/4 Blue Blue、例 30 Red Blue(全探索と一致) - 実行時間(実測、$N=5\times10^4$、負荷平均 13〜16): 交互の道 0.43 s(出力 3.0 万文字)、
Bの上に赤い道 0.12 s、ランダムな木 0.17 s、深さの浅いランダム 0.25 s、毛虫 0.35 s、青い背骨に赤い星を 400 本ずつ 0.31 s。Fraction 版は交互の道 4.36 s・毛虫 3.37 s。$N=2\times10^5$ では本解でも交互の道 7.67 s・毛虫 4.21 s - 扱っていない範囲: 一般グラフ(閉路あり)の赤青 Hackenbush、緑の辺、$N=2\times10^5$ を通す表現、原典の確認