ABC247 CをPythonで解く|再帰で数列を作る考え方

読了 約6分 たびすけ
AtCoder ABC247 Cの再帰的な数列構成を学ぶ図

次に読む記事

関連するテーマの記事を、先に確認できます。

ABC247 Cの答えは、S1 = [1]を基底にし、n ≥ 2ではSn = Sn-1 + [n] + Sn-1として列を作ることです。入力の制約は1 ≤ N ≤ 16なので、S1から順に作れば、公式の入力形式に沿ってSNを空白区切りで出力できます。

問題の定義、制約、入出力はAtCoder ABC247 Cの問題文で確認できます。

ABC247 Cの数列の定義

ABC247 Cでは、S1 = [1]を基底(再帰を止める最初の定義)にします。S0の定義は問題文にないため、[0]を基底にはしません。

n ≥ 2のときは、ひとつ前の列を左右に置き、その中央へnを入れて、Sn = Sn-1 + [n] + Sn-1を作ります。ここで+はリストをつなぐ操作です。

小さい列を順に追跡する

まずS1を一度決めると、同じ形を左右へ置く操作だけで次の列を作れます。S2では、[1]の左右に2を入れるので[1, 2, 1]になります。さらにS3では、その列を左右に置いて中央へ3を入れるため、[1, 2, 1, 3, 1, 2, 1]です。

作り方 長さ
S1 [1] 1
S2 S1 + [2] + S1 = [1, 2, 1] 3
S3 S2 + [3] + S2 = [1, 2, 1, 3, 1, 2, 1] 7

この追跡例では、S2S3の左と右にそのまま現れ、中央だけが3に変わります。コードでも、この「ひとつ前の列を一度作り、左右へ再利用する」形をそのまま書きます。

公式サンプルで入出力を確認する

入力では整数Nが1つ与えられ、出力では作ったSNを空白区切りで並べます。公式サンプルは次のとおりです。

N 公式の出力 長さ
1 1 1
2 1 2 1 3
4 1 2 1 3 1 2 1 4 1 2 1 3 1 2 1 15

N=4では、S3を左へ置き、4を中央へ入れ、同じS3を右へ置きます。したがって、表の出力は追跡例の規則と一致します。

基底を取り違えないための確認

S0 = [0]から始めると、N=1[0, 1, 0]となり、公式の出力[1]と一致しません。N=0も公式の制約に含まれないため、実装の停止条件はn == 1にします。

再帰をそのままPythonに写す

関数build(n)は、ひとつ小さい列を先に作り、それを左右へ再利用します。

def build(n):
    if n == 1:
        return [1]
    previous = build(n - 1)
    return previous + [n] + previous

N = int(input())
print(*build(N))

if n == 1の行は、公式のS1 = [1]をそのまま表しています。

previous = build(n - 1)では、ひとつ小さい列を一度だけ作ります。続くprevious + [n] + previousは、同じpreviousを左と右へ使い、中央のnと連結する処理です。

int(input())で入力Nを整数として読み、print(*build(N))で列の要素を空白区切りにして出力します。リストの結合はPython公式ドキュメントのシーケンス演算、入力と出力の呼び出しはPython公式ドキュメントの組み込み関数に対応します。

再帰コードが正しい理由

このコードの返り値が定義どおりになることは、nの小さい値から順に確認できます。

  1. 基底:n=1では[1]を返すため、問題のS1と一致します。
  2. 帰納段階:n-1で正しいSn-1が返ると仮定すると、コードはそれを左右に置き、中央へnを加えます。返り値はSn-1 + [n] + Sn-1となり、問題が定めるSnと一致します。

基底と帰納段階がつながるので、1 ≤ n ≤ Nの各呼び出しは正しい列を返します。

列の長さは2N - 1になる

Snの要素数をLnとします。基底ではL1 = 1です。n ≥ 2なら、長さLn-1の列を2つと中央の1要素を連結するので、Ln = 2Ln-1 + 1になります。

L1 = 21 - 1と一致し、Ln-1 = 2n-1 - 1とすると、Ln = 2(2n-1 - 1) + 1 = 2n - 1です。

したがって、求める列SNの長さは2N - 1です。N=1N=2N=4の長さは、それぞれ1、3、15になります。公式の最大値N=16では、列の要素数は216 - 1 = 65535です。

このコードの計算量

previous = build(n - 1)で子の再帰呼び出しを各nにつき一度だけ行います。長さLn = 2n - 1なので、段nで行うリスト結合の大きさはO(2n)です。

  • 時間計算量:T(1) = O(1)T(n) = T(n-1) + O(2n)よりO(2N)
  • リスト保持:最終列と構築中の列を合わせてO(2N)
  • 再帰の深さ:O(N)

公式解説には、式の中でSn-1を2回呼ぶ別実装も示されています。その実装は同じ子の列を2回計算するためO(N · 2N)ですが、ここで使ったコードは一度作ったpreviousを左右へ再利用するため、計算量はO(2N)です。

実装を確認するときは、N=1N=2N=4を入力し、公式サンプルと出力および長さを照合します。N=3は追跡表から自分で書き出すと、前の列を左右へ置く再帰の形を確認できます。