Dynamic Programming
(1)我们首先看一下Fibnacci Sequence(斐波那契数列)
1 1 2 3 5 8 13 21 …
用数学公式可以定义为:
f
(
x
)
=
{
1
x=0,1
f
i
b
(
n
−
1
)
+
f
i
b
(
n
−
2
)
x!=0,1
f(x)= \begin{cases} 1& \text{x=0,1}\\ fib(n-1) + fib(n-2)& \text{x!=0,1} \end{cases}
f(x)={1fib(n−1)+fib(n−2)x=0,1x!=0,1
从某一种程度上而言,上述的斐波那契数列问题是一个递归问题,同时也是一个重叠子问题,关于递归问题,我们显而易见就可以看出来,但是如何是一个重叠子问题(overlap sub-problem)呢?接下来,继续看:
假设我们求fib(6),如下图所示:
我们要明白的是,让计算机去求解一个fib(6),它只会像上图所示的从上至下进行相关的计算,看我用浅绿颜色标记出来的两个fib(4),在计算机运行中,就进行了相应的重复计算,这个算法的时间复杂度是
O
(
2
n
)
O(2^{n})
O(2n),这也就是我们上述所说的重叠子问题(overlap subproblem),那如何去解决这个重叠子问题呢?这就会体现动态规划的思想了。
我们首先看一下以下的计算方式:
同样是求fib(6)
我们先求fib(1),继而求fib(2)…然后求fib(6),每一次求前面的一个fib的时候,在内存里记录一下,然后当求下一个fib的时候,可以从内存里取出上一个fib的值,这样的话,就避免了重复计算的问题,而且这样的话,解决此问题的时间复杂度就是
O
(
n
)
O(n)
O(n)
(2),我们接下来看一个经典的类似于背包问题的问题
问题的描述是这样的:一个员工去干小时工,小时工的时间以及赋予他的工资如下图所示:
其中横轴表示的是时间,$nums,表示第几个work会收到nums回报。 问题是,这个人该如何选择工作,可以得到最大力度的回报。
好,我们接下来,来计算这个问题,首先,针对每一个work我们都有两种选择,选或者是不选,我们把最优解表示为opt(i),这个公式表示前i个的最优解。value(i)表示第i个的利润。prev(i)表示第i个work的前面所能做的work。
首先,我们先列出一个表,估计就可以看懂我说了什么个意思了。
i | prev(i) | opt(i) |
---|---|---|
1 | 0 | |
2 | 0 | |
3 | 0 | |
4 | 1 | |
5 | 0 | |
6 | 2 | |
7 | 3 | |
8 | 5 |
先忽略opt(I)。我们接下来看opt(i),这个函数:
o
p
t
(
i
)
=
m
a
x
{
选
value(i) + opt(prev(i))
不
选
opt(i-1)
opt(i)=max \begin{cases} 选& \text{value(i) + opt(prev(i))}\\ 不选 & \text{opt(i-1)} \end{cases}
opt(i)=max{选不选value(i) + opt(prev(i))opt(i-1)
然后,我们再回溯上面表格的opt(i)
i | prev(i) | opt(i) | 解释 |
---|---|---|---|
1 | 0 | 5 | max {(value(1)) + opt(prev(1)) , opt(0)} |
2 | 0 | 5 | max {(value(2)) + opt(prev(2)) , opt(1)} |
3 | 0 | 8 | max {(value(3)) + opt(prev(3)) , opt(2)} |
4 | 1 | 9 | max {(value(4)) + opt(prev(4)) , opt(3)} |
5 | 0 | 9 | max {(value(5)) + opt(prev(5)) , opt(4)} |
6 | 2 | 9 | max {(value(6)) + opt(prev(6)) , opt(5)} |
7 | 3 | 10 | max {(value(7)) + opt(prev(7)) , opt(6)} |
8 | 5 | 13 | max {(value(8)) + opt(prev(8)) , opt(7)} |
(3),ok,看到这里,估计对动态规划就有了一个简单的了解,接下来我们用编程的方式去实现一个动态规划问题,就会更加加深对动态规划问题的理解。(python实现)
存在一个数组:arr = [1,2,4,1,7,8,3],问题是这样的,在这个数组中选择不相邻的数字,使其sum和最大。
首先,我们递归实现:
写递归程序重要的一点就是找到递归出口:
这个问题的递归出口如下:
{ r e c − o p t ( 0 ) = a r r [ 0 ] r e c − o p t ( 1 ) = m a x ( a r r [ 0 ] , a r r [ 1 ] ) \begin{cases} rec-opt(0) = arr[0]\\ rec-opt(1) = max (arr[0],arr[1]) \end{cases} {rec−opt(0)=arr[0]rec−opt(1)=max(arr[0],arr[1])
arr = [1,2,4,1,7,8,3]
# i :length of arr
def rec_opt(arr,i):
if i==0:
return arr[0]
if i==1:
return max(arr[0],arr[1])
else:
A = rec_opt(arr,i-2) + arr[i]
B = rec_opt(arr,i-1)
return max(A,B)
然后,我们采用非递归的方式,也就是动态规划的思想,去用python实现这个问题:
def dp_opt(arr,i):
opt = np.zeros(len(arr))
opt[0] = arr[0]
opt[1] = max (arr[0],arr[1])
for i in range(2,len(arr)):
A = opt[i-2] + arr[i]
B = opt[i-1]
opt[i] = max(A,B)
return opt[len(arr) -1s]
(4),上述是一个一维空间上的动态规划,相对而言比较容易,下面我们看一下二维空间下的动态规划问题。这个问题是最近朋友面试的时候所遇到的一个问题,也是十分经典的一个问题,金字塔数字问题:(c++ 实现)
递归
#include<cstdio>
#include<algorithm>
using namespace std;
int n,a[1003][1003] = {0};
bool b[1003][1003];
int opt(int i,int j){
if(i == n)
return a[n][j];
if(!b[i][j]){
a[i][j] = max(opt(i+1,j),opt(i+1,j+1)) + a[i][j];
b[i][j] = true;
}
return a[i][j];
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
for(int j =1;j<=i;j++){
scanf("%d",&a[i][j]);
}
}
printf("%d",opt(1,1));
}
非递归(动态规划)
/*
[[0,0,0,0,7,0,0,0,0],
[0,0,0,3,0,8,0,0,0],
[0,0,8,0,1,0,1,0,0],
[0,2,0,7,0,4,0,0,0],
[4,0,5,0,2,0,6,0,5]]
*/
#include<cstdio>
#include<algorithm>
using namespace std;
int r,a[1002][1002],F[1002][1002];
int main(){
scanf("%d",&r);
for(int i = 1;i<=r;i++)
for(int j =1;j<=i;j++){
scanf("%d",&a[i][j]);
F[i][j] = a[i][j];
}
for(int i =r-1;i>0;i--)
for(int j=1;j<=i;j++){
F[i][j]+=max(F[i+1][j],F[i+1][j+1]);
}
printf("%d",F[1][1]);
}
(5),接下来,我们再看一个迷宫问题