来,照例,先上美图:
何谓动态规划?
简言之就是优化版的分治算法。
优化的分治?什么鬼?
动态规划的本质是分治算法,即:将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。但是一般的分治算法会产生很多的冗余计算,如下图:
可以看到,这里的F3,F2被多次计算,这就导致了算法的执行时间变长。我们是不是可以想到一个办法,用一个东西去保存已经计算出来的量,避免重复多次的计算。
OK,这就是动态规划的基本思想,即:保存已解决的子问题的结果,而在需要时直接获取已保存的结果,这样就可以避免大量的重复计算,从而很大程度上提高我们的算法效率。
先挑个软柿子,就拿斐波那契数列开刀吧…………
1. 斐波那契数列 |
- 递归算法:
#include<iostream>
using namespace std;
int fibonacci(int n)
{
if (n <= 1)
return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main()
{
int i,n;
cin >> n;
for (i = 0; i < n; i++)
{
cout << fibonacci(i) << " ";
}
cout << endl;
return 0;
}
- 动态规划:
因为斐波那契数列在使用递归计算时,会产生很多的重复计算,为了解决这个问题,我们可以设一个数组来保存已经计算出来的值,牺牲空间换时间嘛。这样,在计算时,一个数至多只需要后退两步就可以得到结果。
#include<iostream>
using namespace std;
const int N = 100;
int a[N];
void fibonacci(int n)
{
a[0] = 1;
a[1] = 1;
for (int i = 2; i <= n; i++)
{
a[i] = a[i - 2] + a[i - 1];
}
}
int main()
{
int i, n;
cin >> n;
fibonacci(n);
for (i = 0; i <=n; i++)
{
cout << a[i] << " ";
}
cout << endl;
return 0;
}
总结:
在使用递归求斐波那契数列时,采用的是普通的分治算法,计算每一个数都需要递归到它的末端,再一层一层往回返,相当于一个整体的自顶向下的过程。
在使用动态规划时,先分析出它的底层的值,然后每次只往前退两步就可以得到结果,可以看做是一个部分的自顶向下,每个值只计算一层,需要的时候直接调用,就会节省很多时间。
2. 不相邻且和最大 |
问题描述:
给定一个数组,A[0……n-1],要求得出这个数组中不相邻的数之和的最大值MAX。额,有点绕……
例:数组:A={1,2,4,1,7,8,3}
选出其中不相邻的数之和的最大值。我们可以选1、4、7、3。和的最大值就是15。
额……有点复杂,
我们可以这样想:
对于数组中的每个元素,只有两种状态,选或不选。这就好办了。拿上面的数组A={1,2,4,1,7,8,3}为例。
设一个值为OPT,表示当前的最优方案。例:OPT(6)就表示选择第6个元素3时的最佳方案。
-
对于第6个元素3:
(1)选它:MAX_1 = A[6]+OPT(4)
(2)不选他:MAX_2 = OPT(5)
(3)MAX= max(MAX_1,MAX_2)
以此类推,直至求得最后的最大值MAX。
由上图可以看出,这也是一个递归的过程,我们先写出递归方程:
- 递归出口:
出 口 = { i = 0 : O P T ( 0 ) = A [ 0 ] i = 1 : m a x ( A [ 0 ] , A [ 1 ] ) 出口 =\begin{cases}i=0:OPT(0)=A[0] \\i=1:max(A[0],A[1]) \end{cases} 出口={i=0:OPT(0)=A[0]i=1:max(A[0],A[1]) - 选与否:
O P T ( i ) = m a x { 选 : O P T ( i − 2 ) + A [ i ] 不 选 : O P T ( i − 1 ) OPT(i) =max\begin{cases}选:OPT(i-2)+A[i] \\不选:OPT(i-1) \end{cases} OPT(i)=max{选:OPT(i−2)+A[i]不选:OPT(i−1)
根据递归方程,我们可以写出递归代码:
#include<iostream>
using namespace std;
int max(int a, int b)
{
if (a > b)
return a;
else return b;
}
int fun(int *p,int i)
{
if (i == 0)
return p[0];
else if (i == 1)
{
return max(p[0], p[1]);
}
else
{
int sum_1 = fun(p, i - 2) + p[i];
int sum_2 = fun(p, i - 1);
return max(sum_1, sum_2);
}
}
int main()
{
int a[] = { 1,2,4,1,7,8,3 };
int sum = fun_f(a, 7);
cout << sum << endl;
return 0;
}
但是呢,和上面的斐波那契数列一样,它在进行递归的过程中,也会产生很多的重复计算,同样采用上面斐波那契数列的方法,设定一个数组用来保存如果选当前这个值是,它当前的最优解是多少。
开辟一个数组opt,它的大小是和要操作的数组的大小一样,我这里就简写为10。先把数组初始化为0。将两个递归出口的值填到opt数组中,然后从2~n-1进行循环操作。
-
操作:
(1)赋初值:- 当i=0时,即:当待选数组A中只有一个元素时,就只能选择它。
- 当=1时,即:待选数组A中有两个元素时,就选择两者中大较大值。
即:opt[1] = max(A[1],A[0]);
(2)元素的选或不选:
- 如果选择当前元素,则当前和的最大值就是选择与当前元素的隔一个的元素的最佳值,再加上当前元素。即:
int sum_1 = opt[i - 2] + A[i];
- 如果不选择当前元素,则当前和的最大值就是选择前一个元素的最佳值。
即:int sum_2 = opt[i - 1];
- 取选或不选的解的最大值作为当前的最优解,存到opt数组中,
即:opt[i] = max(sum_1, sum_2);
(3)最后的最优解就是opt数组的最后一个元素值。
#include<iostream>
using namespace std;
int max(int a, int b)
{
if (a > b)
return a;
else return b;
}
int fun_f(int *A, int n)
{
int opt[10] = { 0 };
opt[0] = A[0];
opt[1] = max(A[1],A[0]);
for (int i = 2; i < n; i++)
{
int sum_1 = opt[i - 2] + A[i];
int sum_2 = opt[i - 1];
opt[i] = max(sum_1, sum_2);
}
return opt[n-1];
}
int main()
{
int a[] = { 1,2,4,1,7,8,3 };
int sum = fun_f(a, 7);
cout << sum << endl;
return 0;
}
总结:
由上面两个例子可以得出,要写出这类问题的动态规划算法,要经过以下几个步骤:
- 先分许问题,找到问题中的递归出口。
- 写出递归方程。
- 判断有无重叠的冗余计算的问题,如果没有,则直接写递归代码,或将递归代码改写为非递归方式。如果有,则执行第4步。
- 用一个东西去保存已经计算出来的值,在后面需要用到时,直接调用,不用再花时间去计算。
再看一个比较难的问题,拼凑数
3. 拼凑数 |
问题描述:
给定一个数组A={3、34、4、12、5、2},再给定一个数S,如果在数组A中能拼凑得到S,则返回true,否则返回false。
呃呃呃,这还用写算法?我一眼就能看出来。。。
如果数据量很大,这个S又很大,就必须通过算法来完成。
同样,使用我们上面的选和不选的方法,设定一个OPT表示当前的拼凑结果。
比如:
在数组A中我们从后往前看,如果我们选择第6个元素2,那么当前的拼凑结果就是OPT(A[0……i-1],S-A[i]),即:
我们选择了第6个数,当前的拼凑值S就要减去第6的元素,然后在前面的数组中继续拼凑,直到找到需要的结果。
-
这同样是一个递归的过程,分析如下:
(1)递归出口:
- 当S等于0,即:已经拼凑得到结果,则直接返回true。
- 当已经匹配到数组的“最后一个元素”,即:第一个元素(因为我们是从后向前匹配的)。如果这时,如果A[0]恰好等于当前的拼凑值S,则返回true,反之则返回false。
- 如果当前匹配到的数组的值比当前的拼凑值要大,则直接在数组前面的元素中继续拼凑。
(2)元素的选或不选:
- 如果选择当前元素,则继续在前面的数组中继续拼凑,这时的拼凑值应该减去选择的元素。
- 如果不现在当前元素,则直接在前面的元素中进行拼凑,这时的拼凑值不发生变化。
由上面的分析可以写出递归方程:
- 递归出口:
O P T ( i ) = { S = 0 : t r u e i = 0 : A [ 0 ] 是 否 S , 相 等 返 回 t r u e , 反 之 为 f a l s e A [ i ] > S : O P T ( A , i − 1 , S ) OPT(i) =\begin{cases}S=0:true \\i=0:A[0]是否S,相等返回true,反之为false\\A[i]>S:OPT(A,i-1,S) \end{cases} OPT(i)=⎩⎪⎨⎪⎧S=0:truei=0:A[0]是否S,相等返回true,反之为falseA[i]>S:OPT(A,i−1,S) - 选与否:
O P T ( i ) = { 选 : O P T ( A , i − 1 , S − A [ i ] ) 不 选 : O P T ( A , i − 1 , S ) OPT(i) =\begin{cases}选:OPT(A,i-1,S-A[i]) \\不选:OPT(A,i-1,S) \end{cases} OPT(i)={选:OPT(A,i−1,S−A[i])不选:OPT(A,i−1,S)
先根据递归方程写出递归代码:
#include<iostream>
using namespace std;
int fun(int *p,int i,int S)//在前i个里找有没有之和为n的
{
if (S == 0)
return 1;
if (i == 0)
{
if (p[0] == S)
{
return 1;
}
else
{
return 0;
}
}
if (p[i] > S)
{
return fun(p, i-1, S);
}
int A = fun(p, i-1, S - p[i]);
int B = fun(p, i - 1, S);
return (A+B);
}
int main()
{
int arr[10] = { 3,34,4,12,5,2 };
int b = fun(arr, 6, 13);
if (b == 0)
cout << "false"<<endl;
else
cout << "true" << endl;
return 0;
}
分析这个题,如果采用普通的递归算法,它不可避免会产生重复的计算,所以,我们可以使用上面的方法,设定一个东西用来保存当前已经拼凑的结果。但是呢,这里,我们不能想上面那样简单的用一个一维数组来保存当前的结果。
可以使用一个二维数组 tag[M][N] 来保存选择过程中的重叠字问题。这个二维数组的行数M,就是A数组中元素的个数,列数N就是要拼凑的值S的大小。
-
设计数组tag[M][N]:
(1)初始化:由上面的递归出口可知
- 给数组所有元素都置0(false)。
- 从第二行起,每行的首元素置1(true)(即:这时拼凑值S=0),表示当前的拼凑值已经为0,则返回true。
- 第一行中,即:i=0。这种情况下,只有当A[0]与拼凑值相等时才返回true,所以给tag[0][A[0]]置1,表示true。这里需要判断A[0]是否小于S,如果大于,就会发生数组越界的情况。
(2)填剩下的表格:
- 从第一行第一列进行循环填表。这里的【j】就是当前的拼凑值。
- 如果当前的A[i] > j,就是表明匹配到的数组A[i]比当前的拼凑值还要大,就要从数组A的前面去继续拼凑,拼凑值不变。在tag数组中就表现为向上跳一行。即:
tag[i][j] = tag[i - 1][j];
。 - 否则,就用a和b分别表示是否选择当前数组A的元素。(0表示不选,非0表示选)
- 如果选择,就在数组A中继续往前匹配,拼凑值就应该减去选择的元素。即:
int a = tag[ i - 1][j - A[i]];
。 - 如果不选择,就在数组A中继续往前匹配,拼凑值不变。即:
int b = tag[i - 1][j];
.。 - 最后,将a和b取加法放到 tag[i][j] 中,(非0表示true嘛,因为C中没有纯粹的布尔值)
- 如果选择,就在数组A中继续往前匹配,拼凑值就应该减去选择的元素。即:
- 填完表格之后,这个tag数组的最后一个元素,就是拼凑的结果,0表示拼凑失败,非0表示拼凑成功。
#include<iostream>
using namespace std;
int fun_f(int *A,int n,int S)
{
int tag[100][100] = { 0 ,0};//n行S列的一个二维数组,初始化为0.
for (int i = 1; i < n; i++)//从第二行开始,每一行的第一个都置1
{
tag[i][0] = 1;
}
if (A[0] <= S)//在第一行中,只有tag[0][p[0]]置1,其它都中0.
{
tag[0][A[0]] = 1;
}
for (int i = 1; i < n; i++)
{
for (int j = 1; j <=S; j++)
{
if (A[i] > j)
{
tag[i][j] = tag[i - 1][j];
}
else
{
int a = tag[ i - 1][j - A[i]];
int b = tag[i - 1][j];
tag[i][j] = (a+b);
}
}
}
return tag[n-1][S];
}
int main()
{
int A[10] = { 3,34,4,12,5,2 };
int b = fun(A, 6, 13);
if (b == 0)
cout << "false"<<endl;
else
cout << "true" << endl;
return 0;
}
总结:从上面的三个问题可以看到,解决这类问题的方法就是,先写出递归方程,然后分许递归方程中,找到其中的冗余计算,再想办法解决这些冗余计算,就可以得到优化版的分治算法。
嗯?动态规划这么简单吗?这也太……了。
当然不是,上面这这只是解决了冗余计算的问题,俗称备忘录方法,动态规划还有一个很重要的原理:最优化原理
- 如果问题仅仅由具有交叠的子问题组成,则可以简单采用上述带有记忆功能的分治法进行求解。即:备忘录大法。
- 如果待解决问题属于最优决策问题,即:在对原问题划分为子问题或多阶段后,在每一个子问题或阶段,都有一个决策过程以获取子问题或阶段的最优。这时还必须保证通过每一个子问题或阶段的最优决策能得到原问题的最优决策,即:要求原问题满足最优化原理
- 一个典型的例子就是【矩阵连乘】
以上仅属拙见,如有错误,敬请斧正。
咳咳,等我学会了,就更新矩阵连乘。如果没学会,那就断更吧,嘿嘿。