1.动态规划基础
1.1动态规划的基本思想
动态规划建立在最优原则的基础上,在每一步决策上列出可能的局部解,按某些条件舍弃不能得到最优解的局部解,通过逐层筛选减少计算量。每一步都经过筛选,以每一步的最优性来保证全局的最优性。具体来说,动态规划算法仍然是将待求解的问题的若干子问题,采用列表技术,将从小到大的子问题的计算答案存储于一张表中,由于将原问题分解后的各个子问题可能存在重复,所以当重复遇到该子问题时,只需要查表继续问题的求解,而不需要重复计算。所以动态规划算法的基本思想是记录子问题并不断填表。
1.2动态规划的基本要素
通常一个可以用动态规划算法求解的问题应该具有3个要素:最优子结构、无后效性和子问题重叠性。
最优子结构:动态规划算法的关键在于正确的找出基本的递推关系式和恰当的边界条件。要做到这一点,必须将原问题分解为几个相互联系的阶段,在每一个子问题的求解中,均利用它前面子问题的最优化结果,依次进行,最有一个子问题所得的最优解就是整个问题的最优解。
无后效性:将各个阶段依次排好之后,一旦某阶段的状态已经确定,它以前各阶段的状态无法直接影响未来的决策,并且当前状态的决策只是对以往决策的总结。
子问题重叠性:动态规划计算最优值时,每次计算所产生的子问题并不总是新问题,有些问题被重复计算多次,但是动态规划将这些子问题的解存放在表格中,不需要重复计算,提高了程序的效率。
1.3动态规划的基本方法
动态规划问题千奇百怪,有诸多变种,但是动态规划具有比较鲜明的特征,即最优子结构和重叠子问题。解决动态规划问题的思路很重要,掌握下面五步之后再加以练习能够解决许多动态规划问题。
- 确定dp的含义:dp数组中存放的是每个子问题的最优解。
- 推导动态转移方程:在动态规划问题中
- dp的初始化
- 遍历顺序
- 打印表格
2.矩阵连乘问题
给定个矩阵,其中矩阵的维数为×,且与是可乘的,考察这个矩阵的连乘积。由于矩阵乘法满足结合律,所以计算矩阵的连乘可以有许多不同的计算次序,这种计算次序可以用加括号的方式来确定。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩****阵连乘积需要的数乘次数最少?
设有四个矩阵A、B、C、D,它们的维数分别是:50×10,10×40,40×30,30×5,其完全加括号方式为:(A((BC)D)),(A(B(CD))),((AB)(CD)),(((AB)C)D),((A(BC))D)所需的乘法次数分别为16000,10500,36000,87500,34500。
对于个矩阵的连乘积,设其不同的计算次序为。每种加括号方式都可以分解为两个矩阵的加括号方式:,其递推式为:
卡特兰数是组合数学中一个常出现在各种计数问题中的数列。其递推式如下:
该递推关系的解为:。
卡特兰数的渐近增长为 ~
2.1分析最优子结构
设个矩阵连乘的最佳计算次序为,则与连乘的计算次序都是最优的。矩阵连乘计算次序问题的最优解包含着子问题的最优解。这种性质称为最优子结构性质,问题的最优子结构性质是该问题可用DP方法求解的显著特征。
2.2递归关系建立
将矩阵连乘积记为,这里。的总计算量为:的计算量加上的计算量,再加上和相乘的计算量。
设计算的最佳计算次序所对应的乘法次数为,则原问题的最优解为
当时,,因此
当时,
2.3代码分析
cpp
#include<stdio.h>
#define N 7
void MatrixChain(int *p,int n,int m[][N],int s[][N])
{
for(int i = 0; i <= n; ++i) m[i][i] = 0;//自乘的消耗为0
for(int r = 2; r <= n; ++r)
{
for(int i = 1; i <= n - r + 1; ++i)
{
int j = i + r - 1;
m[i][j] = m[i+1][j] + p[i-1]*p[i]*p[j];//试探性的赋值。
s[i][j] = i;
for(int k = i + 1; k < j; ++k)
{
int t = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j];
if(t < m[i][j])
{
m[i][j] = t;
s[i][j] = k;
}
}
}
}
}
void Traceback(int i, int j, int s[][N]){
if(i == j) printf("A%d",i);
else
{
printf("(");
Traceback(i,s[i][j],s);
Traceback(s[i][j]+1,j,s);
printf(")");
}
}
int main()
{
int p[N]={30,35,15,5,10,20,24};
int m[N][N],s[N][N];
MatrixChain(p,N-1,m,s);
printf("矩阵的最佳乘积方式为:");
Traceback(1,6,s);
return 0;
}
3.电路布线问题
在一块电路板的上、下两端分别有个接线柱。根据电路设计,要求用导线将上端接线柱与下端接线柱相连,如图所示:
其中是的一个排列,导线称为该电路的第条连线。对于任何,第条连线和第条连线相交的充分且必要条件是。
在制作电路板时,要求将这条连线分布到若干绝缘层上。在同一层上的连线不可相交。电路布线问题要确定将那些连线安排在第一层上,使得该层上要有尽可能多的连线。换句话说,该问题要确定导线集的最大不相交子集。
3.1最优子结构分析
记。的最大不相交子集为。。
(1)当时,。
(2)当时,
若。此时,。所以,从而。
若。此时,
如果,则对任意的有且。在这种情况下是的最大不相交子集,从而。
如果,则对任意的,有。从而。因此,。另一方面有,因此又有,从而有。
3.2递归关系建立
(1)当时,
(2)当时,
电路布线问题的最优值为。
3.3代码分析
cpp
void MNS(int C[],int n,int **size)
{
for(int j = 0; j < C[1];++j) size[1][j] = 0;
for(int j = C[1]; j <= n;++j) size[1][j] = 1;
for(int i = 2; i < n; ++i)
{
for(int j = 1; j < C[i]; ++j)
size[i][j] = size[i-1][j];
for(int j = C[i]; j <= n; ++j)
size[i][j] = max(size[i-1][j],size[i-1][C[i]-1]+1);
}
size[n][n] = max(size[n-1][n],size[n-1][C[n]-1]+1);
}
4.最长公共子序列
若给定子序列,则是X的子序列 是指存在一个严格递增下标序列使得对于所有的有。
给定两个序列和,当另一序列既是的子序列又是的子序列时,称是序列和的公共子序列 。我们的问题是给定两个序列和,找出和的最长公共子序列。
4.1最优子结构分析
设序列和的最长公共子序列为,则
(1)若,则,且是和的最长公共子序列。
(2)若且,则是和的最长公共子序列。
(3)若且,则是和的最长公共子序列。
由此可见,两个序列的最长公共子序列包含了这两个序列的前缀的最长公共子序列。因此,最长公共子序列问题具有最优子结构性质。
4.2递归关系建立
设二维数组记录序列和的最长公共子序列的长度。其中;。当或时,空序列是它们的最长公共子序列,此时其它情况下:
4.3代码分析
cpp
void LCSLength()
{
for(int i = 1; i <= m; ++i) c[i][0] = 0;//存放各个子问题的最优值
for(int j = 1; j <= n; ++j) b[0][j] = 0;//存放各个子问题最优值的来源
for(int i = 1; i < m; ++i)
{
for(int j = 1; i <= n; ++j)
{
if(x[i]==y[j])
{
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-1] = c[i-1][j];
b[i][j] = 3;
}
else
{
c[i][j] = c[i][j-1];
b[i][j] = 2;
}
}
}
}
5.图像压缩问题
图像的变位压缩存储格式将所给的像素点序列,分割个连续段。第个像素段中,有个像素,且该段中每个像素都只用位表示。设,则第个像素段为
设,则。因此需要用3位表示,如果限制,则需要用8位来表示。因此第个像素段所需要的存储空间为位。按此格式存储像素序列,则需要位的空间。
图像压缩问题要求确定像素序列的最优分段,使得依此分段所需要的存储空间最少。每个分段的长度不超过255位。
5.1最优子结构分析
设、是的最优分段。显而易见,、是的最优分段。图像压缩问题满足最优子结构性质。
设,,是像素序列的最优分段所需的存储位数。
其中。
5.2代码分析
cpp
void Compress(int n, int p[], int s[], int l[], int b[]) {
const int Lmax = 256;
const int header = 11;
s[0] = 0;
for (int i = 1; i <= n; i++) {
b[i] = length(p[i]); // 计算像素点 p[i] 需要的存储位数
int bmax = b[i];
s[i] = s[i - 1] + bmax; // 赋初值
l[i] = 1;
for (int k = 2; k <= i && k <= Lmax; k++) {
if (bmax < b[i - k + 1]) {
bmax = b[i - k + 1];
}
if (s[i] > s[i - k] + k * bmax) {
s[i] = s[i - k] + k * bmax;
l[i] = k;
}
}
s[i] += header; // 添加头部信息的开销
}
}
6.凸多边形最优三角剖分
凸多边形:一个简单多边形及其内部构成一个闭凸集时,称该简单多边形为凸多边形,即凸多边形边界上或内部的任意两点所连成的直线段上所有点均在凸多边形的内部或边界上。
为方便描述,用多边形顶点的逆时针序列表示凸多边形,即,表示具有条边的凸多边形。
若与是多边形上的不相邻的两个顶点,则线段称为多边形的一条弦。弦将多边形分割成两个多边形和。
多边形的三角剖分是将多边形分割成互不相交的三角形的弦的集合T。
凸多边形的最优三角剖分:给定凸多边形P,以及定义在由多边形的边和弦组成的三角形上的权函数w,要求确定该凸多边形的三角剖分,使得该三角剖分中诸三角形上权之和为最小。
6.1最优子结构分析
假设存在一个凸多边形的最优剖分,它的一个子凸多边形不是最优剖分。也就是说存在一个代价更小的三角剖分。如果是这样的话,使用替换,在保证其它子三角剖分不变的情况下,会产生一个新的整体三角剖分,它的代价更小,则与是最优三角剖分的假设矛盾。所以,凸多边形的最优三角剖分具有最优子结构性。
6.2递归关系建立
定义,为凸子多边形的最优三角剖分所对应的权函数值,取其最优值。为方便起见,退化的多边形具有权值0。根据此定义,要凸多边形P的最有权值为。
的值可以利用最优子结构性质递归地计算。当时,凸子多边形至少有3个顶点。由最优子结构性质,的值应为,代表该三角形的权值,其中。因此
6.3代码分析
cpp
void MinWeightTriangulation(int *weights, int n) {
int t[N][N] = {0}; // 用于存储子问题的最优解
int s[N][N] = {0}; // 用于存储分割点
// 初始化
for (int i = 1; i < n; i++) {
t[i][i] = 0;
}
// 动态规划计算
for (int r = 2; r < n; r++) {
for (int i = 1; i < n - r + 1; i++) {
int j = i + r - 1;
t[i][j] = t[i + 1][j] + get_weight(i - 1, i, j, weights);
s[i][j] = i;
// 尝试所有分割点
for (int k = i + 1; k < j; k++) {
int u = t[i][k] + t[k + 1][j] + get_weight(i - 1, k, j, weights);
if (u < t[i][j]) {
t[i][j] = u;
s[i][j] = k;
}
}
}
}
}
7.0-1背包问题
给定个物品和1个背包。物品的重量是,其价值为** ,背包的容量为** 。如何选择装入背包的物品,使得装入背包中物品的总价值最大? 通常称物体不可分割的背包问题为0-1背包问题。
问题的形式化描述为,给定,,,,要求找出元0-1向量,满足:
7.1最优子结构性分析
假设是所给0-1背包问题的已给最优解,则是下面相应子问题的一个最优解:
7.2递归关系建立
令表示子问题的最优解。表示该问题的子问题的最优解。
最优解的递归关系式为:
7.3代码分析
cpp
void knapsack(int W, int* p, int *w, int size)
{
int C[size][W];//用于存储子问题的最优解
for(int i = 0; i < size; ++i)
for(int j = 0; j < W; ++j)
C[i][j] = 0;
for(int i = 1; i < size; ++i)
for(int j = 1; j < W; ++j)
{
if(w[i-1] < j)
{
C[i][j]=max(C[i-1][j],C[i-1][j-w[i-1]+p[i-1]);
}
else C[i][j] = C[i-1][j];
}
}
8.最优二叉查找树
给定个关键字组成的有序序列,用这些关键字构造一棵二叉查找树 ,该树具有性质:存储于每个节点的元素大于左子树中任一个节点中的元素,小于其右子树中任意节点的元素。
通常用平均比较次数来作为衡量不同二叉查找树查找效率的标准。设在表示为的二叉查找树中,元素的结点深度为,查找概率为;虚节点为,的结点深度为,查找概率为。那么平均比较次数通常被定义为:
最优二叉查找树是在所有表示有序序列的二叉查找树中,具有最小平均比较次数的二叉树。
8.1最优子结构分析
将由实结点和虚结点构成的二叉查找树记为。设定元素作为该树的根结点,。则二叉查找树的左子树由实结点和虚结点组成,记为,而右子树由实结点和虚结点组成,记为 。
如果是最优二叉查找树,假设它的左子树不是一个最优二叉查找树,也就是说存在另一个二叉查找树有更小的查找次数,那么在右子树不变的情况下,拥有该左子树的二叉查找树的效率比原树更高,那么原树就不是最优二叉查找树。则左子树和右子树也是最优二叉查找树。
8.2递归关系建立
设的一棵由实结点和虚节点构成的最优二叉查找子树为,则表示 的平均比较次数。选定结点作为的根结点,则左子树为,右子树,相应的比较次数分别为和。用表示查找实结点的概率,用表示需节点的查找概率。
其中
令
得到:
其中
8.3代码分析
cpp
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
void build_optimal_bst(vector<int> s, vector<double> p, vector<double> q) {
int n = s.size();
// 初始化 C 和 R 数组
vector<vector<double>> C(n + 1, vector<double>(n + 1, 0));
vector<vector<int>> R(n + 1, vector<int>(n + 1, 0));
// 计算 W 数组
vector<vector<double>> W(n + 1, vector<double>(n + 1, 0));
for (int i = 1; i <= n; ++i) {
W[i][i - 1] = q[i - 1];
}
// 动态规划填充 C 和 R 数组
for (int l = 1; l <= n; ++l) { // 子树长度从1到n
for (int i = 0; i <= n - l; ++i) {
int j = i + l;
C[i][j] = numeric_limits<double>::max();
for (int r = i; r < j; ++r) {
double t = W[i][j] + C[i][r] + C[r + 1][j];
if (t < C[i][j]) {
C[i][j] = t;
R[i][j] = r;
}
}
}
}
// 更新 W 数组
for (int l = 1; l <= n; ++l) {
for (int i = 0; i <= n - l; ++i) {
int j = i + l;
W[i][j] = W[i][j - 1] + p[j] + q[j + 1];
}
}
cout << "Cost matrix C:" << endl;
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= n; ++j) {
cout << C[i][j] << " ";
}
cout << endl;
}
cout << "\nRoot position matrix R:" << endl;
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= n; ++j) {
cout << R[i][j] << " ";
}
cout << endl;
}
}
int main() {
vector<int> s = {1, 3, 5, 7}; // 有序序列 S
vector<double> p = {0.15, 0.1, 0.25, 0.1}; // 查找概率 p
vector<double> q = {0.05, 0.15, 0.1, 0.15, 0.05}; // 边界及间隙概率 q
// 构建 OBST
build_optimal_bst(s, p, q);
return 0;
}