最初の 2 つの投稿では、暴力的な再帰のプロセスを紹介しました。一般的に言えば、それは自然の知恵の使用+ 継続的な試みです。この記事では、暴力的再帰を動的計画法に変換する方法を紹介します。
フィボナッチ数列
フィボナッチ数列はご存知かと思いますが、1列目の値が1、2列目の値が2、7列目の値が13というように定められており、全体がfになります。 (N) = f(N -1 ) + f(N - 2)。
次に、暴力的な再帰を使用してそれを解決しようとすると、コードは次のようになります。
public static int f(int N) {
if (N == 1) {
return 1;
}
if (N == 2) {
return 1;
}
return f(N - 1) + f(N - 2);
}
このような数行のコードで実際にフィボナッチ数列を実現しているのですが、このときf(7)の値が必要であれば、それを展開すると実際には二分木の形になります。f(7) は f(5) と f(6) に依存します。f(6) は f(5)、f(4) などに依存します。。。。
図から、f(7) の値を取得したい場合、多くのメソッドに依存することになり、一部のメソッドは何度も実行されることがわかります。このときテーブル構造があれば、「 「前の結果をキャッシュ」します。後でこれに遭遇したときに、値を直接取得します。もっと便利ですか、O ( N ) O(N)を使用してくださいO ( N )時間計算量でこの問題を解決できます。
この「キャッシュ」が動的プログラミングです。統合する特定のトピックに到達します。
トピック
N 個の位置が 1 ~ N、N >= 2 と記録され、一列に並んでいるとします。ロボットがあり、最初にロボットは開始
位置にいます (開始は 1 の中央の位置です)。 ~N)。
ロボットが現在位置 1 にある場合、次のステップは右側の位置 2 のみです。
ロボットが位置 N にある場合、次のステップは左の位置 N - 1 までしか進むことができません。
真ん中の場合は左右に移動できます。ロボットは K 歩を進み、最終的に目標位置に到達することが規定されています (目標も 1 ~ N のいずれかです)。
ロボットに開始位置から目標位置まで K 歩を歩くように依頼します。方法は合計で何通りありますか。
図に示すように、現在の開始点が 2、終了点が 4、合計 4 つの位置、合計 4 つのステップがあるとします。そこに到達する方法はいくつかあります。合計 3 つです。
- 2 -> 1 -> 2 -> 3 -> 4
- 2 -> 3 -> 4 -> 3 -> 4
- 2 -> 3 -> 2 -> 3 -> 4
暴力的な再発
- トピック分析により、基本ケースを確認し、ステップが 0 の場合は、進むステップがないことを意味し、戻ってきます。
- cur = 1 の場合、次のステップは右へのみ進むことができます。
- cur = N の場合、次のステップは左へのみ進むことができます。
- それ以外の場合は、中央で左と右を使用することができ、左に移動してターゲットに到達する方法 + 右に移動してターゲットに到達する方法の合計がメソッドの数になります。
コード
パラメータ cur: ロボットの現在位置 step: 残りステップ数 target: 目標位置 N: 合計 N ポジション
K: 合計ステップ数
//方法返回机器人从cur出发,走step步,到达target的方法数
public static int ways(int cur, int K, int target, int N) {
return process(cur, K, target, N);
}
public static int process(int cur, int step,int target, int N) {
//当步数为0时,看当前位置是否在目标位置,如果在,则方法数 + 1,否则认为没走到为0
if (step == 0) {
return cur == target ? 1 : 0;
}
//无论怎么走,每走一步 step 一定 -1
//当前位置为1时,必须向右,所以下一步会是在2位置
if (cur == 1) {
return process(2, step - 1, target, N);
}
//当前位置为N时,必须向左,所以下一步在N -1位置
if (cur == N) {
return process(N - 1, step - 1, target, N);
}
//否则,在中间,可能向左走,也可能向右走
return process(cur + 1, step - 1, target, N) + process(cur - 1, step - 1, target, N);
}
最適化
暴力的な再帰を使用して動的計画法に最適化するにはどうすればよいですか? 最も重要なポイントの 1 つは、呼び出しプロセスに応じて繰り返しの接続があるかどうかを確認することです。繰り返しの解がある場合は、間違いなく動的計画法に最適化できます。。
上記のコードから、ways メソッドの結果に影響を与える要因は何であるかを分析できます。
Nは固定でターゲットも固定ですが、上のコードだとcurの現在位置が変化するだけでステップはどんどん減少していきます。したがって、全体的な結果に影響を与えるパラメータは、現在位置と歩数です。
そのプロセスを紐解いてみましょう。たとえば、開始 = 7、ターゲット = 15、ステップ = 10 の場合、位置 7 から位置 15 まで開始すると、どのように動くかを確認するのに 10 ステップかかります。
7が真ん中の位置です。開始後は両側を歩くことができます。現在位置が7の場合、残りのステップは8です。サブプロセスは繰り返し呼び出しがあり、暴力的再帰から動的計画法まで最適化できます。 。現時点では前の通話は気にしないでください。
丸で囲んだ 2 つの場所の現在位置はどちらも 7 なので、残りの歩数は 8 で、目標は 15 に固定されています。したがって、この時点で目標に到達する方法の数は同じでなければなりません。したがって、7 -> 6 -> 7 であっても、7 -> 8 -> 7 であっても気にしないでください。
このことから、状態を決定する鍵となるのはcurとstepであり、この2つが決まれば結果が決まると分析できます。
キーが見つかり、そのキーに従ってキャッシュされたテーブルを表す 2 次元配列が作成されます。現在の場所に来て、残りのステップ数が同じであれば、ターゲットに到達するためのメソッドの数も同じでなければなりません。キャッシュ テーブルに存在する場合は、キャッシュ テーブルを通じて直接取得できます。 。
コード
//cur范围:0 ~ N
//step范围:0 ~ K
public static int ways2(int cur, int K, int target, int N) {
//创建一个N + 1和 K + 1返回的数组,保证到任何位置都能囊括进去
int[][] dp = new int[N + 1][K + 1];
for (int i = 0; i <= N; i++) {
for (int j = 0; j <= K; j++) {
dp[i][j] = -1;
}
}
return process2(cur, K, target, N, dp);
}
public static int process2(int cur, int step, int target, int N, int[][] dp) {
//不等于 -1 说明 之前计算过当来到cur位置时,剩余step 到达target的方法数
if (dp[cur][step] != -1) {
return dp[cur][step];
}
//走到这,说明没算过
int ans = 0;
if (step == 0) {
ans = cur == target ? 1 : 0;
} else if (cur == 1) {
ans = process2(2, step - 1, target, N, dp);
} else if (cur == N) {
ans = process2(N - 1, step - 1, target, N, dp);
} else {
ans = process2(cur + 1, step - 1, target, N, dp) + process2(cur - 1, step - 1, target, N, dp);
}
//记录当前位置到达target的方法数。
dp[cur][step] = ans;
return ans;
}
このとき、コードは「バカキャッシュ」によって最適化されており、各ステップのメソッドの数は dp テーブルを通じて記録されます。このトップダウンの動的プログラミングは「記憶検索」と呼ばれます。 、キャッシュ テーブルを通じて記憶を形成し、時間のためにスペースを使用し、より多くの時間を交換するためにキャッシュ テーブルを使用します。
上記のコードを再度最適化するにはどうすればよいでしょうか?
上記は、cur の現在位置と残りのステップ数に基づいて 2 次元配列キャッシュ テーブルを構築します。この 2 次元配列には、指定された cur とステップの各ステップの結果がすべて含まれます。この dp を描画してみませんか?テーブル?? 現在のロボットが位置 2 にあり、位置は合計 5 つあり、歩くには 6 歩かかり、目標は位置 4 に移動することであるとします。
したがって、 cur = 2 K = 6 N =5 target = 4 、描かれる絵は次のようになります。
行は現在位置、列は歩数ですが、行 0 には到達できないため、行 0 の位置には x が付けられます。初期位置は 2 で、残り 6 ステップに星が付いています。
グラフを完成させ、現在の位置と各ステップの結果メソッドの数をグリッドに記入するだけです。ポジション 2 ~ 6 のステップ数をテーブルから直接知ることはできますか。
では、この表を改善するにはどうすればよいでしょうか? 最初の暴力的な再帰コードから始まり、答えを直接与えるいくつかの基本的なケースがあります。コードロジックに従って、一行ずつ完成させていきます。
public static int process(int cur, int step, int target, int N) {
if (step == 0) {
return cur == target ? 1 : 0;
}
if (cur == 1) {
return process(2, step - 1, target, N);
}
if (cur == N) {
return process(N - 1, step - 1, target, N);
}
return process(cur + 1, step - 1, target, N) + process(cur - 1, step - 1, target, N);
}
基本ケースから始めましょう。残りのステップが 0 の場合、cur の位置がターゲット上にある場合、それは到達可能なメソッドが存在することを意味し、ターゲット位置にない他のメソッドの数を意味します。は 0 なので、最初の列が出てきます。
下を見続けます。現在の状態は cur と step (パラメーターが渡されます)、現在の位置 cur = 1 の場合、質問の意味によれば、次のステップは 2 つの位置にのみ移動でき、毎回step - 1 を移動するので、このとき cur = 1, step = 1 の場合、 cur = 2, step = 0 の結果に依存します。1行目の任意の位置におけるcurの依存関係は以下のようになります。
現在の cur = N の場合、次のステップは左 (N -1) にのみ移動でき、毎回ステップ - 1 に移動するため、N 行の任意の位置での cur の依存関係は次のようになります。 。
さて、現在は一般的な位置だけが残っています。この時点で cur = 3 であると仮定して、コードを見てください。左または右に移動できます。したがって、この時点での依存関係は次のようになります。
コードによると、この時点で cur = 3 の場合、依存する結果を追加するかどうか、この時点で dp テーブルが完了しているかどうかがわかります。
cur = 2 および step = 6 の場合、合計 13 のメソッド解がターゲット = 4 の位置に到達できることがわかります。このとき、dpテーブルを直接構築した場合、変数curとstepに従って結果が直接得られるのでしょうか?
コード
動的プログラミング コード全体は非常に簡潔です。
public static int ways3(int cur, int K, int target, int N) {
int[][] dp = new int[N + 1][K + 1];
//先将对应target位置标1
//初始化int[][]默认值都是0,所以其他值不用管
dp[target][0] = 1;
//按列遍历
for (int step = 1; step <= K; step++) {
//先将第一列的值确定
dp[1][step] = dp[2][step - 1];
//因为我单独遍历N行时的值,所以这个遍历到 N -1即可
for (int j = 2; j < N; j++) {
//按照依赖关系,当前位置依赖左上和左下的位置
dp[j][step] = dp[j - 1][step - 1] + dp[j + 1][step - 1];
}
//再将最后一列的值确定
dp[N][step] = dp[N - 1][step - 1];
}
return dp[cur][K];
}