Day 172-Q1 — 2026-10-08 | 根付き木の赤青 Hackenbush(部分木の値を「一番簡単な数」の規則で下から合成し、既約分数で出力する)

2026-10-08 赤色 Master (Phase 8+) ★★★★★★★★★ ゲーム理論(焦点帯 M1)。Day171-Q1 は茎(一本の鎖)で、値は 2 進展開を読むだけだった。今回は枝分かれする木。茎の規則は使えず、「辺 1 本を部分木の下に足したら値がどう変わるか」という関数を作って下から合成する。Day171-Q1 の「発展1」をそのまま本問にした

問題

頂点 $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 の指数」を持つことが本体になる。

概念図

入力例 2: 部分木の値 x を下から合成する(青い辺は f_B、赤い辺は −f_B(−x)) 地面 R: −1R: −1頂点 2 の上の値 x = −1 + (−1) = −2青い辺: f_B(−2)k=4 (−2+4>1) → 2/2^3 = 1/4青い辺: f_B(0) = 1 根の値 = 1/4 + 1 = 5/4 > 0 → どちらが先手でも Blue の勝ち (青 2 本・赤 2 本なので、本数の差で決めると「後手勝ち」と誤る) 青い辺を 1 本下に足す f_B(x) xf_B(x) 1/23/2 01 −1/23/4 −11/2 −21/4 −31/8 x<0 では分母が 2^(k−1) 倍に増える

ヒント(クリックで段階的に開示)

ヒント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 解説

1局面の値を「左の選択肢 | 右の選択肢」で書く

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$。

2木は「根から出る辺ごとの和」

根から出る辺どうしは、どちらを切っても他方に影響しない。独立なゲームの和なので値は和になる。頂点 $v$ の上の部分も同じで、$v$ から出る辺ごとの値を足したものが「$v$ の上の値 $x$」になる。

3辺 1 本を下に足す関数 $f_B$

$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(実測)。

4分母が大きくなるので整数 2 つで持つ

$f_B$ は $x<0$ のたびに分母を $2^{k-1}$ 倍する。深さ $5\times10^4$ の道で色が交互だと、分母は約 $2^{5\times10^4}$(10 進で 1.5 万桁)になる。値を $(a,e)$=$a/2^e$ で持ち、Python の任意精度整数でそのまま計算する。約分は「分子が偶数のときだけ」末尾の 0 を落とせば足りる(奇数なら既約)。

5出力

分母が 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 進出力で ValueErrorPython 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、例 2 5/4 Blue Blue、例 3 0 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$ を通す表現、原典の確認

自己評価

自分の回答

気づき・メモ