一、动态规划的基本思想

  动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。

  将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。为了达到此目的,我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。

二、动态规划的基本要素(特征)

  1、最优子结构:

  当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。

  2、重叠子问题:

   在用递归算法自顶向下解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一个表格中,在以后尽可能多地利用这些子问题的解。

三、用动态规划求解问题的主要步骤

  1、找出最优解的性质,并刻画其结构特征;

  2、递归地定义最优值(写出动态规划方程);

  3、以自底向上的方式计算出最优值;

  4、根据计算最优值时得到的信息,构造一个最优解。

  说明:(1)步骤 1~3 是动态规划算法的基本步骤;
     (2)在只需要求出最优值的情形,步骤 4 可以省略;
     (3)若需要求出问题的一个最优解,则必须执行步骤 4。

四、动态规划实例

  1、矩阵连乘问题

  m × n 矩阵 A 与 n × p 矩阵 B 相乘需耗费的时间。我们把 m x n x p 作为两个矩阵相乘所需时间的测量值。

  现在假定要计算三个矩阵 A、B  和 C 的乘积,有两种方式计算此乘积。

  (1)先用 A 乘以 B 得到矩阵 D,然后 D 乘以 C 得到最终结果,这种乘法的顺序为(AB)C;

  (2)先用 B 乘以 C 得到矩阵 E,然后 E 乘以 A 得到最终结果,这种乘法的顺序为 A(BC)。

  尽管这两种不同的计算顺序所得的结果相同,但时间消耗会有很大的差距。

  实例:

图1.1 A、B 和 C 矩阵

  矩阵乘法符合结合律,所以在计算 ABC 矩阵连乘时,有两种方案,即 (AB)C  和 A(BC)。

  对于第一方案(AB)C  和,计算:

图1.2 AB 矩阵相乘

  其乘法运算次数为:2 × 3 × 2 = 12

图1.3 (AB)C 矩阵连乘

  

  其乘法运算次数为:2 × 2 × 4 = 16
  总计算量为:12 + 16 = 28 

  对第二方案 A(BC),计算:

图1.4 BC 矩阵相乘

  其乘法运算次数为:3 × 2 × 4 = 24

图1.5  A、B 和 C  矩阵连乘

  其乘法运算次数为:2 × 3 × 4 = 24

  总计算量为:24 + 24 = 48

  可见,不同方案的乘法运算量可能相差很悬殊。

  问题定义:

  给定 n 个矩阵 {A1, A2, …, An},其中 A与 Ai+1 是可乘的,i = 1,2,…,n-1。考察这 n 个矩阵的连乘积 A1A2…An。 由于矩阵乘法满足结合律,所以计算矩阵的连乘可以有许多不同的计算次序。

  这种计算次序可以用加括号的方式来确定。完全加括号的矩阵连乘积可递归地定义为:

  (1)单个矩阵是完全加括号的;

  (2)矩阵连乘积 A 是完全加括号的,则 A 可表示为 2 个完全加括号的矩阵连乘积 B 和 C 的乘积并加括号,即 A = (BC)。设有四个矩阵 A, B, C, D,总共有五种完全加括号的方式:   (A((BC)D))  ,  (A(B(CD)))  ,  ((AB)(CD))  ,  (((AB)C)D)  ,  ((A(BC)D))。

  a. 找出最优解的性质,并刻画其结构特征;

  将矩阵连乘积 AiAi+1…Aj ,简记为 A[i : j], 这里 i≤j;考察计算 A[1:n] 的最优计算次序。

  设这个计算次序在矩阵 Ak 和 Ak+1 之间将矩阵链断开,1 ≤ k < n,则其相应完全加括号方式为 (A1A2…Ak)(Ak+1Ak+2…An)。

  总计算量 = A[1:k] 的计算量 +  A[k+1:n] 的计算量 + A[1:k] 和 A[k+1:n]相乘的计算量

  特征:计算 A[1:n] 的最优次序所包含的计算矩阵子链 A[1:k] 和 A[k+1:n] 的次序也是最优的。

  b. 递归地定义最优值(写出动态规划方程);

图1.6 建立递归关系

  c. 以自底向上的方式计算出最优值。

 #include <iostream>
using namespace std; #define NUM 51
int p[NUM]; //矩阵维数 P0 x P1,P1 x P2,P2 x P3,...,P5 x P6
int m[NUM][NUM]; //最少乘次数 / 最优值数组
int s[NUM][NUM]; //最优断开位置 void MatrixChain(int n)
{
for (int i = ; i <= n; i++) m[i][i] = ; for (int r = ; r <= n; r++) //矩阵个数
for (int i = ; i <= n - r+; i++) //起始
{
int j=i+r-; //结尾
m[i][j] = m[i+][j] + p[i-]*p[i]*p[j]; //计算初值,从i处断开,计算最优断开位置
s[i][j] = i;
for (int k = i+; k < j; k++)
{
int t = m[i][k] + m[k+][j] + p[i-]*p[k]*p[j];
if (t < m[i][j]) { m[i][j] = t; s[i][j] = k;}
}
}
} void TraceBack(int i, int j)
{
if(i==j)
cout<< "A" << i;
else
{
cout << "(";
TraceBack(i, s[i][j]);
TraceBack(s[i][j]+, j);
cout << ")";
}
} int main()
{
int n;
cin >> n; //矩阵的个数
int temp;
for(int i = ; i < n; i++)
cin >> p[i] >> temp; //矩阵的维数
p[n] = temp;
MatrixChain(n);
cout << m[][n] << endl; //最少乘次数
TraceBack(, n); //按照最优断开位置列出乘法顺序
return ;
}

  2、矩阵连乘之备忘录方法

  备忘录方法是动态规划算法的变形。与动态规划算法一样,备忘录方法用一个表格保存已解决的子问题的答案,再碰到该子问题时,只要简单地查看该子问题的解答,而不必重新求解。备忘录方法的控制结构与直接递归方法的控制结构相同,区别仅在于备忘录方法为每个解过的子问题建立了备忘录以备需要时查看,避免了相同子问题的重复求解。

 #include <iostream>
using namespace std; #define NUM 51
int p[NUM]; //矩阵维数 P0 x P1,P1 x P2,P2 x P3,...,P5 x P6
int m[NUM][NUM]; //最少乘次数 / 最优值数组
int s[NUM][NUM]; //最优断开位置 int LookupChain(int i, int j)
{
if(m[i][j] > ) return m[i][j];
if(i == j) return ;
int u = LookupChain(i, i) + LookupChain(i+, j) + p[i-]*p[i]*p[j];
s[i][j] = i;
for(int k = i+; k < j; k++)
{
int t = LookupChain(i, k) + LookupChain(k+, j) + p[i-]*p[k]*p[j];
if(t < u) {u = t; s[i][j] = k;}
}
m[i][j] = u;
return u;
} int MemoizedMatrixChain(int n)
{
for(int i = ; i <= n; i++)
for(int j = i; j <= n; j++) m[i][j] = ;
return LookupChain(, n); } void TraceBack(int i, int j)
{
if(i==j)
cout<< "A" << i;
else
{
cout << "(";
TraceBack(i, s[i][j]);
TraceBack(s[i][j]+, j);
cout << ")";
}
} int main()
{
int n;
cin >> n; //矩阵的个数
int temp;
for(int i = ; i < n; i++)
cin >> p[i] >> temp; //矩阵的维数
p[n] = temp;
MemoizedMatrixChain(n);
cout << m[][n] << endl; //最少乘次数
TraceBack(, n); //按照最优断开位置列出乘法顺序
return ;
}

  动态规划与备忘录方法比较:

  (1)相同点

  这两种算法都利用了子问题重叠性质。对每个子问题,两种方法都只解一次,并记录答案。再次遇到该子问题时,不重新求解而简单地取用已得到的答案,节省了计算量,提高了算法的效率。

  (2)不同点

  动态规划是自底向上的方式计算;备忘录是自顶向下的方式计算。

  当一个问题的所有子问题都至少要解一次时,用动态规划算法比用备忘录方法好;当子问题中的部分子问题可不必求解时,用备忘录方法则较有利,因为从其控制结构可以看出,该方法只解那些需要求解的子问题。

  3、最长公共子序列

  最长公共子序列的结构:

  设序列 X = {x1,x2,…,xm} 和 Y = {y1,y2,…,yn}  的最长公共子序列为 Z =  {z1,z2,…,zk},则
    a. 若 x= yn,则 z= x= yn,且 zk-1 是 xm-1 和 yn-1 的最长公共子序列;
    b. 若 xm ≠ yn 且 zk ≠ xm,则 Z 是 xm-1 和 Y 的最长公共子序列;
    c. 若 xm ≠ yn 且 zk ≠ yn,则 Z 是 X 和 yn-1 的最长公共子序列。

  总结:(1)两个序列的最长公共子序列包含了这两个序列的前缀的最长公共子序列;

     (2)最长公共子序列问题具有最优子结构性质。

  子问题的递归结构:

  由最长公共子序列问题的最优子结构性质可知,要找出 X 和 Y 的最长公共子序列,可按以下方式递归地进行:

    a. 当 xm = yn 时,找出 Xm-1 和 Yn-1 的最长公共子序列,然后在其尾部加上 xm(=yn)即可得 X 和 Y 的一个最长公共子序列;

    b. 当 xm ≠ yn 时,必须解两个子问题,即找出 Xm-1 和 Y 的一个最长公共子序列及 X 和 Yn-1 的一个最长公共子序列。

  这两个公共子序列中较长者为 X 和 Y 的一个最长公共子序列。

  用 c[i][j] 记录序列 Xi 和 Yj 的最长公共子序列的长度。Xi = {x1,x2,…,xi},Yj = {y1,y2,…,yj}。

  当 i = 0 或 j = 0 时,空序列是 Xi 和 Yj 的最长公共子序列,故此时 C[i][j] = 0。其它情况下,由最优子结构性质可建立递归关系如下:

图3.1 由最优子结构性质建立递归关系

  例如:X = {A,B,C,B,D,A,B},Y = {B,D,C,A,B,A}。

  

  最长公共子序列为 4,即{B,C,B,A}。

 #include<iostream>
using namespace std; #define NUM 100
int c[NUM][NUM]; //最长公共子序列中的字母个数
int b[NUM][NUM]; //存放方向编号 void LCSLength(int m, int n, char x[],char y[])
{ //数组c的第0行、第0列置0
for (int i = ; i <= m; i++) c[i][] = ;
for (int i = ; i <= n; i++) c[][i] = ; //根据递推公式构造数组c
for(int i = ; i <= m; i++)
for(int j = ; j <= n; j++)
{
if(x[i] == y[j])
{c[i][j] = c[i-][j-] + ; b[i][j] = ;} // ↖
else if(c[i-][j] >= c[i][j-])
{c[i][j] = c[i-][j]; b[i][j] = ;} // ↑
else
{c[i][j] = c[i][j-]; b[i][j] = ;} // ←
}
} void LCS(int i, int j, char x[])
{
if(i == || j == ) return;
if(b[i][j] == ) {LCS(i-, j-, x); cout << x[i];}
else if(b[i][j] == ) LCS(i-, j, x);
else LCS(i, j-, x);
} int main()
{
char x[NUM];
char y[NUM];
int m, n;
cin >> m;
for(int i = ; i <= m; i++)
cin >> x[i];
cin >> n;
for(int i = ; i <= n; i++)
cin >> y[i];
LCSLength(m, n, x, y);
cout << c[m][n] << endl;
LCS(m, n, x);
return ;
}

  4、最大子段和

  给定由 n 个整数(包含负整数)组成的序列 a1,a2,...,an,求该序列子段和的最大值。

  当所有整数均为负值时定义其最大子段和为 0。所求的最优值为:

图4.1 最大子段和公式

  例如:

  当(a1,a2, ……a7,a8)= (1,-3,7,8,-4,12, -10,6)时,最大子段和为:

图4.2 最大子段和实例

  (1)最大子段和分治算法

  所给的序列 a[1:n] 分为长度相等的两段 a[1:n/2] 和 a[n/2+1:n] ,分别求出这两段的最大子段和,则 a[1:n] 的最大子段和有三种情形:

    Ⅰ. a[1:n] 的最大子段和与 a[1:n/2] 的最大子段和相同;

    Ⅱ. a[1:n] 的最大子段和与 a[n/2+1:n] 的最大子段和相同;

    Ⅲ. a[1:n] 的最大子段和为 ai+…+aj,且 1 ≤ i ≤ n/2,n/2+1 ≤ j ≤ n。

 #include<iostream>
using namespace std; #define NUM 50 int MaxSubSum(int *a, int left, int right)
{
int sum = ;
if(left == right) sum = a[left] > ? a[left] : ;
else
{
int center = (left + right) / ;
int leftsum = MaxSubSum(a, left, center);
int rightsum = MaxSubSum(a, center+, right); int s1 = ;
int lefts = ;
for(int i = center; i >= left; i--)
{
lefts += a[i];
if(lefts > s1)
s1 = lefts;
} int s2 = ;
int rights = ;
for(int i = center+; i <= right; i++)
{
rights += a[i];
if(rights > s2)
s2 = rights;
} sum = s1 + s2;
if(sum < leftsum) sum = leftsum;
if(sum < rightsum) sum = rightsum;
}
return sum;
} int MaxSum(int n, int *a)
{
return MaxSubSum(a, , n);
} int main()
{
int a[NUM] = {};
int n;
cin >> n;
for(int i = ; i <= n; i++)
cin >> a[i];
cout<< MaxSum(n, a) <<endl;
return ;
}

  (2)最大子段和动态规划算法

  由bj的定义易知,当 bj-1 > 0 时 b= bj-1 + aj,否则 b= aj。则计算 b的动态规划递归式:b= max{bj-1+aj, aj},1 ≤ j ≤ n。

 #include<iostream>
using namespace std; #define NUM 50 int MaxSum(int n, int *a)
{
int sum = , b = ;
for(int i = ; i <= n; i++)
{
if(b > ) b += a[i];
else b = a[i];
if(b > sum) sum = b;
}
return sum;
} int main()
{
int a[NUM] = {};
int n;
cin >> n;
for(int i = ; i <= n; i++)
cin >> a[i];
cout<< MaxSum(n, a) <<endl;
return ;
}

  5、最长单调递增子序列

  例如:

  a[] = {1,6,2,4,3}。

  则最长单调递增子序列为 3,即 {1,2,3}。

 #include<iostream>
using namespace std; #define NUM 50 int LIS(int n, int *a)
{
int b[NUM] = {}; //每一步最长单调递增子序列
b[] = ;
int max = ;
for(int i = ; i < n; i++)
{
int k = ;
for(int j = ; j < i; j++) {
if(a[j] <= a[i] && k < b[j]) {
k = b[j];
}
b[i] = k+;
} if(max < b[i]) max = b[i];
}
return max;
} int main()
{
int a[NUM];
int n;
cin >> n;
for(int i = ; i < n; i++)
cin >> a[i];
int ans = LIS(n, a);
cout << ans <<endl;
return ;
}

  6、0-1背包

  给定一个物品集合 s ={1,2,3,…,n},物品i的重量是 wi,其价值是 vi,背包的容量为 W,即最大载重量不超过 W。在限定的总重量 W 内,我们如何选择物品,才能使得物品的总价值最大。

  注意:(1)如果物品不能被分割,即物品i要么整个地选取,要么不选取;

     (2)不能将物品 i  装入背包多次,也不能只装入部分物品 i,则该问题称为 0—1 背包问题;

     (3)如果物品可以拆分,则问题称为背包问题,适合使用贪心算法。

  例如:

 #include <iostream>
using namespace std; #define NUM 50 void Knapsack(int v[], int w[], int c, int n, int m[][])
{
int jMax = min(w[n]-,c); //背包剩余容量上限,范围[0..w[n]-1]
for(int j = ; j <= jMax; j++) m[n][j] = ;
for(int j = w[n]; j <= c; j++) m[n][j] = v[n]; //限制范围[w[n]~c] for(int i = n-; i > ; i--)
{
jMax = min(w[i]-, c);
for(int j = ; j <= jMax; j++)//背包不同剩余容量 j <= jMax < c
m[i][j] = m[i+][j];//没产生任何效益 for(int j = w[i]; j <= c; j++) //背包不同剩余容量 j-wi > c
m[i][j] = max(m[i+][j], m[i+][j-w[i]]+v[i]); //价值增加vi
}
m[][c] = m[][c];
if(c >= w[]) m[][c] = max(m[][c], m[][c-w[]]+v[]);
} void Traceback(int m[][], int w[], int c, int n, int x[])
{
for(int i=; i<n; i++)
if(m[i][c] == m[i+][c]) x[i]=;
else {x[i]=; c-=w[i];}
x[n] = (m[n][c]) ? : ;
} int main()
{
int c = ;
int w[] = {,,,,};
int v[] = {,,,,};
int x[NUM]; //存储被选中的物品编号,选中x[i] = 1,否则x[i] = 0
int m[][];
int n = (sizeof(v) / sizeof(v[])) - ; Knapsack(v, w, c, n, m); cout << "背包能装的最大价值为:" << m[][c] <<endl; Traceback(m, w, c, n, x); cout<<"背包装下的物品编号为:";
for(int i = ; i <= n; i++)
{
if(x[i] == ) cout<<i<<" ";
}
cout<<endl;
return ;
}

  

[C++] 动态规划之矩阵连乘、最长公共子序列、最大子段和、最长单调递增子序列、0-1背包的更多相关文章

  1. 动态规划-最长单调递增子序列(dp)

    最长单调递增子序列 解题思想:动态规划 1.解法1(n2) 状态:d[i] = 长度为i+1的递增子序列的长度 状态转移方程:dp[i] = max(dp[j]+1, dp[i]); 分析:最开始把d ...

  2. nyist oj 214 单调递增子序列(二) (动态规划经典)

    单调递增子序列(二) 时间限制:1000 ms  |  内存限制:65535 KB 难度:4 描写叙述 ,a2...,an}(0<n<=100000).找出单调递增最长子序列,并求出其长度 ...

  3. ny214 单调递增子序列(二) 动态规划

    单调递增子序列(二) 时间限制:1000 ms  |  内存限制:65535 KB 难度:4 描述 给定一整型数列{a1,a2...,an}(0<n<=100000),找出单调递增最长子序 ...

  4. HD1160FatMouse's Speed(最长单调递增子序列)

    FatMouse's Speed Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) ...

  5. [dp]最长单调递增子序列LIS

    https://www.51nod.com/tutorial/course.html#!courseId=12 解题关键: 如果将子序列按照长度由短到长排列,将他们的最大元素放在一起,形成新序列$B\ ...

  6. poj1631Bridging signals(最长单调递增子序列 nlgn)

    Bridging signals Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 12251   Accepted: 6687 ...

  7. NYOJ17 最长单调递增子序列 线性dp

    题目链接: http://acm.nyist.edu.cn/JudgeOnline/problem.php?pid=17 分析: i=1 dp[i]=1 i!=1 dp[i]=max(dp[j]+1) ...

  8. 动态规划 最长公共子序列 LCS,最长单独递增子序列,最长公共子串

    LCS:给出两个序列S1和S2,求出的这两个序列的最大公共部分S3就是就是S1和S2的最长公共子序列了.公共部分 必须是以相同的顺序出现,但是不必要是连续的. 选出最长公共子序列.对于长度为n的序列, ...

  9. 算法之动态规划(最长递增子序列——LIS)

    最长递增子序列是动态规划中最经典的问题之一,我们从讨论这个问题开始,循序渐进的了解动态规划的相关知识要点. 在一个已知的序列 {a1, a 2,...an}中,取出若干数组成新的序列{ai1, ai ...

随机推荐

  1. 理解$watch、$apply与$digest

    Angular环境 浏览器里面有一个事件队列(event queue),用户触发啥事儿,或者网络请求,延时操作(例如定时器之类),都是一个event,浏览器会轮询这些事件,然后调用这些回调(这里的回调 ...

  2. memcpy与memmove

    函数原型: void* memcpy(void *dst,void const *src,size_t count) void* memmove(void *dst,void const *src,s ...

  3. SSM mapper.xml

    MyBatis 真正的力量是在映射语句中.这里是奇迹发生的地方.对于所有的力量,SQL 映射的 XML 文件是相当的简单.当然如果你将它们和对等功能的 JDBC 代码来比较,你会发现映射文件节省了大约 ...

  4. 【P3572】little bird(单调队列+DP)

    一眼看上去这个题就要DP,可是应该怎么DP呢,我们发现,数据范围最多支持O(NlogN),但是这种DP貌似不怎么有,所以应该是O(N)算法,自然想到单调队列优化DP. 然后我们先考虑如果不用单调队列应 ...

  5. centos下安装python2.7.9和pip以及数据科学常用的包

    以前一直用ubantu下的python,ubantu比较卡.自己倾向于使用centos,但默认的python版本太低,所以重新装了一个python和ipython centos6.5安装python2 ...

  6. Java -- 数据库 多表操作,1对多,多对多,1对1。 基于dbutils框架

    1. 1对多,部门--员工 为例, 多的一方建外键. domain,建立bean对象 public class Department { private String id; private Stri ...

  7. eclipse build path 以及 clean(转)

    1.设置"source folder"与"output folder". source folder:存放.Java源文件的根目录:output folder: ...

  8. Java JDK、Tomcat、Eclipse环境配置

    Java 下载地址:http://www.oracle.com/ 根据提示一步一步进行安装,通常安装到C:\Program Files\Java,包含: 环境变量配置: JAVA_HOME:C:\Pr ...

  9. Jedis源代码探索

    [连接池实现] [一致性hash实现]   [Redis客户端-Jedis源代码探索][http://blog.sina.com.cn/s/blog_6bc4401501018bgh.html]   ...

  10. zoj3229 有源汇上下界最大流

    题意:有一个人每天给妹子拍照,每个妹子有最少拍照数,每天有最大拍照数,每天只能给某些特定的妹子拍照,求最大拍照数 题解:很容易看出来的有源汇上下界最大流,对于有源汇 的上下界最大流,我们按照无源汇的操 ...