Decode Ways
大文字のメッセージを、コード A = 1、B = 2、以降 Z = 26 までの数字に変換し、区切り文字を入れずにコードを続けて書きました。数字列 s が与えられます。この数字列を生成できたメッセージが何通りあるかを返してください。
各文字は、1桁または隣り合う2桁の数字から読み取ります。また、コードが 0 で始まることはありません。06 は 6 ではなく、単独の 0 も文字ではありません。読み取り方が存在しない場合は、0 を返してください。
関数
- sstring
- デコードする数字列
- 戻り値integer
- s に符号化される文字メッセージの数
制約
1 ≤ s.length ≤ 100sには数字0から9だけが含まれ、0で始まる場合もあります。-
sのすべての接頭辞と接尾辞の読み方はそれぞれ231未満なので、答えと途中で計算するすべての個数は符号付き32ビット整数に収まります。
例
- 入力
- s = "2611"
- 出力
- 4
- 説明
- 4つの読み方は
2 6 1 1(BFAA)、26 1 1(ZAA)、2 6 11(BFK)、26 11(ZK)です。61は26より大きいため、中央の数字がペアになることはありません。
- 入力
- s = "1203"
- 出力
- 1
- 説明
0は、前にある2と組み合わせて20にする必要があるため、読み方は1 20 3(ATC)になります。先に12と読むと0が余ってしまい、03は 0 から始まります。
- 入力
- s = "06"
- 出力
- 0
- 説明
- 最初の文字は
0で始まる必要があります。単独の0は文字ではなく、06はコードではないため、この文字列を与えるメッセージはありません。
提出時に隠しテスト+25件
発展問題
s に * も含まれる場合はどうでしょうか。これは 1 から 9 までの任意の数字を表します。O(n) 時間で読み取り数を数え、その数を 10^9+7 で割った余りを返せますか?
ヒント
1つずつ開いてください。開くたびに少しずつ答えに近づきます。
最初の数字だけを見てください。最初の文字は何通りに読めますか。また、それぞれの選択の後、文字列には何が残りますか?
文字列の残りの読み取り回数は、残りの部分がどこから始まるかだけで決まり、そこにどうたどり着いたかには関係ありません。それぞれの開始位置を一度だけ数え、その回数を再利用してください。
ways(i)を最初のi桁の読み方の数とし、ways(0) = 1とします。桁i-1が0でない場合はways(i-1)を加え、位置iの前の2桁が10から26までの数を形成する場合はways(i-2)を加えます。必要なのは直近2つの個数だけです。
解説
各桁はそれぞれ単独で文字になるか、隣の桁と組み合わさって2桁の文字になるため、読み方の数はフィボナッチ数のように増えていきます。1が45個あるだけで、読み方は1836311903通りあります。すべての読み方を列挙するのは現実的ではありません。この問題を解く鍵は、読み方を完成させる方法の数が、到達した位置だけで決まることです。そのため、各位置を一度ずつ数えればよいのです。注意が必要なのは0です。0は10または20の2桁目にしかなれません。
再帰を使って両方の読み方を試してみましょう
正しいが、最大のテストでは終わらない
考え方
インデックス i に立ち、次の数字を見ます。それが 0 なら、ここから始まる文字はないので、この経路で得られる読み方はありません。そうでなければ、その数字を1文字として読み、残りの読み方の数を i+1 から数えられます。その数字と次の数字で10から26までの数ができるなら、両方を1文字として読むこともでき、i+2 から数えます。2つの選択肢では最初の文字が異なるため、重複せずに数を足し合わせられます。i が文字列の末尾に達したとき、読み方を1つ最後まで読み終えたことになるので、1を返します。
"2611" の場合、最初の文字は 2 または 26 です。2 の後は、61が大きすぎるため、次の文字は 6 でなければなりません。どちらの分岐もその後は 1 1 または 11 で終わるため、合計は2 × 2 = 4です。
答えは正しいものの、何も記憶されません。1が並ぶ文字列では、各呼び出しが2つに分岐し、呼び出し回数はフィボナッチ数列の規則に従うため、1が45個あると約 5 × 10^9 回の呼び出しが必要です。答えが小さいからといって処理量も減るわけではありません。1が44個続き、その後に3が55個、最後に 0 がある場合、答えは0ですが、各経路が最後の数字で行き止まるまで、再帰処理は1のあらゆる読み方をすべての3を通じてたどるため、呼び出し回数は約 10^11 回になります。
アルゴリズム
- インデックス
iから末尾までの数字の読み方の数を数えるヘルパー関数waysFrom(i)を書きます。 iがsの長さと等しい場合は、1 を返します。iの位置の数字が0の場合は、0 を返します。- 次の文字が1桁を使う読み方である
waysFrom(i+1)から始めます。 - 数字
iとi+1が26以下の数を形成する場合は、waysFrom(i+2)を加えます。waysFrom(0)を返します。
def numDecodings(s):
n = len(s)
def ways_from(i):
# The number of ways to decode s[i:].
if i == n:
return 1 # nothing left: one finished reading
if s[i] == "0":
return 0 # no letter code starts with 0
ways = ways_from(i + 1) # read one digit
if i + 1 < n and int(s[i:i + 2]) <= 26:
ways += ways_from(i + 2) # read two digits, 10 to 26
return ways
return ways_from(0)メモ化を使った再帰
考え方
再帰は同じ質問を何度も繰り返します。"11111"では、インデックス3からの数え上げは、1 1 1の後、11 1の後、そして1 11の後に必要になりますが、インデックス3以降の数字だけに依存するため、毎回同じ結果になります。最初に計算したときに各カウントを配列memoに保存し、その後はそこから読み取ります。
計算されていないスロットには、0ではなく-1を設定します。ここでは0も実際の答えだからです。30で終わる文字列では、どの位置も読み取り数は0です。目印に0を使うと、これらの位置は訪問するたびに未知のままに見え、再帰は以前と同じくらい遅くなります。
位置はn個あり、それぞれ一定の処理量で一度だけ計算されるため、時間計算量はO(n)です。メモと呼び出しスタックは、それぞれO(n)の領域を使います。ここでは呼び出しのネストは最大100段で、どの言語でも処理できます。
アルゴリズム
- 各インデックスに1つずつスロットを持つ配列
memoを作り、すべて-1に設定します。 waysFrom(i)では、文字列の末尾に達したら1を返し、-1でない場合はmemo[i]を返します。- それ以外の場合は、通常の再帰と同様に数えます。
0の場合は0を返し、それ以外の場合は、2桁の数字が10から26を形成するときにwaysFrom(i+1)とwaysFrom(i+2)を加算します。 - 0も含めた数を
memo[i]に保存し、それを返します。 waysFrom(0)を返します。
def numDecodings(s):
n = len(s)
memo = [-1] * n # memo[i]: ways to decode s[i:], -1 until worked out
def ways_from(i):
if i == n:
return 1
if memo[i] != -1:
return memo[i]
ways = 0
if s[i] != "0":
ways = ways_from(i + 1) # read one digit
if i + 1 < n and int(s[i:i + 2]) <= 26:
ways += ways_from(i + 2) # read two digits, 10 to 26
memo[i] = ways
return ways
return ways_from(0)2つのカウンターを使ったボトムアップ方式
考え方
再帰を逆向きにたどり、接頭辞の数を数えます。ways(i)を、最初のi桁の読み方の数とします。このような読み方の最後の文字は、インデックスi-1の数字単独か、インデックスi-2とi-1の2桁のどちらかです。前者は数字が1から9である必要があり、残りの読み方はways(i-1)通りです。後者は10から26になる必要があり、残りの読み方はways(i-2)通りです。したがって、ways(i)は条件を満たす場合の数の合計です。空の接頭辞には、空のメッセージという読み方が1通りあるので、ways(0) = 1です。
"1203"を順に見ていきます。1の後の数は1です。12の後は2です。読み方は1 2と12です。0は単独では使えず、20だけが有効なので、数は2より前の数、つまり1に戻ります。3は単独で使え、03はコードではないため、数は1のままです。
各数の計算で参照するのは、2つ前までの数だけなので、テーブルの代わりにtwoBackとoneBackの2つの変数を使います。これは各桁あたり一定の処理を行う1回の走査です。時間計算量はO(n)、空間計算量はO(1)で、再帰はまったく使いません。
アルゴリズム
twoBack = 0とoneBack = 1を設定します。これは空の接頭辞の数です。- 各インデックス
iについて、currentを 0 で初期化し、桁iが0でなければoneBackを加えます。 i ≥ 1で、桁i-1が0ではなく、桁i-1とiが 26 以下の数を作る場合は、twoBackを加えます。- 値を順にシフトします。
twoBack = oneBackとし、次にoneBack = currentとします。 - 最後の桁の後に、
oneBackを返します。
def numDecodings(s):
# ways(i) counts the readings of the first i digits; ways(0) = 1.
two_back, one_back = 0, 1 # ways(i-1) and ways(i) before digit i is read
for i in range(len(s)):
current = 0
if s[i] != "0":
current = one_back # digit i is a letter on its own
if i >= 1 and s[i - 1] != "0" and int(s[i - 1:i + 1]) <= 26:
current += two_back # digits i-1 and i form one letter, 10 to 26
two_back, one_back = one_back, current
return one_back
落とし穴と境界ケース
この問題の誤答のほとんどは、ゼロ、または更新を忘れるメモ化から生じます。
0を文字として扱ったり、06を6として扱ったりする。ゼロは10または20を完成させる場合にのみ使えるため、"30"、"100"、"06"はいずれも読み方が0通りです。- 2桁の部分を
≤ 26だけで判定する。05は数としては5ですが、コードではありません。2桁のうち先頭の数字が0でないことを確認してください。 - まだ計算していないメモ化スロットの印として0を使う。実際に読み方が0通りとなる位置も多いため、そうしたスロットは保存済みとして扱われず、訪問のたびに再計算されます。44個の1の後に3が続き、最後が
0の場合、すべてのスロットが0となり、呼び出し回数は再び約10^11になります。 - インデックス0より前の数字を読む。2桁の判定は
i ≥ 1でガードしてください。Pythonではs[-1]が最後の数字を黙って読み取り、他の言語では文字列の範囲外を読み取ります。 sを1つの数値に変換する。100桁の数値はどの整数型にも収まらず、変換によって先頭のゼロが削除され、答えが変わってしまいます。数字を1桁ずつ処理してください。- LuaとRでは位置が1から始まるため、文字列の末尾は位置
n+1で、最初の2桁判定は位置2で行います。
よくある質問4
Decode Ways の時間計算量はどれくらいですか?
ボトムアップの解法では各桁を一度だけ読み取り、処理量は一定なので、実行時間は O(n)、追加の領域は O(1) です。メモ化再帰も実行時間は O(n) ですが、メモと呼び出しスタックに O(n) の領域を使用します。通常の再帰は指数時間です。1だけの文字列では、呼び出し回数は 1.618^n のように増加します。
Decode WaysはClimbing Stairsとどのように関係していますか?
どちらも、サイズ1と2のステップで線を進む方法の数を数えます。Climbing Stairsではすべてのステップが可能なので、その数はフィボナッチ数になります。Decode Waysでは、1桁のステップには1から9までの数字が必要で、2桁のステップには10から26までの数が必要なため、条件を満たす場合にのみ和の各項を加算します。1だけからなる文字列ではすべてのステップが可能で、その数はフィボナッチ数と正確に一致します。
Decode Waysでは、ゼロをどのように扱いますか?
0単独では文字になれないため、その前の数字と組み合わせる必要があり、コードになるのは10と20だけです。ボトムアップのループでは、1桁の場合は0によって何も加算されず、1または2の後にある場合に限り、2桁前までのカウントが加算されます。先頭の0、連続する2つの0、または3から9までの数字の後にある0があると、答えは0になります。
Decode Ways は O(1) の領域で解けますか?
はい。ある接頭辞の個数は、1桁短い接頭辞と2桁短い接頭辞の個数だけで決まるため、表全体を2つの変数で置き換えられます。各ステップでは、それらから新しい個数を計算し、1つずつずらします。
似た問題
同じ考え方を使う問題です。2〜3問解くとパターンが身につきます。
Python
def numDecodings(s):
# ここにコードを書いてくださいケース1
ケース2
ケース3
入力
s = "2611"
期待値
4