关于动态规划的介绍很多,本文希望通过重复几个最经典的例题来理解动态规划。
1.为什么要用动态规划?
1.1动态规划解决的是哪类问题?
与分治法不同,适用于动态规划求解的问题经分解得到的子问题往往不是相互独立的。
1.2什么情况下能用动态规划?
使用动态规划需要满足以下性质1.最优子结构2.重叠子问题
1.最优子结构:当问题的最优解包含了子问题的最优解时,称该问题具有最优子结构。
2.重叠子问题:在递归求解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。
1.3为什么要用动态规划
动态规划能提高解决上述这类问题的效率。
动态规划利用子问题的重叠性质,对每一个子问题只解一次,而后将其保存在一个表格中,当再次需要解决此子问题是,只需要简单的常数时间查看结果。
2.设计动态规划算法的步骤
(1)找出最优解的性质,并刻画其结构特征。
(2)递归的定义最优值。
(3)以自低向上的方式计算出最优值。
(4)根据计算最优值得到的信息,构造最优解。
3.经典例题
问题1.求最长公共子序列
思路
事实上,最长公共子序列问题具有最优子结构性质。
设序列X={x1,x2,…,xm}和Y={y1,y2,…,yn}的最长公共子序列为Z={
z1,z2,…zk},则
(1)若xm=yn,则zk=xm=yn,且Zk-1是Xm-1和Ym-1的最长公共子序列。
(2)若xm!=yn,且zk!=xm,则Zk-1是Xm-1和Y的最长公共子序列。
(3)若xm!=yn,且zk!=yn,则Zk-1是Yn-1和Y的最长公共子序列。
令c[i][j]表示序列Xi和Yj的最长公共子序列的长度。
则由最优子结构性质可以建立递归关系如下:
1)c[i][j]=0;//当i=0,j=0
2)c[i][j]=c[i-1][j-1]+1//当i,j>0;xi=yj
3)c[i][j]=max{c[i][j-1],c[i-1][j]}//i,j>0;xi!=yj
代码实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define M 100
#define N 100
int c[M][N];// 用于保存Xi和Yi的最长公共子序列的长度
char lcstr[10000];
int lcsLength(char strx[],char stry[],int b[][N])//b[i][j]用于保存c[i][j]由哪一个子问题得到的
{
int m,n,i,j;
m=strlen(strx);
n=strlen(stry);
for(i=0;i<=m;i++)
{
c[i][0]=0;
}
for(i=1;i<=n;i++)
{
c[0][i]=0;
}
for(i=1;i<=m;i++)//用迭代
{
for(j=1;j<=n;j++)
{
if(strx[i-1]==stry[j-1])
{
c[i][j]=c[i-1][j-1]+1;
b[i][j]=1;
}else if(c[i-1][j]>=c[i][j-1])
{
c[i][j]=c[i-1][j];
b[i][j]=2;
}else{
c[i][j]=c[i][j-1];
b[i][j]=3;
}
}
}
return c[m][n];
}
//构造最长公共子序列,此函数打印最大公共子序列
void lcs(int i,int j,char strx[],int b[][N])
{
if(i==0||j==0)
{
if(b[0][0]==1)
{
printf("%c",strx[0]);
}
return ;
}
if(b[i][j]==1){
lcs(i-1,j-1,strx,b);
printf("%c",strx[i-1]);
}else if(b[i][j]==2)
{
lcs(i-1,j,strx,b);
}else
{
lcs(i,j-1,strx,b);
}
}
int main()
{
char x[]="abc";
char y[]="bc";
int record[M][N];
int result=lcsLength(x,y,record);
printf("%d\n",result);
int strlenx=strlen(x);
int strleny=strlen(y);
int i,j;
for(i=0;i<strlenx;i++)
{
for(j=0;j<strleny;j++)
{
printf("%d ",record[i][j]);
}
printf("\n");
}
lcs(strlenx,strleny,x,record);
return 0;
}
问题2 求一个字符串中的最长的回文子串
回文是指正着读和倒着读,结果一样,比如abcba或abba。
分析:
令状态方程p[i][j]=0表示起始位置为i,结束位置为j的字符串不为回文,
p[i][j]=1,表示此回文。
状态转移方程为
p[i][j]=
{
p[i][j]=1;//若p[i+1][j-1]==1且str[i]==str[j];
p[i][j]=0//其他
}
代码实现
#include <iostream>
#include <string>
using namespace std;
string longestPalindrome(string &str)
{
int length=str.size();
int start,maxlen;//最大子字符串的起点位置和长度
int len;//长度的临时变量
int p[100][100]={false};
for(int i=0;i<length;i++)
{
p[i][i]=1;
if(p[i][i]&&str[i]==str[i+1])
{
p[i][i+1]=true;
maxlen=2;
start=i;
}
}
//len从1开始还是从0开始?
//从0开始,否则当i=0,len=str.size(),i+len会越界,所以len的取值区间[0,length-1]
for(len=2;len<length;len++)
{
for(int i=0;i<length-1-len;i++)//当长度为len+1,起点i的位置最大位置length-(len+1)-1
{
int j=i+len;
if(p[i+1][j-1]&&str[i]==str[j])
{
p[i][i+len]=1;
maxlen=len+1;
start=i;
}else{
p[i][i+len]==0;
}
}
}
if(maxlen>2){
return str.substr(start,maxlen);
}
return NULL;
}
int main()
{
string str="auuabab";
cout<<longestPalindrome(str);
return 0;
}