Day 180-Q5 — 2026-10-16 | K 乗(u^K)の出現数と、異なる K 乗の数を runs と接尾辞配列のランクで厳密に(Day177-Q5 のハッシュによる重複除去を、ランクの区間の LCP で置き換える)

2026-10-16 赤色 Master (Phase 8+) ★★★★★★★★★ 文字列(焦点帯 M4)。Day177-Q5 は平方($K=2$)を runs から数え、異なる平方は法 $2^{61}-1$ のハッシュで重複を除いた。本問は $K$ を一般にし、重複除去を「同じ長さの候補を接尾辞配列のランク順に並べ、隣どうしの区間の LCP の最小が長さ以上か」で厳密に行う。ハッシュの衝突の心配がなくなる

問題

英小文字からなる文字列 $S$(長さ $N$)と整数 $K\ge2$ が与えられる。空でない文字列 $u$ を $K$ 回並べた $u^K$ の形の部分文字列を $K$ 乗と呼ぶ。

次の 2 つを空白区切りで出力せよ。

  1. $K$ 乗の出現の数(組 $(i,\ell)$ で $S[i..i+K\ell)=S[i..i+\ell)^K$ となるものの数)
  2. 異なる $K$ 乗の数(文字列として異なるものの数)

入力形式

S K

制約

#制約
1$1\le N\le2\times10^5$
2$2\le K\le N$

入出力例

入力例1

abaabaabaab 3

出力例1

3 3

(周期 3・長さ 11 の run から、長さ 9 の 3 乗が $11-9+1=3$ か所:$(aba)^3,(baa)^3,(aab)^3$。すべて異なる)

入力例2

abaabaabaabcabaabaaba 3

出力例2

4 3

(c の後ろの run にも $(aba)^3$ が 1 つあり、出現は 4。異なるものは前の run の $(aba)^3$ と同じなので 3。run ごとに数えて足すだけだと 4 と誤る)

入力例3

aabaabaabaab 2

出力例3

12 5

(runs は周期 3・長さ 12 の全体と、長さ 2 の $aa$ が 4 本。全体の run から長さ 6 が $12-6+1=7$ か所、長さ 12($j=2$)

背景と出典

  • runs 定理(runs の数は $N$ 未満)と Lyndon 根による列挙は Bannai, I, Inenaga, Nakashima, Takeda, Tsuruta「The "Runs" Theorem」(SIAM J. Comput. 2017) と記憶している(本文は未確認)。runs の列挙のコードは Day175-Q5 のものをそのまま使った
  • Fine–Wilf の定理(周期 $p,q$ を持ち長さ $\ge p+q-\gcd(p,q)$ なら $\gcd(p,q)$ も周期)は Fine–Wilf 1965 と記憶している(原典未確認)
  • Thue–Morse 語に 3 乗が無いこと(Thue 1912、原典未確認)を、検算の 1 つに使った
  • 本問の問題文・制約・入力例は自作

概念図

入力例 2(K = 3):周期 3 の run が 2 本。3 乗の出現は 3 + 1 = 4、異なるのは 3 a 0 b 1 a 2 a 3 b 4 a 5 a 6 b 7 a 8 a 9 b 10 c 11 a 12 b 13 a 14 a 15 b 16 a 17 a 18 b 19 a 20 run (0, 11, p=3) run (12, 21, p=3) abaabaaba baabaabaa aabaabaab abaabaaba ← 前の run と同じ文字列 出現数:run ごとに Σ_j (L − 3jp + 1) = (11 − 9 + 1) + (9 − 9 + 1) = 4 run の中:開始を p ずらすと同じ文字列 → 先頭 min(p, L − 3jp + 1) 個が候補 run をまたぐ重複:候補を(長さ, 接尾辞配列のランク)で並べ、隣のランクの区間の LCP の最小 ≥ 長さなら同じ 位置 0 と 12 の abaabaaba はランクが隣り合い LCP ≥ 9 → 1 つに数える(ハッシュを使わず厳密)

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

ヒント1(方向性)

$K$ 乗 $w=u^K$($|u|=q$)は周期 $q$ を持ち長さ $Kq\ge2q$。$w$ の最小周期 $p'$ について Fine–Wilf から $p'\mid q$ が言える。だから $w$ は「周期 $p'$ の run」のちょうど 1 つに含まれる。

ヒント2(アプローチ)

run $(l,r,p)$(区間 $[l,r)$、最小周期 $p$、長さ $L=r-l\ge2p$)に含まれる $K$ 乗で最小周期が $p$ のものは、長さ $Kjp$($j=1,2,\dots$、$Kjp\le L$)で、出現は $L-Kjp+1$ 個。全部の run・全部の $j$ で足せば出現数。

ヒント3(誘導)
  • 同じ run・同じ $j$ の中では、開始位置を $p$ ずらすと同じ文字列。先頭 $\min(p,\ L-Kjp+1)$ 個は互いに異なる
  • 別の run で同じ文字列が出ることがある(入力例 2)。候補を(長さ, 開始位置の接尾辞配列のランク)で並べ、同じ長さで隣り合う 2 つは「ランクの間の LCP の最小 $\ge$ 長さ」なら同じ文字列
  • 同じ文字列の出現は、接尾辞配列でランクが連続した区間になるので、隣どうしの比較だけで全部の重複が分かる

模範解答 (Python)

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


def sa_is(s, upper):
    # SA-IS(AtCoder Library の Python 移植と同じ手順)。s は 0..upper の整数列
    n = len(s)
    if n == 0:
        return []
    if n == 1:
        return [0]
    if n == 2:
        return [0, 1] if s[0] < s[1] else [1, 0]
    sa = [0] * n
    ls = [False] * n
    for i in range(n - 2, -1, -1):
        ls[i] = ls[i + 1] if s[i] == s[i + 1] else (s[i] < s[i + 1])
    sum_l = [0] * (upper + 1)
    sum_s = [0] * (upper + 1)
    for i in range(n):
        if not ls[i]:
            sum_s[s[i]] += 1
        else:
            sum_l[s[i] + 1] += 1
    for i in range(upper + 1):
        sum_s[i] += sum_l[i]
        if i < upper:
            sum_l[i + 1] += sum_s[i]

    def induce(lms):
        for i in range(n):
            sa[i] = -1
        buf = sum_s[:]
        for d in lms:
            if d == n:
                continue
            sa[buf[s[d]]] = d
            buf[s[d]] += 1
        buf = sum_l[:]
        sa[buf[s[n - 1]]] = n - 1
        buf[s[n - 1]] += 1
        for i in range(n):
            v = sa[i]
            if v >= 1 and not ls[v - 1]:
                sa[buf[s[v - 1]]] = v - 1
                buf[s[v - 1]] += 1
        buf = sum_l[:]
        for i in range(n - 1, -1, -1):
            v = sa[i]
            if v >= 1 and ls[v - 1]:
                buf[s[v - 1] + 1] -= 1
                sa[buf[s[v - 1] + 1]] = v - 1

    lms_map = [-1] * (n + 1)
    m = 0
    for i in range(1, n):
        if not ls[i - 1] and ls[i]:
            lms_map[i] = m
            m += 1
    lms = [i for i in range(1, n) if not ls[i - 1] and ls[i]]
    induce(lms)
    if m:
        sorted_lms = [v for v in sa if lms_map[v] != -1]
        rec_s = [0] * m
        rec_upper = 0
        rec_s[lms_map[sorted_lms[0]]] = 0
        for i in range(1, m):
            l, r = sorted_lms[i - 1], sorted_lms[i]
            end_l = lms[lms_map[l] + 1] if lms_map[l] + 1 < m else n
            end_r = lms[lms_map[r] + 1] if lms_map[r] + 1 < m else n
            same = True
            if end_l - l != end_r - r:
                same = False
            else:
                while l < end_l:
                    if s[l] != s[r]:
                        break
                    l += 1
                    r += 1
                if l == n or s[l] != s[r]:
                    same = False
            if not same:
                rec_upper += 1
            rec_s[lms_map[sorted_lms[i]]] = rec_upper
        rec_sa = sa_is(rec_s, rec_upper)
        for i in range(m):
            sorted_lms[i] = lms[rec_sa[i]]
        induce(sorted_lms)
    return sa


class LCE:
    # 接尾辞配列 + LCP 配列 + Sparse Table で、2 つの接尾辞の最長共通接頭辞を O(1)
    def __init__(self, s):
        n = len(s)
        sa = sa_is(s, max(s) if s else 0)
        rank = [0] * n
        for i, v in enumerate(sa):
            rank[v] = i
        lcp = [0] * max(n - 1, 0)
        h = 0
        for i in range(n):
            if h:
                h -= 1
            if rank[i] == 0:
                continue
            j = sa[rank[i] - 1]
            while i + h < n and j + h < n and s[i + h] == s[j + h]:
                h += 1
            lcp[rank[i] - 1] = h
        table = [lcp]
        k = 1
        while 2 * k <= len(lcp):
            prev = table[-1]
            table.append([a if a < b else b for a, b in zip(prev, prev[k:])])
            k *= 2
        self.n, self.rank, self.table = n, rank, table

    def query(self, i, j):
        # s[i:] と s[j:] の共通接頭辞の長さ(i != j)
        if i >= self.n or j >= self.n:
            return 0
        a, b = self.rank[i], self.rank[j]
        if a > b:
            a, b = b, a
        k = (b - a).bit_length() - 1
        t = self.table[k]
        x, y = t[a], t[b - (1 << k)]
        return x if x < y else y


def lyndon_ends(s, lce, inv):
    # end[i] = s[i:] の最長 Lyndon 接頭辞の終わり。inv=True なら文字の大小を反転した順序で
    n = len(s)
    end = [0] * (n + 1)
    for i in range(n - 1, -1, -1):
        j = i + 1
        while j < n:
            if s[i] != s[j]:                        # 1 文字目で決まるときは LCE を引かない
                if (s[i] < s[j]) != inv:
                    j = end[j]
                    continue
                break
            l = lce.query(i, j)
            if j + l == n:
                break                               # s[j:] が s[i:] の接頭辞 → s[i:] の方が大きい
            a, b = s[i + l], s[j + l]
            if (a < b) != inv:
                j = end[j]                          # s[i:] < s[j:]: Lyndon 語を延ばす
            else:
                break
        end[i] = j
    return end


def compute_runs(a, fw, bw):
    n = len(a)
    runs = set()
    for inv in (False, True):
        end = lyndon_ends(a, fw, inv)
        for i in range(n):
            j = end[i]
            p = j - i
            r = j + (fw.query(i, j) if j < n and a[i] == a[j] else 0)
            if r - i < 2 * p and (i == 0 or a[i - 1] != a[j - 1]):
                continue
            l = i - (bw.query(n - i, n - j) if i > 0 and a[i - 1] == a[j - 1] else 0)
            if r - l >= 2 * p:
                runs.add((l, r, p))
    return runs


def main():
    data = sys.stdin.read().split()
    s, K = data[0], int(data[1])
    n = len(s)
    a = [ord(c) - 96 for c in s]
    fw = LCE(a)
    bw = LCE(a[::-1])
    runs = compute_runs(a, fw, bw)
    occ = 0
    cand = []                                   # 長さ * (n+1) + 開始位置のランク
    rank = fw.rank
    N1 = n + 1
    for l, r, p in runs:
        L = r - l
        q = K * p                               # 長さ K*j*p の K 乗(j = 1, 2, ...)
        while q <= L:
            c = L - q + 1
            occ += c
            base = q * N1
            for i in range(l, l + (p if p < c else c)):   # 同じ run では先頭 p 個だけが異なる
                cand.append(base + rank[i])
            q += K * p
    cand.sort()
    # 同じ長さで隣り合うランクの 2 つは、その区間の LCP の最小が長さ以上なら同じ文字列
    table = fw.table
    distinct = 0
    prev_len = -1
    prev_rank = -1
    for x in cand:
        ln, rk = divmod(x, N1)
        if ln != prev_len:
            distinct += 1
        else:
            k = (rk - prev_rank).bit_length() - 1
            t = table[k]
            u, v = t[prev_rank], t[rk - (1 << k)]
            if (u if u < v else v) < ln:
                distinct += 1
        prev_len, prev_rank = ln, rk
    print(occ, distinct)


main()

計算量: SA-IS・Kasai は $O(N)$、Sparse Table $O(N\log N)$、runs の列挙は Day175-Q5 と同じ。候補の数を $C$ とすると並べ替え $O(C\log C)$、重複の判定は 1 組 $O(1)$。実測(Python 3.14、Apple M5 Pro、起動込み)で $N=2\times10^5$、$K=2$ の Fibonacci 語が 1.04 s・233 MB(候補は $N=10^5$ で 766141 個)、乱択 2 文字 0.64 s、$a^N$ 0.38 s。

Step-by-Step 解説

1$K$ 乗はちょうど 1 つの run に入る

$w=u^K$、$|u|=q$、$w$ の最小周期を $p'$ とする。$w$ は周期 $q$ と $p'$ を持ち、長さ $Kq\ge2q\ge q+p'$ なので Fine–Wilf により $\gcd(p',q)$ も周期。$p'$ が最小なので $p'\mid q$。$|w|\ge2p'$ だから $w$ を周期 $p'$ のまま左右に延ばせるだけ延ばしたものが run で、それは一意。逆に、run $(l,r,p)$ の中の長さ $Kjp$ の部分文字列は周期 $p$・長さ $\ge2p$ で、最小周期は $p$(より小さい周期があると、run の根が原始的でなくなる)。だから「run・$j$・開始位置」と「$K$ 乗の出現」は 1 対 1。

2出現数

run ごとに $\sum_{j\ge1,\ Kjp\le L}(L-Kjp+1)$。$a^N$ のように 1 つの run から $\Theta(N^2/K)$ 個の出現が出るので、出現を 1 つずつ列挙してはいけない($N=2\times10^5$ の $a^N$ で $K=2$ の出現は $10^{10}$ 個)。

3run の中での重複

同じ run・同じ長さ $Kjp$ の出現は、開始位置が $p$ 違えば同じ文字列(周期 $p$)。開始位置の差 $d$ が $0<d<p$ なら、同じ文字列だと長さ $\ge2p$ の部分文字列が周期 $d$ と $p$ を持ち、Fine–Wilf で周期 $\gcd(d,p)<p$ になって矛盾。だから異なる候補は先頭 $\min(p,L-Kjp+1)$ 個。

4run をまたぐ重複をランクで除く

候補(長さ $\ell$, 開始 $i$)を整数 $\ell\cdot(N+1)+\mathrm{rank}(i)$ にして並べる。同じ $\ell$ で隣り合う 2 つ(ランク $a<b$)は、接尾辞 $S[\mathrm{sa}[a]..]$ と $S[\mathrm{sa}[b]..]$ の共通接頭辞の長さ $=\min(\mathrm{lcp}[a..b-1])\ge\ell$ のとき同じ文字列。同じ文字列を接頭辞に持つ接尾辞はランクが連続するので、隣どうしだけ比べれば十分。LCP の最小は runs の列挙に作った Sparse Table をそのまま使う。

5ハッシュとの違い

Day177-Q5 は(長さ, 法 $2^{61}-1$ のハッシュ)の集合で重複を除いた。衝突の確率は小さいが 0 ではない。本解は比較が厳密で、Fibonacci 語 $N=10^5$・$K=2$ の異なる平方の数 71341 が Day177-Q5 のハッシュ版の値と一致した。

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

案計算量採否理由
全 $(i,\ell)$ を調べる$O(N^3/K)$検算用のみ$N\le60$ の全探索
run ごと・$j$ ごとの候補数を足すだけ(重複を除かない)$O(\text{runs}\cdot N/Kp)$不採用(誤り)800 入力中 362 で異なる数を誤る(実測)
出現を $j=1$(長さ $Kp$)だけ数える同上不採用(誤り)800 入力中 305 で出現数を誤る(実測)。$a^N$ の $a^{2K}$ などを落とす
異なる数を(長さ, 最小周期)の組で数える同上不採用(誤り)800 入力中 364 で誤り(実測)。同じ長さ・周期でも中身が違う
ハッシュで重複除去(Day177-Q5)$O(C)$ 期待不採用衝突の可能性が残る。本解は厳密で、並べ替えの $O(C\log C)$ だけ重い
接尾辞オートマトンで異なる $K$ 乗を数える$O(N)$ 状態不採用状態ごとに「$K$ 乗か」を判定する方法が自明でない
runs+ランクの区間 LCP(本解)$O(N\log N+C\log C)$採用$N=2\times10^5$ で 1.04 s 以下

前提が変わったら

  • $K$ が分数(指数 $e$ の「繰り返し」、例えば長さ $\ge e\cdot$周期)なら、run ごとに長さ $\lceil ep\rceil..L$ の部分文字列を数える形になり、異なる数は長さごとに先頭 $p$ 個の候補で同じ方法が使える
  • 異なる $K$ 乗の文字列そのものを全部出力するなら、候補を重複除去した後に開始位置と長さを出すだけ
  • $N=10^6$ なら、Python では runs の列挙(Lyndon 配列と Sparse Table)が律速。$N=5\times10^5$ の Fibonacci 語で 3.32 s・582 MB(実測)だった

よくあるミス

ミス原因正しい書き方
出現を 1 つずつ列挙する$a^N$ で $\Theta(N^2)$ 個run ごとに閉じた式 $L-Kjp+1$
$j\ge2$ を忘れる「最小周期の $K$ 倍」だけを考える長さ $Kjp$ の全部の $j$
run の中の重複を除き忘れる開始位置を全部候補にする先頭 $\min(p,L-Kjp+1)$ 個
run をまたぐ重複を除き忘れるrun ごとに独立と思うランクで並べて LCP で比較(入力例 2)
LCP の区間を $[a,b]$ にするKasai の配列の添字の取り違えランク $a<b$ の共通接頭辞は $\min(\mathrm{lcp}[a..b-1])$

次のステップ

  • 発展1: 指数が分数の繰り返し(例えば指数 $\ge2.5$)の異なる数を同じ方法で数える
  • 発展2: 異なる平方の数が $N$ 以下(Fraenkel–Simpson と記憶、未確認)であることを、本解で $N\le20$ の全文字列(2 文字)について確かめる
  • 発展3: runs の列挙を $N=10^6$ で 3 秒以内にする(Lyndon 配列の作り方の改善)

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

  • 全探索との一致(実測): $N\le60$、乱択(1〜4 文字)・Fibonacci 語・周期文字列に 1 文字混ぜたもの・b を baa に置き換えたもの、$K\in\{2,3,4,5\}$ の 1600 入力(乱数の種 2 つ)で、出現数・異なる数とも全探索と全件一致
  • 誤った方法(実測、別の 800 入力): 重複を除かない 362、$j=1$ だけ 305、(長さ, 周期)で数える 364 で誤り
  • 既知の値との照合(実測): Fibonacci 語 $N=10^5$・$K=2$ の異なる平方 71341 は Day177-Q5(ハッシュ)と一致。Thue–Morse 語 $N=10^5$・$K=3$ は出現 0。Fibonacci 語 $N=10^5$・$K=4$ も出現 0
  • 時間(実測、起動込み、$N=10^5$、$K=2$/$3$): 乱択 2 文字 0.31/0.29 s、乱択 26 文字 0.25/0.24 s、Fibonacci 語 0.46/0.35 s、$a^N$ 0.19/0.19 s、長さ 300 の塊の繰り返し 0.30/0.29 s、Thue–Morse 0.33/0.30 s、Tribonacci 語 0.38/0.29 s。$N=2\times10^5$($K=2$): Fibonacci 1.04 s・233 MB、Tribonacci 0.83 s、乱択 2 文字 0.64 s、$a^N$ 0.38 s。$N=5\times10^5$: Fibonacci 3.32 s・582 MB、乱択 2 文字 1.89 s
  • 候補の数(実測、$N=10^5$、$K=2$): Fibonacci 766141 個(runs 76387 本)、Tribonacci 428761 個、塊の繰り返し 116800 個、乱択 2 文字 61708 個
  • 扱っていない範囲: runs 定理の原典の確認、分数の指数、$N=10^6$

自己評価

自分の回答

気づき・メモ