ダイナミックプログラミング
これを読んだ後、あなたがアルゴリズムを感じることと動的プログラミングはなかったので、動的計画法は、5つの一般的に使用されるアルゴリズムの戦略の一つであり、DPと呼ばれる、中国語翻訳は、「ダイナミックプログラミング」背の高い翻訳することができますが、上の無数の人々のこの音苦いピットですそんなに、彼ら「ダイナミック」と「計画」深すぎる表現ではありません。
、最初の平野それを理解するために簡単な例を与える配列、もし一つだけの要素で最大の要素を見つけるために、ラフなプロトタイプを持っており、それはそれで、その後、さらに内部のプラス要素の配列には、再帰的な関係は、そのとき、あなたがあります大きいほど、解決策は、小さな問題を保存するときにすることができ、大きな問題を解決するDP戦略の典型的な最大の配列、である、ちょうどそれが現在の最大と追加された新しい要素を比較もたらす、現在の最大の要素を知っています直接。
ちょうどあなたがまだ、混乱少しを感じるあなたはそれが何を意味するのか知っている栗のいくつかの簡単な少し以下、慌てる必要はありません場合は、と述べました。
フィボナッチは議論します
1は、第1の数であり、2番目の数字は第3の数、前方の数及び2に等しいそれぞれの背後数から出発し、1です。要件:入力nは、n番目のフィボナッチ数の出力。
最初の例では、私たちの最後のセクションでは、列の数が--Fibonacci問題を引用したとき、それはそう繰り返しを思い付くために、あまりにも古典的で再帰的な分割統治の戦略を説明しています。
私たちは、フィボナッチ数列を解決すると、再帰的な方法の深さのセクションで分析する場合は、計算F(4)とf(3)にしたいとき、あなたは計算され、そのようなあなたのコンピューティングF(5)として、繰り返し動作の多くがあったでしょうF(4)(3)を再度計算し、F(2)、(3)F算出し、また、F(2)及びf(1)を算出し、以下の図を参照してください。
5興味の友人あなたがF(100)を計算したい場合は、その後、あなたはそれが行われていない永遠(手動面白い)を待つ必要があり、比較的小さく、かつので、F(3)とf(2)繰り返した操作の場合、これはとにかく、試すことができ、私が試してみました。
public static int fibonacci(int n){//递归解法
if(n == 1) return 1;
else if(n == 2) return 1;
else return fibonacci(n - 1) + fibonacci(n - 2);
}
上記の非常に簡単な再帰溶液、コードルックスであるが、アルゴリズムの複雑さはO(2 ^ n)は、インデックスレベルの複雑さに達し、プラスN場合が大きいスタックでいるより大きなメモリのオーバーヘッドになり、非常に効率が悪いです。
私たちはこの問題解決するためにDP戦略についてお話しましょう
、我々は非効率的なアルゴリズムにつながる根本原因は、再帰的な大きなスタックメモリのオーバーヘッドがあることを知っているし、何回より少ない数は、計算を繰り返す、我々は数が少ない、などを持っているので、 F(2)、F(3 )... これらの計算は、なぜダブルカウント、その後、あなたはDP戦略を使用して、計算されたF(n)が保存されました。私たちは、コードを見て:
/**
* 对斐波那契数列求法的优化:如果单纯使用递归,那重复计算的次数就太多了,为此,我们对其做一些优化
* 假设最多计算到第100个斐波那契数
* 用arr这个数组来保存已经计算过的Fibonacci数,以确保不会重复计算某些数
*/
private static int arr[] = new int[100];
public static int Fibonacci(int n){
if(n <= 2){
return 1;
}else{
if(arr[n] != 0) //判断是否计算过这个f(n)
return arr[n];
else{
arr[n] = Fibonacci(n-1)+Fibonacci(n-2);
return arr[n];
}
}
}
ARRアレイを0に初期化され、ARR [I] Fに示される(i)は、それぞれが第一ARR決定[I]は0を算出していない示している場合はゼロでない場合は、再帰的計算は、それが有することを示す、0でありますそれが直接返されることを計算しました。
長い再帰があるようとして、それは必然的に大規模なスタックメモリのオーバーヘッドを持っていますので、そこには、減少したが、非常に重要ではありませんが、これは、大部分が重複して計算を回避するという利点が、スタックメモリのオーバーヘッドがあります。だから我々は持っている道の再発に対するフィボナッチ数列を計算するために、実際には、このアイデアはまた、DPの戦略に沿った、すべての計算値を保存されます。
//甚至可以使用递推来求解Fibonacci数列
public static int fibonacci(int n){
if(n <= 2) return 1;
int f1 = 1, f2 = 1, sum = 0;
for(int i = 3; i <= n; i++){
sum = f1+f2;
f1 = f2;
f2 = sum;
}
return sum;
}
パスの数を解きます
ロボットは、ありますどのように多くの異なるパス、それは右下の角に到達しようとする「完了」を右または起工で唯一のステップ求めることができますか?(* 3 7メッシュ)
最も重要な問題のようなDP戦略は状態遷移方程式を見つけることです正確には、最も困難なステップです。
1、我々は(i、j)はまた、2例に到達するために、この事実によって見ることができ、一つは(I、J-1)からのもので、他のに到達するために右のステップを取る、(I-1からですこれらの2つの場合のパスは、のパスの番号(i、j)にそのすべての数一緒になるようにj)は、ダウン到達するために一歩を踏み出します。
これにより、状態遷移方程式が記載されています。
F(I、J)= F(I-1、J)+ F(I、J-1)
- 図2は、結果をDPの考え方によれば、既に算出され、後で再利用するために格納され、ここでは、F(i、j)を格納するための2次元アレイを考えます。
図3に示すように、小から大規模計算の問題のために、大規模の問題は、逆多重化問題の小スケールを使用して計算しました。
コードの実装
/**
* 此题是求解路径个数,让你从(1,1)走到某个特定的位置,求一共有多少种走法
* @param i
* @param j
* @return
*/
public static int Count_Path(int i, int j){
int result[][] = new int[i][j];
for (int k = 0; k < i; k++) { //将二维数组初始化为1
Arrays.fill(result[k],1);
}
for (int k = 1; k < i; k++) {
for (int l = 1; l < j; l++){
result[k][l] = result[k-1][l]+result[k][l-1];
}
}
return result[i-1][j-1];
}
暁には、手順を話し合うリープフロッグ
又是那只熟悉的青蛙,和上节递归与分治中相同的例题,一只青蛙一次可以跳上1级台阶,也可以跳上2级,求该青蛙跳上一个n级的台阶共有多少种跳法。详细思路可以看看上一篇文章——递归与分治策略。
我们下面先回顾一下上次用的递归算法:
public static int Jump_Floor1(int n){
if(n <= 2){
return n;
}else{ //这里涉及到两种跳法,1、第一次跳1级就还有n-1级要跳,2、第一次跳2级就还有n-2级要跳
return Jump_Floor1(n-1)+Jump_Floor1(n-2);
}
}
其实和第一个例子斐波那契一样,之所以把它又拉出来讨论,是因为它的递归解法中涉及的重复计算实在太多了,我们需要将已经计算过的数据保存起来,以避免重复计算,提高效率。这里大家可以先自己试着改一下其实和第一个例子的改进方法是一样的,用一个数组来缓存计算过的数据。
/**
* 看完递归的方法不要先被它的代码简洁所迷惑,可以分析一下复杂度,就会发现有很多重复的计算
* 而且看完这个会发现和Fibonacci的递归方法有点像
* @非递归
*/
private static int result[] = new int[100];
public static int Jump_Floor2(int n){
if(n <= 2){
return n;
}else{
if(result[n] != 0)
return result[n];
else{
result[n] = Jump_Floor2(n-1)+Jump_Floor2(n-2);
return result[n];
}
}
}
下面将难度做一提升,我们来讨论一道DP策略里的经典例题——最长公共子列问题
最长公共子序列问题
给定两个序列,需要求出两个序列最长的公共子序列,这里的子序列不同于字串,字串要求必须是连续的一个串,子序列并没有这么严格的连续要求,我们举个例子:
比如A = "LetMeDownSlowly!" B="LetYouDownQuickly!" A和B的最长公共子序列就是"LetDownly!"
比如字符串1:BDCABA;字符串2:ABCBDAB,则这两个字符串的最长公共子序列长度为4,最长公共子序列是:BCBA
我们设 X=(x1,x2,.....xn) 和 Y={y1,y2,.....ym} 是两个序列,将 X 和 Y 的最长公共子序列记为LCS(X,Y),要找它们的最长公共子序列就是要求最优化问题,有以下几种情况:
- 1、n = 0 || m = 0,不用多说最长的也只能是0,LCS(n,m) = 0
- 2、X(n) = Y(m),说明当前序列也是相等的,那就给这两个元素匹配之前的最长长度加一,即LCS(n,m)=LCS(n-1,m-1)+1
- 3、X(n) != Y(m),这时候说明这两个元素并没有匹配上,那所以最长的公共子序列长度还是这两个元素匹配之前的最长长度,即max{LCS(n-1,m),LCS(n,m-1)}
由此我们可以列出状态转移方程:(用的别人的图)
我们可以考虑用一个二维数组来保存LCS(n,m)的值,n表示行,m表示列,作如下演示,比如字符串1:ABCBDAB,字符串2:BDCABA;
1、先对其进行初始化操作,即将m=0,或者n=0的行和列的值全填为0
2、判断发现A != B,则LCS(1,1) = 0,填入其中
3、判断B == B,则LCS(1,2) = LCS(0,1)+1=1,填入其中
4、判断B != C,则LCS(1,3)就应该等于LCS(0,3)和LCS(1,2)中较大的那一个,即等于1,通过观察我们发现现在的两个序列是{B}和{ABC}在比较,即使现在B != C,但是因为前面已经有一个B和其匹配了,所以长度最少已经为1了,所以当C未匹配时,子序列的最大值是前面的未比较C和B时候的最大值,所以填1
5、再比较到B和B,虽然两者相等,但是只能是LCS(n-1,m-1)+1,所以还是1,因为一个B只能匹配一次啊,举个例子:就好像是DB和ABCB来比较,当第一个序列的B和第二个序列的第二个B匹配时,就应该是D和ABC的最长子序列+1,所以如下填表:
6、掌握规律后,我们直接完整填完这个表
代码实现:
/**
* 求最长公共子序列问题
* Talk is cheap, show me the code!
* 参考公式(也是最难的一步):
* { 0 i = 0, j = 0
* c[i][j] = { c[i-1][j-1]+1 i,j>0, x[i] == y[i]
* { max{c[i-1][j],c[i][j-1]} i,j>0, x[i] != y[i]
* 参考书目:算法设计与分析(第四版)清华大学出版社 王晓东 编著
* 参考博客:https://www.cnblogs.com/hapjin/p/5572483.html
* 比如A = "LetMeDownSlowly!" B="LetYouDownQuickly!" A和B的最长公共子序列就是"LetDownly!"
* @param x
* @param y
* @Param c 用c[i,j]表示:(x1,x2....xi) 和 (y1,y2...yj) 的**最长**公共子序列的长度
* @return 最长公共子序列的长度
*/
//maybe private method
private static int lcsLength(String x, String y, int[][] c){
int m = x.length();
int n = y.length();
//下面是初始化操作,其实也没必要,因为Java中默认初始化为0,其他语言随机应变
for (int i = 0; i <= m; i++) c[i][0] = 0;
for (int i = 0; i <= n; i++) c[0][i] = 0;
//用一个序列的每个元素去和另一个序列分别比较
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if(x.charAt(i-1) == y.charAt(j-1)){ //如果遇到相等的,就给序列的上一行上一列的加1
c[i][j] = c[i-1][j-1]+1;
}else if(c[i-1][j] >= c[i][j-1]){ //取上一次最大的,保证最长子序列的最长要求
c[i][j] = c[i-1][j];
}else{
c[i][j] = c[i][j-1];
}
}
}
return c[m][n];
}
0-1背包问题
也是很经典的一道算法题:0-1背包问题说的是,给定背包容量W,一系列物品{weiht,value},每个物品只能取一件,计算可以获得的value的最大值。
最优解问题,当然是我们DP,最难的一步还是状态转移方程,我们先把方程给出来,再对其进行讨论.
m[i][j] = max{ m[i-1][j-w[i]]+v[i] , m[i-1][j]}
M [i] [j]は、1,2を表し、...、I番目のアイテム、バックパックの最大容量値j、W [i]が表す物品のi番目の重み、V [i]を表し、i番目の項目値
我々は、Mを格納する2次元アレイを使用するには、iは、1,2を表し、...、I番目の項目は、Jは、バックパックの容量を表す
二つの状況、最大値は、現在計算される、項目ごとに、 :1、それらにこの記事を、この記事では触れません
- 1、我々はホールドは、...、I 1,2であり、m [I-1] [j]はバックパックの中にi番目の最大値項目ではありません、よく理解、彼らにそれを考慮していません-1記事、ナップザック容量J
あなたはそれがに行きたいので、2、[I-1] [メートル、それは以前にバックパックの良い場所に残された、袋の場合を考える JW [I]] i番目のバックパックの所与の記事を示し、場所空いたスペースは、利用可能なバックパックJW [i]は、1,2-スペースが利用可能にする、...、物品のI-1番目の最大値をM [I-1] [JW置かれている [I]を、唯一[I]に、すなわちM [I-1] [JW Vを与えるために一緒に配置する必要がある [I] + V [I]。
したがって、状態遷移方程式は、袋に採取し、2例を最大値に保持されますM [i]は[J] =最大{M [I-1] [JW [I] + V [i]は、M [I-1]〜[J]}
コードの実装
/**
* 此函数用于计算背包中能存放的最大values
* @param m m[i][j]用于记录1,2,...,i个物品在背包容量为j时候的最大value
* @param w w数组存放了每个物品的重量weight,w[i]表示第i+1个物品的weight
* @param v v数组存放了每个物品的价值value,v[i]表示第i+1个物品的value
* @param C C表示背包最大容量
* @param sum sum表示物品个数
* 状态转移方程: m[i][j] = max{ m[i-1][j-w[i]]+v[i] , m[i-1][j]}
* m[i-1][j]很好理解,就是不将第i个物品放入背包的最大value
* m[i-1][j-w[i]]+v[i]表示将第i个物品放入背包,m[i-1][j-w[i]]表示在背包中先给第i个物品把地方腾出来
* 然后背包可用空间就是j-w[i],在这些可用空间里1,2,...,i-1个物品放的最大value就是m[i-1][j-w[i]],那
* 最后再加上第i个物品的value,就是将第i个物品放入背包的最大value了
*/
public static void knap(int[][] m, int[] w,int[] v, int C, int sum){
for(int j = 0; j < C; j++){ //初始化 stuttering
if(j+1 >= w[0]){ //第一行只有一个物品,如果物品比背包容量大就放进去,否则最大value只能为0
m[0][j] = v[0];
}else{
m[0][j] = 0;
}
}
for(int i = 1; i < sum; i++){
for(int j = 0; j < C; j++){
int a = 0, b = 0; //a表示将第i个物品放入背包的value,b表示不放第i个物品
if(j >= w[i])
a = m[i-1][j-w[i]]+v[i];
b = m[i-1][j];
m[i][j] = (a>b?a:b);
}
}
}