Day 175-Q1 — 2026-10-11 | 赤青 Hackenbush の木を N=2×10^5 で(値を符号展開のまま持ち、和は短い側の桁数だけ書き換える)

2026-10-11 赤色 Master (Phase 8+) ★★★★★★★★★ ゲーム理論(焦点帯 M1)。Day172-Q1 は木の値を「分子 / 2^指数」の多倍長整数で持ち、$N=5\times10^4$ に絞った($N=2\times10^5$ の交互の道で 7.67 s)。今回は値を符号展開(+ と − の列)で持つ。辺を 1 本下に足す操作は「先頭に 1 文字付ける」だけになり、重いのは枝分かれでの足し算だけになる。足し算は短い方の桁数ぶんだけ長い方の先頭を書き換えればよく、長い方を土台にする(small-to-large)と全体 $O(N\log N)$ 文字の操作で済む

問題

頂点 $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 が先手のときの勝者」を空白区切りで出力せよ。

符号展開とは、0 から始めて「+ なら右の子、− なら左の子」と超現実数の木(誕生日の木)を下り、値に着くまでの道順を + と - の列で書いたものである。例: $1=$ +、$\frac12=$ +-、$\frac34=$ +-+、$-\frac14=$ -++。値が 0 なら 0 と出力する。

入力形式

N
p_2 p_3 ... p_N
C

制約

  • $2\le N\le 2\times10^5$
  • $1\le p_i<i$
  • $C$ は B と R からなる長さ $N-1$ の文字列

入出力例

入力例1

3
1 2
BR

出力例1

+-
Blue Blue

(茎 BR。値 $\frac12$)

入力例2

5
1 2 2 1
BRRB

出力例2

++--
Blue Blue

(値 $\frac54=2-\frac12-\frac14$。Day172-Q1 の入力例 2 と同じ木)

入力例3

7
1 2 3 4 5 1
BRRRRR

出力例3

-+---
Red Red

(左は茎 BRRRR(値 $\frac1{16}$、符号展開 +----)、右は赤い辺 1 本($-1$)。和 $-\frac{15}{16}$ の符号展開は -+---。長い方 +---- の先頭 2 文字だけが -+ に書き換わり、後ろの --- はそのまま残る。3 例とも全状態探索と一致)

---

背景と出典

  • 符号展開(sign expansion): Conway の *On Numbers and Games* で、超現実数を + と − の列で表す方法。赤青 Hackenbush の茎(一本の鎖)は、根から順に青を +、赤を − と読んだ列がそのまま値の符号展開になる(Day171-Q1 の規則「最初の同色の並び ±k、その後は ±2^-i」は、この列を数に直す式)。本セッションでは原典を参照していない
  • 辺を下に足す関数: Day172-Q1 で $f_B(x)=x+1\ (x\ge0)$、$\frac{x+k}{2^{k-1}}\ (x<0)$ を全状態探索で確かめた。これは「$x$ の符号展開の先頭に + を付ける」ことと同じである(Step 2 で示す)。値が $x$ の部分木は、符号展開が同じ茎と同じ値なので、その下に青い辺を足せば「青+その茎」の茎になる、と考えても分かる
  • 本問は自作。Day172-Q1 の「前提が変わったら」と「発展1」($N=2\times10^5$ を符号展開で通す)をそのまま本問にした

概念図

入力例 3: 木 → 値の符号展開。和では長い方の先頭だけ書き換わる地面(頂点 1)BRRRRR(値 −1)茎 BRRRR = 1/16符号展開 +----x(土台)+----並び r=1(+)/小数部 d = − − − −y(足す側)-y の小数部 k=0 文字 → x の先頭 r と d₁ だけ数に直すx + y-+---後ろ 3 文字はそのまま書き換えz₀ = 1 + (−1) + (−1)·2⁻¹ = −1/2 → 符号展開 "-+"値: −1 + 1/2 − 1/4 − 1/8 − 1/16 = −15/16

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

ヒント1(方向性)

Day172-Q1 の $f_B$ を、$x$ の符号展開で書き直してみる。$x=-1$(-)→ $\frac12$(+-)、$x=-2$(--)→ $\frac14$(+--)、$x=-\frac12$(-+)→ $\frac34$(+-+)。

ヒント2(アプローチ)

辺 1 本は「先頭に 1 文字」で $O(1)$。残る問題は頂点での和。$y$ の分母が $2^k$ なら、$x+y$ と $x$ の符号展開は「小数部の $k+2$ 文字目以降」が一致する。だから長い方 $x$ の先頭 $k+1$ 文字だけを数に直して $y$ を足し、符号展開に戻して差し替えればよい。

ヒント3(誘導)
  • 符号展開を「先頭の同符号の並び(符号と長さ $r$)」と「小数部の列 $d_1d_2\dots$($d_j=\pm1$、値 $\pm2^{-j}$)」に分けて持つ。$r$ は数で持つ(整数部が大きい木で列を伸ばさないため)
  • 小数部は逆順の bytearray(先頭が末尾)で持つと、先頭の書き換えが del と extend で済む
  • 頂点では、子のうち小数部が一番長いものを土台にして、他の子を足す

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys

TO_SIGN = bytes.maketrans(b'10', b'+-')


def canon(z, k):
    # z / 2^k(k>0 なら z は奇数)の符号展開を (先頭の並びの符号, 並びの長さ, 小数部の符号列) で返す
    # 小数部は '1'=+ / '0'=- の bytes で、位 1/2 が先頭
    if k == 0:
        return (1 if z >= 0 else -1), abs(z), b''
    neg = z < 0
    if neg:
        z = -z
    m = z >> k
    f = z - (m << k)                       # 1..2^k-1 の奇数
    digits = format((f - 1) >> 1, '0%db' % k).encode()
    if neg:
        return -1, m + 1, digits.translate(bytes.maketrans(b'01', b'10'))
    return 1, m + 1, digits


def exact(o):
    # 符号展開 o を (分子, 指数) に。小数部の長さ k が指数
    sg, r, d = o
    k = len(d)
    if k == 0:
        return sg * r, 0
    pm = int(bytes(d[::-1]), 2)
    return (sg * r << k) + 2 * pm - (1 << k) + 1, k


def add(x, y):
    # x に y を足す(x を書き換える)。x の小数部が長いときは先頭 e+1 桁だけ作り直す
    ya, k = exact(y)
    sg, r, d = x
    if len(d) <= k + 1:
        xa, kx = exact(x)
        K = max(kx, k)
        z = (xa << (K - kx)) + (ya << (K - k))
        if z == 0:
            x[0], x[1], x[2] = 1, 0, bytearray()
            return
        t = min((z & -z).bit_length() - 1, K)
        nsg, nr, nd = canon(z >> t, K - t)
        x[0], x[1], x[2] = nsg, nr, bytearray(nd[::-1])
        return
    e = k
    if e:
        pm = int(bytes(d[-1:-e - 1:-1]), 2)
        xe = (sg * r << e) + 2 * pm - (1 << e) + 1
    else:
        xe = sg * r
    s = 1 if d[-e - 1] == 49 else -1       # 49 は b'1'(位 2^-(e+1) の符号)
    z = 2 * xe + 2 * ya + s                # 奇数。z / 2^(e+1) が新しい前半の値
    del d[-e - 1:]
    nsg, nr, nd = canon(z, e + 1)
    d.extend(nd[::-1])
    x[0], x[1] = nsg, nr


def push_edge(o, c):
    # 辺を 1 本下に足す = 符号展開の先頭に c(+1 青 / -1 赤)を付ける
    sg, r, d = o
    if r == 0 or sg == c:
        o[0] = c
        o[1] = r + 1
    else:
        d.extend((b'1' if sg > 0 else b'0') * r)
        o[0] = c
        o[1] = 1


def main():
    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]
    for v in order:
        order.extend(children[v])
    val = [None] * (n + 1)
    for v in reversed(order):
        ch = children[v]
        if not ch:
            o = [1, 0, bytearray()]
        else:
            best = max(ch, key=lambda c: len(val[c][2]))
            o = val[best]
            for c in ch:
                if c != best:
                    add(o, val[c])
                    val[c] = None
            val[best] = None
        if v != 1:
            push_edge(o, 1 if col[v - 2] == 66 else -1)
        val[v] = o
    sg, r, d = val[1]
    if r == 0:
        print(0)
        print("Red Blue")
        return
    s = (b'+' if sg > 0 else b'-') * r + bytes(d[::-1]).translate(TO_SIGN)
    sys.stdout.write(s.decode() + '\n')
    print("Blue Blue" if sg > 0 else "Red Red")


main()

計算量: 辺 1 本は $O(1)$(並びの符号が変わるときは並びの長さ $r$ ぶん小数部へ移すが、その文字数は以後の符号展開の長さとして残るので、全体で文字数の総和に抑えられる)。頂点での和は「短い方の小数部の長さ $+1$」に比例する。土台を小数部が最長の子にすると、足される側の長さはその部分木の辺数以下で、軽い子(部分木の大きさが親の半分以下)に $O(\log N)$ 回ずつ課金できるので、全体 $O(N\log N)$ 文字。$N=2\times10^5$ の 10 種の木で 0.26〜0.95 s(Python 3.14.7、M1 MacBook Pro 8GB、負荷平均 10〜18 での実測、起動込み)。

Step-by-Step 解説

1符号展開を数に直す

符号展開 $s_1s_2\cdots s_L$ の先頭の同符号の並びの長さを $r$、符号を $\sigma$ とすると、値は

$$x=\sigma r+\sum_{j=1}^{L-r}d_j2^{-j}\qquad(d_j=s_{r+j}\in\{+1,-1\})$$

例: +-+ は $r=1$、$d=(-1,+1)$ で $1-\frac12+\frac14=\frac34$。逆向き(数 → 符号展開)は、$z=a/2^k$($a$ 奇数、$k\ge1$)、$z>0$ なら $r=\lfloor z\rfloor+1$、小数部 $f=z-\lfloor z\rfloor$ について $d_j$ は「$(f\cdot2^k-1)/2$ の $k$ 桁の 2 進表記の $j$ 桁目が 1 なら +」。これで小数部はちょうど $k$ 文字になる。

2辺を下に足す = 先頭に 1 文字

$x$ の先頭に + を付けた列の値を、Day172-Q1 の $f_B$ と比べる。

  • $x\ge0$(並びが + か空): 並びが 1 伸びるだけなので $x+1$。$f_B$ と一致
  • $x<0$(並びが − で長さ $r$、小数部の値 $\delta\in[0,1)$): 新しい列は +、- が $r$ 個、元の小数部。値は $1-\sum_{i=1}^{r}2^{-i}+2^{-r}\delta=2^{-r}(1+\delta)$。一方 $x=-r+\delta$ で、$\delta>0$ なら $k=r+1$、$f_B(x)=\frac{1+\delta}{2^r}$、$\delta=0$ なら $k=r+2$、$f_B(x)=2^{-r}$。一致する

赤い辺は − を付ける。実装では、並びの符号と付ける文字が逆のとき、古い並び $r$ 文字を小数部の先頭へ移す。

3和は先頭だけ書き換える

$y$ の小数部の長さを $k$($y$ の分母が $2^k$)、$x$ の小数部を $d_1\dots d_L$($L\ge k+1$)とする。

$$z_0=\Bigl(\sigma r+\sum_{j\le k}d_j2^{-j}\Bigr)+y+d_{k+1}2^{-(k+1)}$$

は分母がちょうど $2^{k+1}$ の数(整数でない)。整数でない数 $z_0$ の木の上の子は $z_0\pm2^{-(k+2)}$ で、その下も「1 段ごとに半分」で進む。$x+y=z_0+\sum_{j\ge k+2}d_j2^{-j}$ なので、$x+y$ の符号展開は「$z_0$ の符号展開(小数部 $k+1$ 文字)+ $d_{k+2}d_{k+3}\dots$」になる。書き換えるのは先頭 $k+1$ 文字と並びの長さだけ。$L\le k+1$ なら両方を数に直して足す(どちらも $O(k)$)。

4どの子を土台にするか

足し算の手間は足される側 $y$ の小数部の長さで決まる。だから小数部が一番長い子を土台にする。逆に「一番短い子を土台」にすると、毛虫(背骨+葉)で毎回長い背骨側を足すことになり、$N=2\times10^5$ で 27.5 s かかった(実測。本解は 0.68 s)。

5出力

根の値の符号展開をそのまま出す。勝者は値の符号(先頭の文字): + なら Blue、− なら Red、空(値 0)なら後手。出力される列は「その木と同じ値の茎」でもある。

迷った他案と採らなかった理由

案計算量採否理由
局面の全状態探索指数検算用のみ辺 12 本まで(500 局面)
分子 / 2^指数 で持つ(Day172-Q1)最悪 $O(N^2/64)$ 語不採用交互の道で 4.43 s、深い木 3.52 s、二分割を繰り返す木 2.58 s($N=2\times10^5$、実測)
符号展開で持つが、土台を最短の子にする最悪 $O(N^2)$不採用毛虫 27.5 s、深い木 26.9 s(実測)
符号展開を Python の str で持ち、毎回 '+' + s で付ける辺ごとに $O(L)$不採用文字列は不変なので、交互の道で $O(N^2)$ 文字のコピー(本セッションでは未計測)
符号展開+逆順 bytearray+最長の子を土台(本解)$O(N\log N)$ 文字採用10 種の木で 0.26〜0.95 s

前提が変わったら

  • 値の 10 進の分数表記が必要なら、符号展開から分子を作ると $2^{2\times10^5}$ 級の整数の 10 進変換が要る。Python 3.12 以降は大きな整数の 10 進変換が準 2 次になったと記憶しているが、本セッションでは計測していない
  • 緑の辺が入ると値は数にならない($*$ が出る)。符号展開では表せないので、本問の表現は使えない
  • 木でなく閉路を含むグラフなら、辺の削除で上の部分が一度に落ちる構造が崩れ、部分木の値の和に分けられない

よくあるミス

ミス原因正しい書き方
土台を先頭の子(や最短の子)にするsmall-to-large を忘れる小数部が最長の子を土台に。逆にすると 27.5 s(実測)
和で $x$ の先頭 $k$ 文字だけ書き換える1 文字足りない$d_{k+1}$ まで含めて数に直し、分母を $2^{k+1}$ ちょうどにする。$k$ 文字だと $z_0$ が整数になりうる
並びの長さ $r$ を列のまま持つ整数部も「文字」と思う赤い葉が 10^5 本並ぶ星だと、和のたびに長い列を作る。$r$ は数で持つ
並びの符号と逆の文字を付けるとき、並びを捨てる+ を付けたら値が増えるだけと思う古い並び $r$ 文字は小数部の先頭に移る(Step 2)
値 0 を先手勝ちにする数のゲームで 0 は後手勝ち値 0 なら Red Blue

次のステップ

  • 発展1: 各頂点の部分木の値の符号展開の長さ(誕生日)を全部出す。$O(N\log N)$ の範囲で収まるか
  • 発展2: 辺を 1 本切った後の値を $Q$ 回聞かれる問題(切る位置ごとに根までの和を直す)
  • 発展3: 原典(*On Numbers and Games* の符号展開、*Winning Ways* の Hackenbush の章)で、茎の値=符号展開の主張を確かめる(本セッションでは未確認)

検証ログ(本セッションで実行した数値)

  • 全状態探索との照合(実測): 辺 1〜12 本のランダムな木(一般・深い木・「木とその色反転を根に並べた」値 0 狙い)500 局面で、符号展開と勝者が一致。不一致 0。内訳は値 0 が 88 局面、整数でない値が 265 局面
  • Day172-Q1 の方式との照合(実測): 辺 1〜400 本のランダムな木 500 局面で一致。$N=5\times10^4$ で 10 種(ランダム、交互の道、色ランダムの道、深い木、毛虫、箒 400・2000、櫛 20・300、浅い木)と、二分割を繰り返す木・直前 50 頂点に付ける木の計 12 入力で出力が一致
  • 実行時間(実測、$N=2\times10^5$、起動込み、本解/Day172-Q1 の方式): 交互の道 0.31/4.43 s(出力 20 万文字)、ランダム 0.95/0.83 s、深い木 0.40/3.52 s、毛虫 0.68/1.57 s、箒 400 0.41/0.35 s、櫛 20 0.34/0.35 s、櫛 300 0.40/0.35 s、浅い木 0.74/0.53 s、二分割を繰り返す木 0.26/2.58 s、直前 50 頂点 0.42/0.27 s。負荷平均 10〜18 の機械なので ±30% 程度ぶれる
  • 土台の選び方の比較(実測): 最短の子を土台にすると毛虫 27.53 s、深い木 26.93 s、二分割を繰り返す木 0.70 s(出力は同じ)
  • 扱っていない範囲: 10 進の分数表記での出力、緑の辺、閉路のあるグラフ、原典の確認

自己評価

自分の回答

気づき・メモ