动态规划(2)
动态规划算法设计要点
- 引入参数来界定子问题的边界. 注意子问题的重叠程度.(边界表示)
- 确定子问题依赖关系. 给出带边界参数的优化函数定义与优化函数的递推关系,找到递推关系的初值. (转移方程)
- 考虑是否需要标记函数
- 采用自底向上的实现技术,从最小的子问题开始迭代计算,计算中用备忘录保留优化函数核标记函数的值。
8.5 求解最长公共子序列问题
问题描述:求给定两个序列的最长公共子序列。字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后形成的序列。
- 如果Z是X和Y的LCS;那么任何Z的前缀都是某个X的前缀和某个Y的前缀的LCS,(最优子结构)
子问题界定
参数 i 和 j 界定子问题
X 的终止位置是 i,Y 的终止位置是 j
Xi =<x1, x2, …, xi>,Yj =<y1, y2, …, yj>
确定子问题的依赖关系–优化函数的地推方程
【问题求解】若设A=<a1, a2, …, am>,B=<b1, b2, …, bn>
定义C[i,j]=|LCS(X[1,i],Y[1,j])|
标记函数
标记函数dp[i,j], 值为
例题
输入:X=<A,B,C,B,D,A,B>, Y=<B,D,C,A,B,A>,
标记函数:
解:X[2],X[3], X[4], X[6], 即 B, C, B, A
LCS(X,Y,m,n)
1. for i←1 to m do
2. C[i,0]←0
3. for i←1 to n do
4. C[0,i]←0
5. for i←1 to m do
6. for j←1 to n do
7. if X[i]=Y[j]
8. C[i,j]←C[i-1,j-1]+1
9. B[i,j]←’↖’
10. else if C[i-1,j] ≥ C[i,j-1]
11. C[i,j]←C[i-1,j]
12. B[i,j]←’↑’
13. else
14. C[i,j]←C[i,j-1]
15. B[i,j]←’←’
利用标记函数构造解
Structure Sequence(B, i, j)
输入:B[i,j]
输出:X与Y的最长公共子序列
1. if i=0 or j=0 then return //一个序列为空
2. if B[i,j] =“↖”
3. 输出X[i]
4. Structure Sequence(B, i-1, j-1)
5. else if B[i,j]=“↑”
Structure Sequence (B, i-1, j)
6. else Structure Sequence (B, i, j-1)
算法的计算复杂度
计算优化函数和标记函数:时间为O(mn)
构造解:每一步至少缩小X 或 Y 的长度,时间Θ(m+n)
空间:Θ(mn)
定义二维动态规划数组dp
其中dp[i][j]为子序列(a0,a1,…,ai-1)和(b0,b1,…,bj-1)的最长公共子序列的长度。每考虑字符a[i]或b[j]都为动态规划的一个阶段(共经历约m×n个阶段)。
-
情况1:a[i-1]=b[j-1](当前两个字符相同)
-
情况2:a[i-1]≠b[j-1](当前两个字符不同)
dp[i][j]为子序列(a0,a1,…,ai-1)和(b0,b1,…,bj-1)的最长公共子序列的长度。 对应的状态转移方程如下:
dp[i][j]=0 i=0或j=0―边界条件 dp[i][j]=dp[i-1][j-1]+1 a[i-1]=b[j-1] dp[i][j]=MAX(dp[i][j-1],dp[i-1][j]) a[i-1]≠b[j-1]
如何求由dp求出LCS?
-
当dp[i] [j] ≠ dp[i] [j-1](左边)并且dp[][][][][][][][][][][][][][][][][i] [j] ≠ dp[i-1] [j](上方)值时:
a[i-1]=b[j-1]- 将a[i-1]添加到LCS中。
-
dp[i][j]=dp[i][j-1]:与左边相等 → j-- dp[i][j]=dp[i-1][j]:与上方相等 → i-- 与左边、上方都不相等:a[i-1]或者b[j-1]属于LCS i--,j--
-
例如,X=(a,b,c,b,d,b),m=6,Y=(a,c,b,b,a,b,d,b,b),n=9。
-
求解代码
#define MAX 51 //序列中最多的字符个数
//问题表示
int m,n;
string a,b; //求解结果表示
int dp[MAX][MAX]; //动态规划数组
vector<char> subs; //存放LCS
void LCSlength() //求dp
{ int i,j;
for (i=0;i<=m;i++) //将dp[i][0]置为0,边界条件
dp[i][0]=0;
for (j=0;j<=n;j++) //将dp[0][j]置为0,边界条件
dp[0][j]=0;
for (i=1;i<=m;i++)
for (j=1;j<=n;j++) //两重for循环处理a、b的所有字符
{ if (a[i-1]==b[j-1]) //情况(1)
dp[i][j]=dp[i-1][j-1]+1;
else //情况(2)
dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
}
void Buildsubs() //由dp构造从subs
{ int k=dp[m][n]; //k为a和b的最长公共子序列长度
int i=m;
int j=n;
while (k>0) //在subs中放入最长公共子序列(反向)
if (dp[i][j]==dp[i-1][j])
i--;
else if (dp[i][j]==dp[i][j-1])
j--;
else //与上方、左边元素值均不相等
{ subs.push_back(a[i-1]); //subs中添加a[i-1]
i--; j--; k--;
}
}
算法分析
LCSlength算法中使用了两重循环,所以对于长度分别为m和n的序列,求其最长公共子序列的时间复杂度为O(m×n)。空间复杂度为O(m×n)。
8.6 求解最长递增子序列问题
问题描述
给定一个无序的整数序列a[0…n-1],求其中最长递增子序列的长度。
例如,a[]={2,1,5,3,6,4,8,9,7},n=9,其最长递增子序列为{1,3,4,8,9},结果为5。
问题求解
设计动态规划数组为一维数组dp,dp[i]表示a[0…i]中以a[i]结尾的最长递增子序列的长度。
对应的状态转移方程如下:
dp[i]=1 0≤i≤n-1
dp[i]=max(dp[i],dp[j]+1) 若a[i]>a[j],0≤i≤n-1,0≤j≤i-1
求出dp后,其中最大元素即为所求。
求解代码
//问题表示
int a[]={2,1,5,3,6,4,8,9,7};
int n=sizeof(a)/sizeof(a[0]);
//求解结果表示
int ans=0;
int dp[MAX];
void solve(int a[],int n)
{ int i,j;
for(i=0;i<n;i++)
{ dp[i]=1;
for(j=0;j<i;j++)
{ if (a[i]>a[j])
dp[i]=max(dp[i],dp[j]+1);
}
}
ans=dp[0];#招最大
for(i=1;i<n;i++)
ans=max(ans,dp[i]);
}
算法分析
solve()算法中含两重循环,时间复杂度为O(n2)。
8.7 求解编辑距离问题
问题描述
设A和B是两个字符串。现在要用最少的字符操作次数,将字符串A转换为字符串B。这里所说的字符操作共有3种:
- 删除一个字符
- 插入一个字符
- 将一个字符替换为另一个字符
问题求解
设字符串A、B的长度分别为m、n,分别用字符串a、b存放。
-
子问题界定:a[1… n]和b[1… m]表示两个序列,子问题:a[1… i]和b[1… j],边界(i, j)
-
确定子问题间的依赖关系–优化函数的递推方程
算法的时间复杂度是O(nm)
两种特殊情况:
设计一个动态规划二维数组dp,其中dp[i] [j]表示a[0…i-1](1≤i≤m)与b[0…j-1](1≤j≤n)的最优编辑距离
- 当B串空时,要删除A中全部字符转换为B,即dp[i] [0]=i(删除A中i个字符,共i次操作);
- 当A串空时,要在A中插入B的全部字符转换为B,即dp[0] [j]=j(向A中插入B的j个字符,共j次操作)
对于非空的情况,当a[i-1]=b[j-1]时,这两个字符不需要任何操作,即dp[i] [j]=dp[i-1] [j-1]。
当a[i-1]≠b[j-1]时,以下3种操作都可以达到目的:
- 将a[i-1]替换为b[j-1],有:dp[i] [j]=dp[i-1] [j-1]+1(一次替换操作的次数计为1)。
- 在a[i-1]字符后面插入b[j-1]字符,有:dp[i][j]=dp[i][j-1]+1(一次插入操作的次数计为1)。
- 删除a[i-1]字符,有:dp[i][j]=dp[i-1][j]+1(一次删除操作的次数计为1)。
此时dp[i][j]取3种操作的最小值
算法分析
] [j]=dp[i-1] [j-1]。
当a[i-1]≠b[j-1]时,以下3种操作都可以达到目的:
- 将a[i-1]替换为b[j-1],有:dp[i] [j]=dp[i-1] [j-1]+1(一次替换操作的次数计为1)。
- 在a[i-1]字符后面插入b[j-1]字符,有:dp[i][j]=dp[i][j-1]+1(一次插入操作的次数计为1)。
- 删除a[i-1]字符,有:dp[i][j]=dp[i-1][j]+1(一次删除操作的次数计为1)。
此时dp[i][j]取3种操作的最小值
算法分析
solve()算法中有两重循环,对应的时间复杂度为O(mn)。