Python
 Computer >> コンピューター >  >> プログラミング >> Python

Pythonで文字列の指定範囲ごとに異なる部分文字列の数を求めるプログラム

長さ n の文字列 s と、クエリのリスト Q が与えられます。各クエリ Q[i] はペア (l, r) を含んでおり、文字列 s のインデックス l から r まで(両端を含む)の範囲に存在する異なる部分文字列の数を数える必要があります。

たとえば、入力が s = "pptpp"、Q = [(1,1), (1,4), (1,1), (0,2)] の場合、出力は [1, 8, 1, 5] になります。その理由は次の通りです。

  • クエリ (1, 1) の場合:範囲内の部分文字列は「p」のみなので、出力は 1

  • クエリ (1, 4) の場合:部分文字列は「p」「t」「pp」「pt」「tp」「ptp」「tpp」「ptpp」の 8 種類なので、出力は 8

  • 再びクエリ (1, 1) の場合:部分文字列は「p」のみなので、出力は 1

  • クエリ (0, 2) の場合:部分文字列は「p」「t」「pp」「pt」「ppt」の 5 種類なので、出力は 5

解法のアプローチ

この問題を解くには、接尾辞配列(Suffix Array)と、それに付随する LCP配列(最長共通接頭辞配列) を利用します。LCP配列の構築には Kasaiのアルゴリズム を使います。ポイントとなるのは次の性質です。

異なる部分文字列の総数 = Σ(各接尾辞の長さ − 隣接する接尾辞とのLCPの長さ)

接尾辞を辞書順にソートすると、隣接する接尾辞どうしが共有している接頭辞こそが「二重にカウントされる部分文字列」に対応します。したがって、各接尾辞の長さから LCP を差し引いて合計すれば、重複を除いた部分文字列の数が求まります。

Kasaiのアルゴリズム(LCP配列の構築)

  1. サイズ n の配列 lcp と inv を 0 で初期化する
  2. i を 0 から n−1 までループし、inv[suff[i]] = i を設定する
  3. k = 0 とし、i を 0 から n−1 までループする
    • inv[i] が n−1 なら、k を 0 に戻して次の反復へ進む
    • j = suff[inv[i] + 1] とする
    • s[i+k] と s[j+k] が一致する限り k を増やし続ける
    • lcp[inv[i]] = k を記録し、k > 0 なら k を 1 減らす
  4. lcp を返す

クエリへの回答(メイン処理)

  1. 結果を格納する空のリスト res を用意する
  2. 各クエリ (left, right) について次を行う
    • sub = s[left:right+1] として対象範囲の部分文字列を取り出す(length = right − left + 1)
    • (インデックス, 接尾辞文字列) のペアをすべて作成し、接尾辞文字列の辞書順でソートする
    • ソート済みの接尾辞配列 suff に対して kasai() を呼び出し、LCP 配列を取得する
    • count を最初の接尾辞の長さで初期化し、残りの各接尾辞について「長さ − LCP」を加算する
    • count を res の末尾に追加する
  3. res を返す

実装例

理解を深めるために、以下のPython実装を見てみましょう。

def kasai(s, suff, n):
    lcp = [0] * n
    inv = [0] * n
    for i in range(n):
        inv[suff[i]] = i
    k = 0
    for i in range(n):
        if inv[i] == n-1:
            k = 0
            continue
        j = suff[inv[i] + 1]
        while i + k < n and j + k < n and s[i + k] == s[j + k]:
            k += 1
        lcp[inv[i]] = k
        if k > 0:
            k -= 1
    return lcp

def solve(s, Q):
    res = []
    for i in range(len(Q)):
        left, right = Q[i]
        sub = s[left: right + 1]
        length = right - left + 1

        suffix = [[i, sub[i:]] for i in range(length)]

        suffix.sort(key=lambda x: x[1])
        suff, suffix = [list(t) for t in zip(*suffix)]

        lcp = kasai(sub, suff, length)
        count = len(suffix[0])
        for i in range(length - 1):
            count += len(suffix[i + 1]) - lcp[i]

        res.append(count)
    return res

s = "pptpp"
Q = [(1,1),(1,4),(1,1),(0,2)]
print(solve(s, Q))

入力

"pptpp", [(1,1),(1,4),(1,1),(0,2)]

出力

[1, 8, 1, 5]

計算量の目安

各クエリでは、接尾辞のソートに O(m log m) 回の比較(m は範囲の長さ)、Kasaiのアルゴリズムによる LCP 構築に O(m) の計算量が必要です。文字列比較のコストを考慮すると、クエリあたりの計算量は O(m² log m) 程度になります。より大規模な入力に対応したい場合は、接尾辞配列を高速に構築する手法や、Suffix Automaton(接尾辞オートマトン)など別のデータ構造の活用も検討するとよいでしょう。

  1. Pythonで数の因子の最小合計を求めるプログラム|素因数分解の考え方

    本記事では、与えられた整数について、積が元の数と等しくなる因子の組み合わせの中から合計が最小となる値を求める方法を、Pythonのコード例とともに解説します。 問題定義 入力として1つの整数が与えられます。この数を複数の因子の積として表したとき、因子の合計が最小になるケースを求めてください。 すべての因子の組み合わせを網羅的に調べて合計を比較する方法もありますが、実はもっとシンプルで効率的なアプローチが存在します。 考え方:素因数の合計が最小になる 鍵となるのは次の性質です。積が一定の値になるとき、因子の合計が最小になるのは、すべての因子を素数まで分解した場合(素因数分解した場合)です。

  2. 【Python】ある数の最大の素因数を求めるプログラムの書き方

    この記事では、「与えられた整数の最大の素因数を求める」という問題に対する解決方法を、具体的なコード例とともにわかりやすく解説します。 問題文 正の整数 n が与えられたとき、その数の最大の素因数を求めます。 例えば n = 15 の場合、15 は 3 × 5 と素因数分解できるため、答えは 5 となります。 解き方のアプローチ 入力された数を、小さい約数から順番に割っていくことで素因数分解します。 割り切れるたびに、その時点での約数(素因数)を「最大値」として更新していきます。 平方根まで調べれば十分なため、計算量を抑えられます。 実装例(サンプルコード) import math def