学习笔记:动态规划
∙
\bullet
∙先来看一个问题:
小张现在有8个任务可选,每个任务都必须在规定的时间段完成不能多也不能少,而且每个任务都有对应的报酬如下图,问小张应如何选择才能拿到最多的报酬?
首先试试贪心能不能解决,怎么贪心呢?优先选报酬夺的?还是优先选时间短的?我们不妨来试试。优先选报酬多的,那么他就会选任务3和任务8,能得到12元;优先选时间短的,那么他就有很多选法,是不是报酬最多的也要看运气,所以贪心并不能解决这个问题。
∙
\bullet
∙那我们换种方法来解决这个问题吧,
首先,每个任务都有选和不选两种选择,我们从最后一个任务开始模拟这个过程。首先我们需要先用一个数组
p
r
e
pre
pre[ ],那么
p
r
e
[
i
]
pre[i]
pre[i]表示在所有编号小于
i
i
i的任务中,编号最接
i
i
i近且时间段与
i
i
i紧密相连(就是做完一个任务紧接着做下一个任务)的任务编号。那么这个数组就可以表示为下表:
i i i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
p r e [ i ] pre[i] pre[i] | 0 | 0 | 0 | 1 | 0 | 2 | 3 | 5 |
开始模拟选与不选的过程:首先我们用一个 o p t opt opt[ ]数组, o p t [ i ] opt[i] opt[i]表示第 i i i个任务能获得的报酬的最优解(此最优解不表示第 i i i个任务的报酬)
第8个任务有两种选择:选第8个任务,那么
o
p
t
[
8
]
=
o
p
t
[
p
r
e
[
8
]
]
+
任
务
8
报
酬
opt[8]=opt[pre[8]]+任务8报酬
opt[8]=opt[pre[8]]+任务8报酬,它表示如果选了第8个任务,也选了之前的某个任务,那么之前的某个任务就不能与第8个任务有时间冲突,所以这种情况下的第8个任务的报酬就是选了的之前某个任务的最优报酬+第8个任务的报酬;不选第8个任务,那么第8个任务的最优报酬就等于第7个任务最优报酬。为了便于理解,我画了一个图:
假设数组
g
a
i
n
[
i
]
gain[i]
gain[i]表示第
i
i
i个任务的最优报酬,那么上述过程就可以简化为下面这个方程:
o
p
t
[
i
]
=
m
a
x
{
o
p
t
[
p
r
e
[
i
]
]
+
g
a
i
n
[
i
]
,
选
o
p
t
[
i
−
1
]
,
不选
opt[i] =max \begin{cases} opt[pre[i]]+gain[i], & \text{选} \\ opt[i-1], & \text{不选} \end{cases}
opt[i]=max{opt[pre[i]]+gain[i],opt[i−1],选不选此过程可递归实现,注意递归出口:
o
p
t
[
0
]
=
0
,
o
p
t
[
1
]
=
g
a
i
n
[
1
]
opt[0]=0,opt[1]=gain[1]
opt[0]=0,opt[1]=gain[1].
如果数据足够大的话,递归就不行了,我们再来看上面的那幅图:
注意到递归重复算了
o
p
t
[
5
]
,
o
p
t
[
3
]
opt[5],opt[3]
opt[5],opt[3],这无疑是对时间和空间的浪费,这种问题叫做重叠子问题,我们已经递归得算出来了
o
p
t
[
5
]
opt[5]
opt[5]和
o
p
t
[
3
]
opt[3]
opt[3],那为何不存储下来,如果以后还要用到它们就可以直接取出来用就行了,这种方法就叫做记忆化存储。那我们就想办法优化一下,这次我们不从最后一个开始选了,我们就从第一个任务开始选,于是用一个循环就能解决问题(循环过程记忆化存储
o
p
t
[
i
]
opt[i]
opt[i]),时间复杂度为
O
(
n
)
O(n)
O(n)。状态转移方程如下:
o
p
t
[
i
]
=
{
m
a
x
(
o
p
t
[
p
r
e
[
i
]
]
+
g
a
i
n
[
i
]
,
o
p
t
[
i
−
1
]
)
i
>
1
g
a
i
n
[
1
]
i
=
1
0
i
=
0
opt[i] = \begin{cases} max(opt[pre[i]]+gain[i],opt[i-1] )&i>1 \\ gain[1]&i=1\\ 0&i=0 \end{cases}
opt[i]=⎩⎪⎨⎪⎧max(opt[pre[i]]+gain[i],opt[i−1])gain[1]0i>1i=1i=0
∙
\bullet
∙最后再来说一说动态规划题目得特点即基本思想:对于一个给定的问题,这个问题可以分解为若干性质相同或相似的子问题,且每个子问题都有其最优解的解决方法,那么这个大问题的最优解就可以由若干个子问题的最优解结合得到。对于上述问题,我之所以要先从最后一个任务来推,就是为了发现,这个大问题的最优解可以由它的子问题的最优解来得到,并且还发现了重叠子问题,这时就需要记忆化存储,一旦某一个子问题的最优解已解出,就需要将其记忆化存储下来,以便下次再遇到同一个子问题就可以直接查表,从而减少时间和空间复杂度。所以动态规划常用于有重叠子问题和最优子结构的问题中。 当然动态规划不常用递归来做,一旦求出了状态转移方程就可以用迭代来做。
∙ \bullet ∙如何发现一个问题是否能用动态规划来做呢,首先要明白问题的最优解结构,以及子问题的最优解结构,如果它们的最优解结构相似或者相同的话,递归的模拟一下由大问题转换为子问题的过程,看是否有重叠子问题以及递归出口等。最后再总结出来状态转移方程就好了。
之后再补上例题。。。。。
∙
\bullet
∙来个简单的例题:
在讲述DP算法的时候,一个经典的例子就是数塔问题,它是这样描述的:
有如图所示的数塔,要求从顶层走到底层,若每一步只能走到相邻的结点,则经过的结点的数字之和最大是多少?
Input
输入数据首先包括一个整数C,表示测试实例的个数,每个测试实例的第一行是一个整数N(1 <= N <= 100),表示数塔的高度,接下来用N行数字表示数塔,其中第i行有个i个整数,且所有的整数均在区间[0,99]内。
Output
对于每个测试实例,输出可能得到的最大和,每个实例的输出占一行。
Sample Input
1
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
Sample Output
30
状态转移方程:
#include <stdio.h>
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
const int maxn=105;
int mp[maxn][maxn];
int main(){
int t;
scanf("%d",&t);
while(t--)
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
scanf("%d",&mp[i][j]);
}
}
for(int i=n-1;i>=0;i--){
for(int j=1;j<=i;j++)
mp[i][j]+=max(mp[i+1][j],mp[i+1][j+1]);
}
printf("%d\n",mp[1][1]);
}
return 0;
}