算法思想--动态规划(上)
Posted 二进制人生
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法思想--动态规划(上)相关的知识,希望对你有一定的参考价值。
基本思想:问题的最优解如果可以由子问题的最优解推导得到,则可以先求解子问题的最优解,再构造原问题的最优解。
能采用动态规划求解的问题的一般要具有3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
理论的东西我们不多说,可以看完问题再回过头来细细体味,你会发现上面的每一句句句真理。
我们从一个例子开始:
有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种走法。
比如,每次走1级台阶,一共走10步,这是其中一种走法。我们可以简写成 1,1,1,1,1,1,1,1,1,1。
再比如,每次走2级台阶,一共走5步,这是另一种走法。我们可以简写成 2,2,2,2,2。
当然,除此之外,还有很多很多种走法。
我们用动态规划问题来看看上述的问题吧:
问题建模:
假如只差一步就能走完整个楼梯,要分为几种情况?因为每一步能走一级或者两级台阶,所以有如下两种情况:
1.最后一步走2级台阶,也就是从8级到10级
2.最后一步走1级台阶,也就是从9级到10级
那么在上面的基础上假设1级到8级有X种走法,1级到9级有Y种走法,那么1级到10级有几种走法?
实际上,10级台阶的所有走法可以根据最后一步的不同分为两个部分。
第一部分:最后一步从9级到10级,这种走法的数量和1级到9级的数量一致,也就是Y种。
第二部分:最后一步从8级到10级,这种走法的数量和1级到8级的数量一致,也就是X种。
总的走法就是两种走法的总和,也就是SUM=X+Y种。
我们把10级台阶的走法表达为F(10)。
此时:
F(10) = F(9)+F(8)
F(9) = F(8)+F(7)
F(8) = F(7)+F(6)
...
F(3) = F(2)+F(1)
看到没,我们把一个复杂的问题分阶段分步的简化,简化成简单的问题,这就是动态规划的思想。
当只有1级台阶和2级台阶时走法很明显,即F(1)=1、F(2)=2,可以归纳出如下公式:
F(n) = F(n-1) + F(n-2)(n >= 3);
F(2) = 2;
F(1) = 1;
动态规划中包含三个重要的概念,最优子结构、边界、状态转移公式。
上面我们分析出F(10)=F(9)+F(8), 其中,F(9)和F(8)是F(10)的最优子结构。
当只有1级和2级台阶时,我们可以直接得出结果,而无需再次简化。我们称F(2)和F(1)是问题的"边界",如果一个问题没有边界,那么这个问题就没有有限解。
F(n) = F(n-1) + F(n-2)是阶段之间的状态转移公式,它是动态规划的核心,决定了问题的每个阶段和上阶段之间的关系。
至此,动态规划的“问题建模就完成了”。
求解问题:
采用递归的方法可以很简单的写出来:
#include <sys/time.h>
unsigned long long cal(int n)
{
if(n==1)
return 1;
else if(n==2)
return 2;
else
return cal(n-1)+cal(n-2);
}
int main()
{
int n;
scanf("%d",&n);
struct timeval start;
struct timeval end;
gettimeofday( &start, NULL );
unsigned long long res = cal(n);
gettimeofday( &end, NULL );
printf("res=%llu,cost time=%lums\n",res,end.tv_sec*1000+end.tv_usec/1000 - start.tv_sec*1000 - start.tv_usec/1000);
return 0;
}
特意计算了台阶数为45,46,47时程序运算时间,发现计算47级台阶要用44秒多的时间,每多一级台阶计算时间呈翻倍增长,这个算法明显有问题,而且采用递归形式容易栈溢出。
而慢的原因在于没有存储每一阶段的计算结果,导致同一个阶段重复计算。计算47级,就等于计算46级和45级。对于分支46级而言,其必定会计算45级和44级,所以45级被计算了两次。依次类推,44级被计算了三次……如果在计算45级时把这一结果保存下来,那么下次计算45级时就可以直接拿来用了。
不难得出优化后的算法:
#include <stdio.h>
#include <sys/time.h>
unsigned long long result[1024] = {0};
unsigned long long cal(int n)
{
if(result[n])
return result[n];
else{
result[n] = cal(n-1)+cal(n-2);
return result[n];
}
}
int main()
{
int n;
scanf("%d",&n);
struct timeval start;
struct timeval end;
result[1] = 1;
result[2] = 2;
gettimeofday( &start, NULL );
unsigned long long res = cal(n);
gettimeofday( &end, NULL );
printf("res=%llu,cost time=%lums\n",res,end.tv_sec*1000+end.tv_usec/1000 - start.tv_sec*1000 - start.tv_usec/1000);
return 0;
}
计算47级在1ms内就可以完成。
递归可以转化为递推,效率会更高,所以程序可以改成:
#include <stdio.h>
#include <sys/time.h>
unsigned long long result[1024] = {0};
int main()
{
int n;
scanf("%d",&n);
struct timeval start;
struct timeval end;
result[1] = 1;
result[2] = 2;
gettimeofday( &start, NULL );
int i = 3;
while(i<n+1)
{
result[i] = result[i-1] + result[i-2];
i++;
}
gettimeofday( &end, NULL );
printf("res=%llu,cost time=%lums\n",result[n],end.tv_sec*1000+end.tv_usec/1000 - start.tv_sec*1000 - start.tv_usec/1000);
return 0;
}
总结解题一般步骤
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。
初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。
实际应用中可以按以下几个简化的步骤进行设计:
(1)分析最优解的性质,并刻画其结构特征。
(2)递归的定义最优解。
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算最优值时得到的信息,构造问题的最优解
上面的问题比较简单,因为它是单维度的,下面引入一道多维度的问题。
输入一个字符串,求回文子串个数,或者是求最长回文子串的长度,或者是打印出所有回文子串。(所谓的回文,就是指倒过来读和顺着读一样,比如abba就是回文字符串,在这里单个字符也算回文串)
解题思路:
对于字符串hakajjakp,很明显,kajjak是一个回文子串。我们要证明它是一个回文子串,首先是判断首字母和末尾字母是否一样,接着再判断里面的字符串,直到结束或者只剩中间一个字符。这是常人的思路,用动态规划的思想来看,我们首要任务是找到状态转移公式。
一个字符串只要首尾字母相等,且中间包夹的是一个回文字符串,那么它就一定是回文字符串,所以不难得出思路:
定义一个二维数组dp,以dp[j][i]标记位置j到i组成的字符串是否是一个回文子串,如果是就将其置1,其中j=<i。
用两重循环可以不重复的遍历所有的子串。外重循环i从0开始递增遍历,内重循环j从i开始递减遍历。
仍旧以hakajjakp为例,当ij处于以下位置时,即j=1,i=6,想证明dp[j][i]是一个回文子串,只要s[i] = s[j],且它包夹的下一个子串dp[j+1][i-1]=1。巧妙之处就在于dp[j+1][i-1]是否是回文子串,已经在i的上一轮循环判断过。
h k a j j a k
j i
#include <stdio.h>
#include <string.h>
int main()
{
char s[2048];
gets(s);
char dp[2048][2048];
int res = 0,i,j,n;
n = strlen(s);
for(i=0;i<n;i++)
{
dp[i][i] = 1;
res++;
for(j=i-1;j >-1;j--)
{
if(j==i-1 && s[j]==s[i])//相邻且相等
dp[j][i] = 1;
else if(s[j] == s[i])//不相邻但相等,取决于它包含的字符串是否是回文串
dp[j][i] = dp[j+1][i-1];
else
dp[j][i] = 0;
if(dp[j][i])
res++;
}
}
printf("%d\n",res);
}
以上是关于算法思想--动态规划(上)的主要内容,如果未能解决你的问题,请参考以下文章