通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
一、基本思想
一般来说,只要问题可以划分成规模更小的子问题,并且原问题的最优解中包含了子问题的最优解,则可以考虑用动态规划解决。动态规划的实质是分治思想和解决冗余,因此,动态规划是一种将问题实例分解为更小的、相似的子问题,并存储子问题的解而避免计算重复的子问题,以解决最优化问题的算法策略。
由此可知,动态规划法与分治法和贪心法类似,它们都是将问题实例归纳为更小的、相似的子问题,并通过求解子问题产生一个全局最优解。
其中贪心法的当前选择可能要依赖已经作出的所有选择,但不依赖于有待于做出的选择和子问题。因此贪心法自顶向下,一步一步地作出贪心选择;
而分治法中的各个子问题是独立的 (即不包含公共的子子问题),因此一旦递归地求出各子问题的解后,便可自下而上地将子问题的解合并成问题的解。
但不足的是,如果当前选择可能要依赖子问题的解时,则难以通过局部的贪心策略达到全局最优解;如果各子问题是不独立的,则分治法要做许多不必要的工作,重复地解公共的子问题。解决上述问题的办法是利用动态规划。
该方法主要应用于最优化问题,这类问题会有多种可能的解,每个解都有一个值,而动态规划找出其中最优(最大或最小)值的解。若存在若干个取最优值的解的话,它只取其中的一个。在求解过程中,该方法也是通过求解局部子问题的解达到全局最优解,但与分治法和贪心法不同的是,动态规划允许这些子问题不独立,也允许其通过自身子问题的解作出选择,该方法对每一个子问题只解一次,并将结果保存起来,避免每次碰到时都要重复计算。因此,动态规划法所针对的问题有一个显著的特征,即它所对应的子问题树中的子问题呈现大量的重复。动态规划法的关键就在于,对于重复出现的子问题,只在第一次遇到时加以求解,并把答案保存起来,让以后再遇到时直接引用,不必重新求解。
二、 案例之最长公共子序列问题
1.子序列和子串的定义:
子序列:一个给定的序列的子序列是在该序列中删除若干元素后得到的序列;
子串:字符串中任意个连续的字符组成的子序列称为该串的子串。
2.两者之间的关系:
从定义可以看出的是子串是比子序列的一个特殊情况,也就是说,子序列包含子串,而子串被包含。子串的定义出现在数据结构里面,子序列出现在算法设计与分析这本书里面。接下来用图说明。
如上图,给定的字符序列: {a,b,c,d,e,f,g,h},它的子序列示例: {a,c,e,f} 即元素b,d,g,h被去掉后,保持原有的元素序列所得到的结果就是子序列。同理,{a,h},{c,d,e}等都是它的子序列。
它的字串示例:{c,d,e,f} 即连续元素c,d,e,f组成的串是给定序列的字串。同理,{a,b,c,d},{g,h}等都是它的字串。
这个问题说明白后,最长公共子序列(以下都简称LCS)就很好理解了。
给定序列s1={1,3,4,5,6,7,7,8},s2={3,5,7,4,8,6,7,8,2},s1和s2的相同子序列,且该子序列的长度最长,即是LCS。
s1和s2的其中一个最长公共子序列是 {3,4,6,7,8}
三、算法分析
分析LCS
1.分析问题的最优子结构
以例子(S1={1,3,4,5,6,7,7,8}和S2={3,5,7,4,8,6,7,8,2}),并结合上图来说:
假如S1的最后一个元素 与 S2的最后一个元素相等,那么S1和S2的LCS就等于 {S1减去最后一个元素} 与 {S2减去最后一个元素} 的 LCS 再加上 S1和S2相等的最后一个元素。
假如S1的最后一个元素 与 S2的最后一个元素不等(本例子就是属于这种情况),那么S1和S2的LCS就等于 : {S1减去最后一个元素} 与 S2 的LCS, {S2减去最后一个元素} 与 S1 的LCS 中的最大的那个序列。
其实到这里你应该有点分治法的感觉了。
假设我们用dp[i,j]表示Xi 和 Yj 的LCS的长度(直接保存最长公共子序列的中间结果不现实,需要先借助LCS的长度)。其中X = {x1 … xm},Y ={y1…yn},Xi = {x1 … xi},Yj={y1… yj}。
2.建立递推公式:
最长公共子序列填充表
最长公共子序列路径表:
路径表解释:
上图是别的例子拿过来作为参看。意思是只有s[i]==s[j]的时候才算入公共子序列中。
四、算法实现
1.代码
string GetLCS_Ty(string str1, string str2)
{
if (str1.length() == 0||str2.length() == 0)
{
return "";
}
int nLen1 = str1.length() +1;
int nLen2 = str2.length() +1;
int **b = (int **)malloc(nLen1*sizeof(int *));
for (int j = 0; j < nLen1;j++)
{
b[j] = (int *)malloc(sizeof(int)*nLen2);
memset(b[j], 0, sizeof(int)*nLen2);
}
/*cout << "数组b初始化:\n";
for (int i = 0; i < nLen1;i++)
{
for (int j = 0; j < nLen2; j++)
{
cout << b[i][j] << " ";
}
cout << endl;
}*/
int **dp = (int **)malloc(nLen1*sizeof(int *));
memset(dp, 0, nLen1*sizeof(int *));
for (int j = 0; j < nLen1; j++)
{
dp[j] = (int *)malloc(sizeof(int)*nLen2);
memset(dp[j], 0, sizeof(int)*nLen2);
}
/*
cout << "数组dp初始化:\n";
for (int i = 0; i < nLen1; i++)
{
for (int j = 0; j < nLen2; j++)
{
cout << dp[i][j] << " ";
}
cout << endl;
}
*/
for (int i = 1; i < nLen1; i++)
{
for (int j = 1; j < nLen2; j++)
{
if (str1[i - 1] == str2[j - 1])
{
dp[i][j] = dp[i - 1][j - 1] + 1;
b[i][j] = 1; //↖
}
else if (dp[i - 1][j] > dp[i][j - 1])
{
dp[i][j] = dp[i - 1][j];
b[i][j] = 2;//↑
}
else
{
dp[i][j] = dp[i][j - 1];
b[i][j] = 3;//←
}
}
}
cout << "\n-----------------\n";
int x = nLen1-1;
int y = nLen2-1;
int k = dp[x][y];
int nclen = k;
cout << "最长公共子序列填充表:\n";
for (int i = 1; i < nLen1;i++)
{
for (int j = 1; j < nLen2;j++)
{
cout << dp[i][j] << " ";
}
cout << endl;
}
cout << endl;
cout << "最长公共子序列路径表:\n";
for (int i = 1; i < nLen1; i++)
{
for (int j = 1; j < nLen2; j++)
{
cout << b[i][j] << " ";
}
cout << endl;
}
cout << endl;
char *LCS = new char[nclen+1];
memset(LCS, 0, nclen + 1);
while (x > 0 && y > 0)
{
if (b[x][y] == 1)
{
LCS[k - 1] = str1[x - 1];
x--;
y--;
k--;
}
else if (b[x][y] == 2)
{
x--;
}
else
{
y--;
}
}
string strtmp = LCS;
free(LCS);
LCS = NULL;
for (int j = 0; j < nLen1; j++)
{
free(b[j]);
b[j] = NULL;
}
delete b;
for (int j = 0; j < nLen1; j++)
{
free(dp[j]);
dp[j] = NULL;
}
delete dp;
return strtmp;
}
void main()
{
char s1[] = "1528936";
char s2[] = "568937";
string strLCS = GetLCS_Ty(s1, s2);
cout <<"最长公共子序列:"<< strLCS.c_str() << endl;
system("pause");
}
2.测试结果