文章目录
千万不要以名字理解这个算法,一点都不动态。
其实主要思想就是假设我已经知道了f(n-1),f(n-2)。。。。怎么去求f(n)。
换句话说是那些f()可以到达 f(n)。f(n)=f(能到达1)+f(能到达2)…
1、 能用动态规划解决的问题
如果一个问题满足以下两点,那么它就能用动态规划解决。
1、问题的答案依赖于问题的规模,也就是问题的所有答案构成了一个数列
例如:一个人有两条腿,2个人有四条腿,n个人有几条腿?答案是2n,这里2n是问题的答案,n是问题的规模,显然问题的答案是依赖问题的规模的。答案是因变量,问题规模是自变量。因此,问题在所有规模下的答案可以构成一个数列(f(1),f(2)…f(n))。
2、大规模问题的答案可以由小规模问题的答案递推得到。
刚才的例子,腿数f(n)可以由f(n-1)求得:f(n)=f(n-1)+2
这么一看有点像递归了。
3、适合用动态规划解决的问题
较为简单的问题例如上边的例子可以用f(n)=2n的表达式直接表示,那么就不必多此一举了。但是在许多场景,f(n)不是这么容易得到,这时候动态规划的魅力就出来了。
动态规划的用法
当要应用动态规划来解决问题的时候,就是想办法完成下面三个关键目标。
1、建立状态方程
这应该是最难的一步,这一步没有太多的规律可说,只需要把握住一个思维:假装我们已经知道了f(1)~f(n-1)的值,然后让他们想办法求得f(n)。在第一个例子中转移方程即为:f(n)=f(n-1)+2
其实就是再求通项公式。。。。(高中数学题,)
缓存并复用以往的结果
我认为这是区分和递归不同的点。
他可以存储以往结果避免了重复计算,大大节省了空间复杂度。
例如上边的例子,如果我们要求f(100),但是刚才求解过f(99)。如果不将其缓存起来,那么求f(100)还需要大量的计算。如果刚刚缓存过,只需要一次运算就可以得到结果。
其实就是搞一个数组变量记录中间的结果。
按顺序从小往大算
题目:
1、斐波那契数列(简单):
斐波那契数列:1,1,2,3,5,8…
当前值为前两个的值。那么第n个值是多少?
1、简单递归
function fib(n) {
if (n<2){
return n
}else {
console.log(a++)
return fib(n-1)+fib(n-2)
}
}
这个算法现在的复杂度为O(2**n)。
因为其中有大量的重复计算,所以时间复杂度大大提高。
2. 动态规划
function dfib(n) {
let result = [];
for (let j = 0; j < n.length; j++) {
result.push(1);
} ;
for (let i = 0; i < n.length; i++) {
if (i < 2) result[i]=1;
else {
//使用状态方程,同时复用以往的结果
result[i]=result[i-1]+result[i-2]
}
return result[-1]
}
}
2、不同路径(困难)
一个机器人位于一个网格的左上角,机器人智能向下或者向右移动一步。那么有多少种方法可以到达右下角?
解这道题,先想一下怎么完成三个子目标(1、建立方程。2、缓存数据。3、由小到大求解)。
1、建立状态转移方程
这是最难的一步(想想一下我们已经知道了f(n-1),f(n-2)。。。怎么求f(n),最难的就是想出怎么定义f(n))。如图所示,第i行和第j列的格子路径数,是等于它左边格子和上面格子的路径数之和:
2、缓存以往的结果
与以往的一位数组不同,这里需要定义一个2维数组,这里的中间结果可以构成一个二维数列。(我认为一般都是f()的参数数量)。
3、按顺序从小到大算。
这次又两个维度,所以需要两次循环,分别逐行逐列的让规模变的更小。(其实因为存储数列是两维的,基本上是按照填充数列的顺序来)
具体代码:
function dpath(m, n) {
let result = [];
for (let i = 0; i < n; i++) {
result.push(mn);
for (let j = 0; j < n; j++) {
mn=[];
mn.push(1);
}
};
result.shift();
for (let i; i < m; i++) {
for (let j; j < n; j++) {
result[i][j]=result[i-1][j]+result[i][j-1]
}
}
return result[-1][-1]
}
3、爬楼梯
题目描述:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
首先还是三部:
1、动态方程(已知f(n-1),f(n-2)…求f(n))
到达f(n)只能是f(n-1),和f(n-2)
f(n)=f(n-1)+f(n-2)
2、缓存结果(一位数组,f(n)的参数是一个)
3、由小到大求解
function dpa(n) {
for (let i = 0; i < n.length ; i++) {
let b = undefined;
result.push(b);
}
for (let i=0;i<n.length;i++){
if (i<3) {
result[i]=i
}else {
result[i]=result[i-1]+result[i-2]
}
}
return result
}
4、超经典的背包问题
题目:有一个容量为 V 的背包,和一些物品。这些物品分别有两个属性,体积 w 和价值 v,每种物品只有一个。要求用这个背包装下价值尽可能多的物品,求该最大价值,背包可以不被装满。
因此定为背包容量为j时,求前i个物品所能达到最大价值。最终状态为f(i,j)
首先还是三部曲:
1、动态方程。
这里将动态方程分为三步:
步骤1-找子问题:
子问题必然是和物品有关的,对于每一个物品,有两种结果:能装下或者不能装下。第一,包的容量比物品体积小,装不下,这时的最大价值和前i-1个物品的最大价值是一样的。第二,还有足够的容量装下该物品,但是装了不一定大于当前相同体积的最优价值,所以要进行比较。由上述分析,子问题中物品数和背包容量都应当作为变量。因此子问题确定为背包容量为j时,求前i个物品所能达到最大价值。
步骤2-确定状态:
由上述分析,“状态”对应的“值”即为背包容量为j时,求前i个物品所能达到最大价值,设为dp[i][j]。初始时,dp[0]j为0,没有物品也就没有价值。
步骤3-确定状态转移方程:
由上述分析,第i个物品的体积为w,价值为v,则状态转移方程为
**注释:**假如我们想要求f(3,4),3的重量是2,那么放入3之后为f(2,1),将3的价值和f(2,1)相加即为f(3,4)加入3的状态,然后和f(3-1,4)想比较,取值大的即为f(3,4)的值。
j<w,dp[i][j] = dp[i-1][j]
//背包装不下该物品,最大价值不变
j>=w, dp[i][j] = max{ dp[i-1][j-list[i].w] + v, dp[i-1][j] }
//和不放入该物品时同样达到该体积的最大价值比较
2、缓存数据:定义一个二维数组 i j
3、重用
let num = 8;
let array1 = [2, 2, 6, 5, 4]
let array2 = [6, 3, 5, 4, 6]
function bag(num, obj1, obj2) {
let result = [];
for (let i = 0; i < num + 1; i++) {
let b = [];
result.push(b);
for (let j = 0; j < obj1.length; j++) {
b.push(0)
}
} ;
result.shift()
for (let i = 1; i < obj1.length + 1; i++) {
for (let j = 0; j < n; j++) {
if (i < array1[j]) {
result[i][j] = result[i - 1][j]
} else {
result[i][j] = Math.max(result[i - 1][i - array2[j]], result[i - 1][j])
}
}
}
return result
}