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

【Python】3段登りが最大k回まで使える階段問題を動的計画法で解く方法


n段の階段と整数kが与えられた場面を考えてみましょう。最初は0段目に立っており、一度の移動で1段、2段、または3段ずつ上ることができます。ただし、「3段ずつの移動」は全体で最大k回までしか使えません。この条件下で、階段を一番上まで登る方法が何通りあるかを求めるのがこの問題です。

たとえば n = 5、k = 2 の場合、答えは 13 になります。実際にあり得る登り方は次の13通りです。

  • [1, 1, 1, 1, 1]
  • [2, 1, 1, 1]
  • [1, 2, 1, 1]
  • [1, 1, 2, 1]
  • [1, 1, 1, 2]
  • [1, 2, 2]
  • [2, 1, 2]
  • [2, 2, 1]
  • [1, 1, 3]
  • [1, 3, 1]
  • [3, 1, 1]
  • [2, 3]
  • [3, 2]

解き方のアプローチ(動的計画法)

この問題は動的計画法(DP)を使うことで効率よく解けます。鍵となるのは、次のような二次元のメモ化テーブルを定義することです。

memo[j][i] = 「3段登りを最大j回まで使えるとき、i段を登る方法の総数」

このテーブルを以下の手順で埋めていきます。

  1. n = 0 の場合は 1 を返します(何もしないという1通り)。
  2. n = 1 の場合も 1 を返します。
  3. k = min(k, n) としておきます(n段しかなければ、それ以上の3段登りは意味がないため)。
  4. (n+1) × (k+1) のサイズの二次元リスト memo を用意します。
  5. すべての行 r について、初期値として memo[r][0] = 1、memo[r][1] = 1、memo[r][2] = 2 を設定します。
  6. 3段登りを一切使わない行(j = 0)は、フィボナッチ数列と同じ漸化式になります。
    memo[0][i] = memo[0][i-1] + memo[0][i-2]
  7. j ≥ 1 の各行について、i = 3 から n まで次のように計算します。まず count = i // 3(i段を登るときに考えられる3段登りの最大回数)を求めます。
    • count ≤ j の場合(3段登りの回数制約が実質的に効かない場合):
      memo[j][i] = memo[j][i-1] + memo[j][i-2] + memo[j][i-3]
    • count > j の場合(ここで3段登りを使うと残り回数が1減る):
      memo[j][i] = memo[j][i-1] + memo[j][i-2] + memo[j-1][i-3]
  8. 最後に memo[k][n] を返せば答えとなります。

ポイントは、直前の一手が1段・2段・3段のどれであるかで場合分けしている点です。「3段登り」で到達する場合は使用回数の予算を1消費するため、参照先が memo[j-1][i-3] に変わります。

Pythonでの実装例

以下が実際のコードです。

class Solution:
    def solve(self, n, k):
        if n == 0:
            return 1
        if n == 1:
            return 1
        k = min(k, n)
        memo = [[0] * (n + 1) for _ in range(k + 1)]
        for r in range(k + 1):
            memo[r][0] = 1
            memo[r][1] = 1
            memo[r][2] = 2
        for i in range(3, n + 1):
            memo[0][i] = memo[0][i - 1] + memo[0][i - 2]
        for j in range(1, k + 1):
            for i in range(3, n + 1):
                count = i // 3
                if count <= j:
                    memo[j][i] = memo[j][i - 1] + memo[j][i - 2] + memo[j][i - 3]
                else:
                    memo[j][i] = memo[j][i - 1] + memo[j][i - 2] + memo[j - 1][i - 3]
        return memo[k][n]


ob = Solution()
print(ob.solve(n=5, k=2))

入力

n = 5, k = 2

出力

13

計算量について

このアルゴリズムでは、テーブルの各セルを高々定数回の加算で埋めていくため、時間計算量は O(n × k)、必要なメモリも同じく O(n × k) となります。全探索で毎回組み合わせを数え上げる方法に比べ、大幅に高速に動作するのが動的計画法の利点です。

  1. Pythonで捕まえられる雨水の総量を計算するプログラム(トレッピング・レイン・ウォーター問題)

    非負整数からなる長さ n の配列が与えられているとします。各要素はバーの高さを表し、それぞれのバーの幅は1です。このとき、雨が降った後に溜め込むことのできる水の総量を計算するのが本記事のテーマです。状況を図にすると、以下のようになります。図を見ると、水が溜まっている部分(青い箱)は全部で8個あります。したがって、このケースの出力は8となります。解法のアプローチこの問題は「スタック」を利用することで効率よく解けます。全体の手順は以下の通りです。スタック st、変数 water := 0、インデックス i := 0 を用意するi が高さ配列のサイズ未満である間、次の処理を繰り返すスタックが空である

  2. Pythonで二分木を2つの木に分割できるパターン数を数えるプログラム

    問題の概要値「0」「1」「2」を含む二分木があるとします。根(ルート)には、少なくとも1つの「0」ノードと1つの「1」ノードが存在しています。ここで、「木の辺(エッジ)を1本削除すると、木が2つの異なる木に分割される」という操作を考えます。このとき、削除後に生成される2つの木のどちらにも「0」と「1」のノードが同時に含まれないように、辺を1本削除する方法が何通りあるかを求めるのがこの問題です。入力例例えば、次のような二分木が与えられたとします。この場合、出力は 1 となります。「0」から「2」へ向かう辺だけが、条件を満たす唯一の削除対象だからです。解法のアプローチこの問題は、DFS(深さ優先探