动态规划
算法总体思想
动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题。
但是经分解得到的子问题往往不是互相独立的。不同子问题的数目常常只有多项式量级。在用分治法求解时,有些子问题被重复计算了许多次。
如果能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,就可以避免大量重复计算,从而得到多项式时间算法。
动态规划基本步骤:
(1)找出最优解的性质,并刻划其结构特征。
(2)递归地定义最优值。
(3)以自底向上的方式计算出最优值。
(4)根据计算最优值时得到的信息,构造最优解。
实例一、完全加括号的矩阵连乘积
问题可递归定义:
(1)单个矩阵是完全加括号的;
(2)矩阵连乘积A是完全加括号的 ,则A可表示为2个完全加括号的矩阵连乘积B和C的乘积并加括号,即 A = (BC)。
设有四个矩阵A,B,C,D它们的维数分别是: A = 50*10 , B = 10*40 , C = 40*30 , D = 30*5
总共有五种完全加括号的方式:
例如:((A(BC))D): 10 * 40 * 30 + 10 * 30 * 50 + 50 * 30 * 5 = 34500
给定矩阵{A1, A2, A3,..., An},其中Ai与A(i+1)是可乘的。i = 1,2,3, ..., n - 1。考察这n个矩阵的连乘积A1*A2*A3...An.
由于矩阵乘法满足结合律,所以计算矩阵的连乘可以有许多不同的计算次序。这种计算次序可以用加括号的方式来确定。
若一个矩阵连乘积的计算次序完全确定,也就是说该连乘积已完全加括号,则可以依此次序反复调用2个矩阵相乘的标准算法计算出矩阵连乘积。
矩阵连乘问题
给定矩阵{A1, A2, A3,..., An},其中Ai与A(i+1)是可乘的。i = 1,2,3, ..., n - 1。考察这n个矩阵的连乘积A1*A2*A3...An. 如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。
穷举法:列举出所有可能的计算次序,并计算出每一种计算次序相应需要的数乘次数,从中找出一种数乘次数最少的计算次序。
算法复杂度分析:
对于n个矩阵的连乘积,设其不同的计算次序为P(n)
由于每种加括号方式都可以分解为两个子矩阵的加括号问题
(A1...Ak)(A(k+1)…An)可以得到关于P(n)的递推式如下:
动态规划:将矩阵连乘积A(i)A(i+1)…A(j)简记为A[i:j],这里 i <= j。
考察计算A[i:j]的最优计算次序。设这个计算次序在矩阵A(k)和A(k+1)之间将矩阵链断开,i <= k < j, 则其相应完全加括号方式为(A(i)A(i+1)...A(k)) * (A(k+1)A(k+2)...A(j))。
计算量:A[i:k]的计算量加上A[k+1,j],再加上A[i:k] * A[k+1][j]的计算量。
分析最优解的结构
特征:计算A[i:j]的最优次序所包含的计算矩阵子链 A[i:k]和A[k+1:j]的次序也是最优的。
矩阵连乘计算次序问题的最优解包含着其子问题的最优解。这种性质称为最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法求解的显著特征。
建立递归关系
设计算A[i:j],1 <= i <= j <= n,所需要的最少数乘次数m[i,j],则原问题的最优值为m[1,n]
当i = j时,A[i:j]=Ai,因此,m[i,i] = 0,i = 1,2,…,n
当i < j时,m[i,j] = m[i,k] + m[k+1,j] + p(i-1)p(k)p(j)
这里A(i)的维数为p(i-1)*p(i)(注:p(i-1)为矩阵A[i]的行数,p(i)为矩阵A[i]的列数)
可以递归地定义m[i,j]为:
k的位置只有j - i种。
计算最优值
对于1 <= i <= j <= n不同的有序对(i,j)对应于不同的子问题。因此,不同子问题的个数最多只有:
(大括号表示C(n,2),组合的意思。后面的符号表示 “紧渐近界记号”)
但是,在递归计算时,许多子问题被重复计算多次。这也是该问题可用动态规划算法求解的又一显著特征。
用动态规划算法解此问题,可依据其递归式以自底向上的方式进行计算。在计算过程中,保存已解决的子问题答案。每个子问题只计算一次,而在后面需要时只要简单查一下,从而避免大量的重复计算,最终得到多项式时间的算法。
用动态规划法求最优解
连乘矩阵假如为:
计算过程为:
从m可知最小连乘次数为m[1][6] = 15125
从s可知计算顺序为((A1(A2A3))((A4A5))A6))
实现:
- /* 主题:矩阵连乘问题
- * 作者:chinazhangjie
- * 邮箱:chinajiezhang@gmail.com
- * 开发语言:C++
- * 开发环境:Mircosoft Virsual Studio 2008
- * 时间: 2010.11.14
- */
- #include <iostream>
- #include <vector>
- using namespace std ;
- class matrix_chain
- {
- public:
- matrix_chain(const vector<int> & c) {
- cols = c ;
- count = cols.size () ;
- mc.resize (count) ;
- s.resize (count) ;
- for (int i = 0; i < count; ++ i) {
- mc[i].resize (count) ;
- s[i].resize (count) ;
- }
- for (int i = 0; i < count; ++ i) {
- for (int j = 0; j < count; ++ j) {
- mc[i][j] = 0 ;
- s[i][j] = 0 ;
- }
- }
- }
- // 使用备忘录方法计算
- void lookup_chain () {
- __lookup_chain (1, count - 1) ;
- min_count = mc[1][count - 1] ;
- cout << "min_multi_count = "<< min_count << endl ;
- // 输出最优计算次序
- __trackback (1, count - 1) ;
- }
- // 使用普通方法进行计算
- void calculate () {
- int n = count - 1; // 矩阵的个数
- // r 表示每次宽度
- // i,j表示从从矩阵i到矩阵j
- // k 表示切割位置
- for (int r = 2; r <= n; ++ r) {
- for (int i = 1; i <= n - r + 1; ++ i) {
- int j = i + r - 1 ;
- // 从矩阵i到矩阵j连乘,从i的位置切割,前半部分为0
- mc[i][j] = mc[i+1][j] + cols[i-1] * cols[i] * cols[j] ;
- s[i][j] = i ;
- for (int k = i + 1; k < j; ++ k) {
- int temp = mc[i][k] + mc[k + 1][j] +
- cols[i-1] * cols[k] * cols[j] ;
- if (temp < mc[i][j]) {
- mc[i][j] = temp ;
- s[i][j] = k ;
- }
- } // for k
- } // for i
- } // for r
- min_count = mc[1][n] ;
- cout << "min_multi_count = "<< min_count << endl ;
- // 输出最优计算次序
- __trackback (1, n) ;
- }
- private:
- int __lookup_chain (int i, int j) {
- // 该最优解已求出,直接返回
- if (mc[i][j] > 0) {
- return mc[i][j] ;
- }
- if (i == j) {
- return 0 ; // 不需要计算,直接返回
- }
- // 下面两行计算从i到j按照顺序计算的情况
- int u = __lookup_chain (i, i) + __lookup_chain (i + 1, j)
- + cols[i-1] * cols[i] * cols[j] ;
- s[i][j] = i ;
- for (int k = i + 1; k < j; ++ k) {
- int temp = __lookup_chain(i, k) + __lookup_chain(k + 1, j)
- + cols[i - 1] * cols[k] * cols[j] ;
- if (temp < u) {
- u = temp ;
- s[i][j] = k ;
- }
- }
- mc[i][j] = u ;
- return u ;
- }
- void __trackback (int i, int j) {
- if (i == j) {
- return ;
- }
- __trackback (i, s[i][j]) ;
- __trackback (s[i][j] + 1, j) ;
- cout <<i << "," << s[i][j] << " " << s[i][j] + 1 << "," << j << endl;
- }
- private:
- vector<int> cols ; // 列数
- int count ; // 矩阵个数 + 1
- vector<vector<int> > mc; // 从第i个矩阵乘到第j个矩阵最小数乘次数
- vector<vector<int> > s; // 最小数乘的切分位置
- int min_count ; // 最小数乘次数
- } ;
- int main()
- {
- // 初始化
- const int MATRIX_COUNT = 6 ;
- vector<int> c(MATRIX_COUNT + 1) ;
- c[0] = 30 ;
- c[1] = 35 ;
- c[2] = 15 ;
- c[3] = 5 ;
- c[4] = 10 ;
- c[5] = 20 ;
- c[6] = 25 ;
- matrix_chain mc (c) ;
- // mc.calculate () ;
- mc.lookup_chain () ;
- return 0 ;
- }
算法复杂度分析:
算法matrixChain的主要计算量取决于算法中对r,i和k的3重循环。循环体内的计算量为O(1),而3重循环的总次数为O(n^3)。因此算法的计算时间上界为O(n^3)。算法所占用的空间显然为O(n^2)。
动态规划算法的基本要素
一、最优子结构
矩阵连乘计算次序问题的最优解包含着其子问题的最优解。这种性质称为最优子结构性质。
在分析问题的最优子结构性质时,所用的方法具有普遍性:首先假设由问题的最优解导出的子问题的解不是最优的,然后再设法说明在这个假设下可构造出比原问题最优解更好的解,从而导致矛盾。
利用问题的最优子结构性质,以自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解。最优子结构是问题能用动态规划算法求解的前提。
同一个问题可以有多种方式刻划它的最优子结构,有些表示方法的求解速度更快(空间占用小,问题的维度低)
二、重叠子问题
递归算法求解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。这种性质称为子问题的重叠性质。
动态规划算法,对每一个子问题只解一次,而后将其解保存在一个表格中,当再次需要解此子问题时,只是简单地用常数时间查看一下结果。
通常不同的子问题个数随问题的大小呈多项式增长。因此用动态规划算法只需要多项式时间,从而获得较高的解题效率。
三、备忘录方法
备忘录方法的控制结构与直接递归方法的控制结构相同,区别在于备忘录方法为每个解过的子问题建立了备忘录以备需要时查看,避免了相同子问题的重复求解。
实现(见矩阵连乘源码)
实例二、最长公共子序列
举个例子,cnblogs这个字符串中子序列有多少个呢?很显然有27个,比如其中的cb,cgs等等都是其子序列,我们可以看出
子序列不见得一定是连续的,连续的那是子串。
我想大家已经了解了子序列的概念,那现在可以延伸到两个字符串了,那么大家能够看出:cnblogs和belong的公共子序列吗?
在你找出的公共子序列中,你能找出最长的公共子序列吗?
从图中我们看到了最长公共子序列为blog,仔细想想我们可以发现其实最长公共子序列的个数不是唯一的,可能会有两个以上,
但是长度一定是唯一的,比如这里的最长公共子序列的长度为4。
若给定的序列X = {x1,x2,…,xm},则另一序列Z = {z1,z2,…,zk},是X的子序列是指存在一个严格下表序列{i1,i2,…,ik}使得对于所有的j = 1,2,…k有zj = xij。例如,序列Z = {B,C,D,B}是序列X = {A,B,C,B,D,A,B}的子序列,相应的递增下标序列为{2,3,5,7}。
给定2个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列。
问题表述:给定2个序列X={x1,x2,…,xm}和Y = {y1,y2,…,yn},找出X和Y的最长公共子序列。
最长公共子序列的结构
设序列X = {x1,x2,…,xm}和Y = {y1,y2,…,yn}的最长公共子序列为Z = {z1,z2,…,zk} ,则
(1)若xm = yn,则zk = xm = yn,且z(k-1)是x(m-1)和y(n-1)的最长公共子序列。
(2)若xm != yn且zk != xm,则Z是x(m-1)和Y的最长公共子序列。
(3)若xm != yn且zk != yn,则Z是X和y(n-1)的最长公共子序列。
由此可见,2个序列的最长公共子序列包含了这2个序列的前缀的最长公共子序列。因此,最长公共子序列问题具有最优子结构性质。
子问题的递归结构
由最长公共子序列问题的最优子结构性质建立子问题最优值的递归关系。用c[i][j]记录序列Xi和Yi的最长公共子序列的长度。其中, Xi={x1,x2,…,xi};Yj={y1,y2,…,yj}。当i = 0或j = 0时,空序列是Xi和Yj的最长公共子序列。故此时C[i][j] = 0。其它情况下,由最优子结构性质可建立递归关系如下:
由于在所考虑的子问题空间中,总共有θ(mn)个不同的子问题,因此,用动态规划算法自底向上地计算最优值能提高算法的效率
计算最优值和构造最长公共子序列(见源码)
实现:
- /* 主题:最长公共子序列
- * 作者:chinazhangjie
- * 邮箱:chinajiezhang@gmail.com
- * 开发语言:C++
- * 开发环境:Microsoft Visual Studio 2008
- * 时间: 2010.11.14
- */
- #include <iostream>
- #include <vector>
- using namespace std ;
- // longest common sequence
- class LonComSequence
- {
- public:
- typedef vector<vector<int> > LCS_Type ;
- typedef vector<vector<int> > MarkType ;
- public:
- LonComSequence (const vector<char>& vSeq1,
- const vector<char>& vSeq2)
- : mc_nEqual (1), mc_nSq1move(2), mc_nSq2move(3)
- {
- m_vSeq1 = vSeq1 ;
- m_vSeq2 = vSeq2 ;
- m_nLen1 = vSeq1.size() ;
- m_nLen2 = vSeq2.size() ;
- // 初始化最长公共子序列的长度
- m_lcsLen.resize (m_nLen1 + 1) ;
- m_mtMark.resize (m_nLen1 + 1) ;
- for (int i = 0; i < m_nLen1 + 1; ++ i) {
- m_lcsLen[i].resize (m_nLen2 + 1) ;
- m_mtMark[i].resize (m_nLen2 + 1) ;
- }
- }
- // 计算最长公共子序列的长度
- int calLcsLength ()
- {
- for (int i = 1; i <= m_nLen1; ++ i) {
- m_lcsLen[i][0] = 0 ; // 序列二的长度为0,公共子序列的长度为0
- }
- for (int i = 1; i <= m_nLen2; ++ i) {
- m_lcsLen[0][i] = 0 ; // 序列一的长度为0,公共子序列的长度为0
- }
- for (int i = 0; i < m_nLen1; ++ i) {
- for (int j = 0; j < m_nLen2; ++ j) {
- if (m_vSeq1[i] == m_vSeq2[j]) {
- m_lcsLen[i+1][j+1] = m_lcsLen[i][j] + 1 ;
- m_mtMark[i+1][j+1] = mc_nEqual ;
- }
- else if (m_lcsLen[i][j+1] >= m_lcsLen[i+1][j]) {
- m_lcsLen[i+1][j+1] = m_lcsLen[i][j+1] ;
- m_mtMark[i+1][j+1] = mc_nSq1move ;
- }
- else {
- m_lcsLen[i+1][j+1] = m_lcsLen[i+1][j] ;
- m_mtMark[i+1][j+1] = mc_nSq2move ;
- }
- }
- }
- return m_lcsLen[m_nLen1][m_nLen2] ;
- }
- // 构造最长公共子序列
- void LCS() {
- cout << "LCS is : " ;
- __LCS(m_nLen1, m_nLen2);
- cout << endl ;
- }
- private:
- void __LCS (int i, int j)
- {
- if (i == 0 || j == 0) {
- return ;
- }
- if (m_mtMark[i][j] == mc_nEqual) {
- __LCS (i - 1, j - 1) ;
- cout << m_vSeq1[i - 1] << " " ;
- }
- else if (m_mtMark[i][j] == mc_nSq1move) {
- __LCS (i - 1, j) ;
- }
- else {
- __LCS (i, j - 1) ;
- }
- }
- private:
- vector<char> m_vSeq1 ; // 序列一
- vector<char> m_vSeq2 ; // 序列二
- int m_nLen1 ; // 序列一的长度
- int m_nLen2 ; // 序列二的长度
- LCS_Type m_lcsLen ; // 最长公共子序列的长度
- MarkType m_mtMark ; // 记录m_lcsLen
- const int mc_nEqual ; // 相等的标志
- const int mc_nSq1move ; // 序列一左移的标志
- const int mc_nSq2move ; // 序列二左移的标志
- } ;
- int main()
- {
- vector<char> s1 ;
- s1.push_back ('A') ;
- s1.push_back ('B') ;
- s1.push_back ('C') ;
- s1.push_back ('D') ;
- s1.push_back ('E') ;
- s1.push_back ('F') ;
- vector<char> s2 ;
- s2.push_back ('B') ;
- s2.push_back ('D') ;
- s2.push_back ('F') ;
- s2.push_back ('G') ;
- s2.push_back ('H') ;
- LonComSequence lcs(s1, s2) ;
- cout << lcs.calLcsLength () << endl ;
- lcs.LCS();
- return 0 ;
- }
算法的改进
在算法lcsLength和lcs中,可进一步将数组b省去。事实上,数组元素c[i][j]的值仅由c[i-1][j-1],c[i-1][j]和c[i][j-1]这3个数组元素的值所确定。对于给定的数组元素c[i][j],可以不借助于数组b而仅借助于c本身在时间内确定c[i][j]的值是由c[i-1][j-1],c[i-1][j]和c[i][j-1]中哪一个值所确定的。
如果只需要计算最长公共子序列的长度,则算法的空间需求可大大减少。事实上,在计算c[i][j]时,只用到数组c的第i行和第i-1行。因此,用2行的数组空间就可以计算出最长公共子序列的长度。进一步的分析还可将空间需求减至O(min(m,n))。
长度的问题我们已经解决了,这次要解决输出最长子序列的问题,
我们采用一个标记函数Flag[i,j],当
①:C[i,j]=C[i-1,j-1]+1 时 标记Flag[i,j]="left_up"; (左上方箭头)
②:C[i-1,j]>=C[i,j-1] 时 标记Flag[i,j]="left"; (左箭头)
③: C[i-1,j]<C[i,j-1] 时 标记Flag[i,j]="up"; (上箭头)
例如:我输入两个序列X=acgbfhk,Y=cegefkh。
1 using System; 2 3 namespace ConsoleApplication2 4 { 5 public class Program 6 { 7 static int[,] martix; 8 9 static string[,] flag; 10 11 static string str1 = "acgbfhk"; 12 13 static string str2 = "cegefkh"; 14 15 static void Main(string[] args) 16 { 17 martix = new int[str1.Length + 1, str2.Length + 1]; 18 19 flag = new string[str1.Length + 1, str2.Length + 1]; 20 21 LCS(str1, str2); 22 23 //打印子序列 24 SubSequence(str1.Length, str2.Length); 25 26 Console.Read(); 27 } 28 29 static void LCS(string str1, string str2) 30 { 31 //初始化边界,过滤掉0的情况 32 for (int i = 0; i <= str1.Length; i++) 33 martix[i, 0] = 0; 34 35 for (int j = 0; j <= str2.Length; j++) 36 martix[0, j] = 0; 37 38 //填充矩阵 39 for (int i = 1; i <= str1.Length; i++) 40 { 41 for (int j = 1; j <= str2.Length; j++) 42 { 43 //相等的情况 44 if (str1[i - 1] == str2[j - 1]) 45 { 46 martix[i, j] = martix[i - 1, j - 1] + 1; 47 flag[i, j] = "left_up"; 48 } 49 else 50 { 51 //比较“左边”和“上边“,根据其max来填充 52 if (martix[i - 1, j] >= martix[i, j - 1]) 53 { 54 martix[i, j] = martix[i - 1, j]; 55 flag[i, j] = "left"; 56 } 57 else 58 { 59 martix[i, j] = martix[i, j - 1]; 60 flag[i, j] = "up"; 61 } 62 } 63 } 64 } 65 } 66 67 static void SubSequence(int i, int j) 68 { 69 if (i == 0 || j == 0) 70 return; 71 72 if (flag[i, j] == "left_up") 73 { 74 Console.WriteLine("{0}: 当前坐标:({1},{2})", str2[j - 1], i - 1, j - 1); 75 76 //左前方 77 SubSequence(i - 1, j - 1); 78 } 79 else 80 { 81 if (flag[i, j] == "up") 82 { 83 SubSequence(i, j - 1); 84 } 85 else 86 { 87 SubSequence(i - 1, j); 88 } 89 } 90 } 91 } 92 }
由于直接绘图很麻烦,嘿嘿,我就用手机拍了张:
好,我们再输入两个字符串:
1 static string str1 = "abcbdab"; 2 3 static string str2 = "bdcaba";
通过上面的两张图,我们来分析下它的时间复杂度和空间复杂度。
时间复杂度:构建矩阵我们花费了O(MN)的时间,回溯时我们花费了O(M+N)的时间,两者相加最终我们花费了O(MN)的时间。
空间复杂度:构建矩阵我们花费了O(MN)的空间,标记函数也花费了O(MN)的空间,两者相加最终我们花费了O(MN)的空间。
实例三、最大子段和
问题表述
n个数(可能是负数)组成的序列a1,a2,…an.求该序列
例如: 序列(-2,11,-4,13,-5,-2) ,最大子段和:
11 - 4 + 13=20。
(1)穷举算法: O(n3), O(n2)
(2)分治法:
将序列a[1:n]从n/2处截成两段:a[1:n/2], a[n/2+1:n]
实例三、最大子段和
问题表述
n个数(可能是负数)组成的序列a1,a2,…an.求该序列 子序列的最大值。
也就是
例如: 序列(-2,11,-4,13,-5,-2) ,最大子段和:
11 - 4 + 13=20。
(1)穷举算法: O(n3), O(n2)
(2)分治法:
将序列a[1:n]从n/2处截成两段:a[1:n/2], a[n/2+1:n]
一共存在三种情况:
a.最大子段和出现在左边一段
b.最大子段和出现在右边一段
c.最大子段和跨越中间的断点
对于前两种情况,只需继续递归调用,而对于第三种情况:
那么S1+S2是第三种情况的最优值。
(3)动态规划法:
定义b[j]:
含义:从元素i开始,到元素j为止的所有的元素构成的子段有多个,这些子段中的子段和最大的那个。
那么:
如果:b[j-1] > 0, 那么b[j] = b[j-1] + a[j]
如果:b[j-1] <= 0,那么b[j] = a[j]
这样,显然,我们要求的最大子段和,是b[j]数组中最大的那个元素。
实现:
- /* 主题:最大子段和
- * 作者:chinazhangjie
- * 邮箱:chinajiezhang@gmail.com
- * 开发语言:C++
- * 开发环境:Microsoft Virsual Studio 2008
- * 时间: 2010.11.15
- */
- #include <iostream>
- #include <vector>
- using namespace std ;
- class MaxSubSum
- {
- public:
- MaxSubSum (const vector<int>& intArr)
- {
- m_vIntArr = intArr ;
- m_nLen = m_vIntArr.size () ;
- }
- // use divide and conquer
- int use_DAC ()
- {
- m_nMssValue = __use_DAC (0, m_nLen - 1) ;
- return m_nMssValue ;
- }
- // use dynamic programming
- int use_DP ()
- {
- int sum = 0 ;
- int temp = 0 ;
- for (int i = 0; i < m_nLen; ++ i) {
- if (temp > 0) {
- temp += m_vIntArr[i] ;
- }
- else {
- temp = m_vIntArr[i] ;
- }
- if (temp > sum) {
- sum = temp ;
- }
- }
- m_nMssValue = sum ;
- return sum ;
- }
- private:
- int __use_DAC (int left, int right)
- {
- // cout << left << "," << right << endl ;
- if (left == right) {
- return (m_vIntArr[left] > 0 ? m_vIntArr[left] : 0) ;
- }
- // 左边区域的最大子段和
- int leftSum = __use_DAC (left, (left + right) / 2) ;
- // 右边区域的最大子段和
- int rightSum = __use_DAC ((left + right) / 2 + 1, right) ;
- // 中间区域的最大子段和
- int sum1 = 0 ;
- int max1 = 0 ;
- int sum2 = 0 ;
- int max2 = 0 ;
- for (int i = (left + right) / 2; i >= left; -- i) {
- sum1 += m_vIntArr[i] ;
- if (sum1 > max1) {
- max1 = sum1 ;
- }
- }
- for (int i = (left + right) / 2 + 1; i <= right; ++ i) {
- sum2 += m_vIntArr[i] ;
- if (sum2 > max2) {
- max2 = sum2 ;
- }
- }
- int max0 = max1 + max2 ;
- max0 = (max0 > 0 ? max0 : 0) ;
- // cout << max0 << ", " << leftSum << ", " << rightSum << endl ;
- return max (max0 , max (leftSum, rightSum)) ;
- }
- private:
- vector<int> m_vIntArr ; // 整形序列
- int m_nLen ; // 序列长度
- int m_nMssValue;// 最大子段和
- } ;
- int main()
- {
- vector<int> vArr ;
- vArr.push_back (-2) ;
- vArr.push_back (11) ;
- vArr.push_back (-4) ;
- vArr.push_back (13) ;
- vArr.push_back (-5) ;
- vArr.push_back (-2) ;
- MaxSubSum mss (vArr) ;
- cout << mss.use_DP () << endl ;
- return 0 ;
- }
实例四、多边形游戏
多边形游戏是一个单人玩的游戏,开始时有一个由n个顶点构成的多边形。每个顶点被赋予一个整数值,每条边被赋予一个运算符”+”或”*”。所有边依次用整数从1到n编号。
游戏第1步,将一条边删除。
随后n-1步按以下方式操作:
(1)选择一条边E以及由E连接着的2个顶点V1和V2;
(2)用一个新的顶点取代边E以及由E连接着的2个顶点V1和V2。将由顶点V1和V2的整数值通过边E上的运算得到的结果赋予新顶点。
最后,所有边都被删除,游戏结束。游戏的得分就是所剩顶点上的整数值。
问题: 对于给定的多边形,计算最高得分。
最优子结构性质
按照顺时针顺序,多边形和顶点的顺序可以写成:
op[1], v[1], op[2], v[2], …, op[n], v[n]
在所给多边形中,从顶点i(1 <= i <= n)开始,长度为j(链中有j个顶点)的顺时针链p(i,j) 可表示为
v[i], op[i+1], v[i+1],…, op[i+j-1], v[i+j-1]
如果这条链在op[i + s]处进行最后一次合并运算(1 <= s <= j-1),则可在op[i+s]处将链分割为2个子链:
从i开始长度为s的链: p(i,s)
从i + s开始,长度为j - s的链:p(i + s,j-s)。
设:
m1是对子链p(i,s)的任意一种合并方式得到的值,而a和b分别是在所有可能的合并中得到的最小值和最大值。
m2是p(i+s,j-s)的任意一种合并方式得到的值,而c和d分别是在所有可能的合并中得到的最小值和最大值。
依此定义有a <= m1 <= b,c <= m2 <= d
(1)当op[i+s] = ‘+’时,显然有a + c <= m <= b + d
(2)当op[i+s] = ’*’时,有
min {ac,ad,bc,bd} <= m <= max {ac,ad,bc,bd}
换句话说,主链的最大值和最小值可由子链的最大值和最小值得到。
实现:
- /* 主题:多边形游戏
- * 作者:chinazhangjie
- * 邮箱:chinajiezhang@gmail.com
- * 开发语言:C++
- * 开发环境:Vicrosoft Visual Studio
- * 时间: 2010.11.15
- */
- #include <iostream>
- #include <vector>
- using namespace std ;
- struct SegInfo
- {
- public:
- SegInfo ()
- : m_nMaxValue (0), m_nMinValue(0)
- {}
- SegInfo (int maxValue, int minValue)
- : m_nMaxValue (maxValue), m_nMinValue (minValue)
- {}
- public:
- int m_nMaxValue ;
- int m_nMinValue ;
- } ;
- class PolyGame
- {
- public:
- PolyGame (const vector<char>& op, const vector<int>& vertex)
- {
- m_vcOp = op ;
- m_vnVertex = vertex ;
- m_nCount = m_vcOp.size () ;
- m_vSeg.resize (m_nCount) ;
- for (int i = 0; i < m_nCount; ++ i) {
- m_vSeg[i].resize (m_nCount) ;
- }
- }
- int beginCalulate ()
- {
- // 初始边界
- for (int i = 1; i < m_nCount; ++ i) {
- m_vSeg[i][1].m_nMaxValue = m_vnVertex[i] ;
- m_vSeg[i][1].m_nMinValue = m_vnVertex[i] ;
- }
- // i: 起点
- // j: 长度
- // s: 子切分位置
- for (int j = 2; j < m_nCount ; ++ j) {
- for (int i = 1; i < m_nCount; ++ i) {
- for (int s = 1; s < j; ++ s) {
- SegInfo si = __calMinAndMax(i, s, j) ;
- if (m_vSeg[i][j].m_nMinValue > si.m_nMinValue) {
- m_vSeg[i][j].m_nMinValue = si.m_nMinValue ;
- }
- if (m_vSeg[i][j].m_nMaxValue < si.m_nMaxValue) {
- m_vSeg[i][j].m_nMaxValue = si.m_nMaxValue ;
- }
- }
- }
- }
- // 找到最大值
- int temp = m_vSeg[1][m_nCount - 1].m_nMaxValue ;
- for (int i = 2; i < m_nCount; ++ i) {
- if (temp < m_vSeg[i][m_nCount - 1].m_nMaxValue) {
- temp = m_vSeg[i][m_nCount - 1].m_nMaxValue ;
- }
- }
- m_nResult = temp ;
- return m_nResult ;
- }
- private:
- // 从i开始,长度为j,s为切分位置
- SegInfo __calMinAndMax (int i, int s, int j)
- {
- int minL = 0 ;
- int maxL = 0 ;
- int minR = 0 ;
- int maxR = 0 ;
- minL = m_vSeg[i][s].m_nMinValue ;
- maxL = m_vSeg[i][s].m_nMaxValue ;
- int r = (i + s - 1) % (m_nCount - 1) + 1 ;
- minR = m_vSeg[r][j - s].m_nMinValue ;
- maxR = m_vSeg[r][j - s].m_nMaxValue ;
- SegInfo si ;
- // 处理加法
- if (m_vcOp[r] == '+') {
- si.m_nMinValue = minL + minR ;
- si.m_nMaxValue = maxL + maxR ;
- }
- else { // 处理乘法
- vector<int> mm ;
- mm.push_back (minL * minR) ;
- mm.push_back (minL * maxR) ;
- mm.push_back (maxL * minR) ;
- mm.push_back (maxL * maxR) ;
- int min = 0 ;
- int max = 0 ;
- for (vector<int>::iterator ite = mm.begin();
- ite != mm.end() ; ++ ite) {
- if (*ite < min) {
- min = *ite ;
- }
- if (*ite > max) {
- max = *ite ;
- }
- }
- si.m_nMinValue = min ;
- si.m_nMaxValue = max ;
- }
- return si ;
- }
- private :
- vector<char> m_vcOp ; // 运算符(下标从1开始)
- vector<int> m_vnVertex ;// 顶点值(下标从1开始)
- int m_nCount ; // 边的个数
- int m_nResult ; // 结果
- vector<vector<SegInfo> > m_vSeg ;// 合并后的信息
- } ;
- int main()
- {
- const int cnCount = 5 ;
- vector<char> op (cnCount + 1);
- vector<int> vertex (cnCount + 1);
- op[1] = '+' ;
- op[2] = '*' ;
- op[3] = '+' ;
- op[4] = '*' ;
- op[5] = '*' ;
- vertex[1] = 10 ;
- vertex[2] = -8 ;
- vertex[3] = 3;
- vertex[4] = -2 ;
- vertex[5] = -1 ;
- PolyGame pg (op, vertex) ;
- cout << pg.beginCalulate () << endl ;
- }