基本思想
与分治法类似,将问题分解为子问题,但子问题往往不是相互独立的。所解决的问题有一个显著的特征,即它所对应的子问题树中的子问题呈现大量的重复。因此动态规划的相应特征是,对于重复出现的子问题,只在第一次遇到时加以求解,并把答案保存起来,让以后再遇到时直接引用,不必重新求解。
常用于求某一问题在某种意义下的最优解。适合采用动态规划的优化问题必须具备最优子结构性质和子问题重叠性质。
当一个问题的优化解包含了子问题的优化解时,则称该问题具有优化子结构性质。
在求解一个问题的过程中,很多子问题的解被多次调用,则称该问题具有子问题的重叠性质。
要点:
多阶段决策过程。每步决策将依赖前步骤的决策结果
优化函数之间存在依赖关系
优化原则,子问题优化能帮助父问题优化
设计算法步骤
- 分析最优解的性质,并刻画其结构特征
- 递归地定义最优值(每个解都有一个值,代价)
- 根据递归方程分解子问题,直到不能分为止
- 自底向上的方式计算最优值,并记录构造最优解所需信息
- 根据计算最优值得到的信息,构造一个最优解
简单来讲:
- 设计状态
- 状态转移方程
最短路径问题
从终点开始,获取短距离的局部最优,为较远距离的局部最优做参数依赖,当局部最优的距离到达起点时,就能取得全局最优
最长公共子序列(LCS)问题
1. 设计状态
f[i][j]表示的是序列Si={xi,x2,...,xi}与序列Sj={xi,x2,...,xj}的最长公共子序列。f[i][j]表示的是序列S_i=\{x_i,x_2,...,x_i\}与序列S_j=\{x_i,x_2,...,x_j\}的最长公共子序列。f[i][j]表示的是序列Si={xi,x2,...,xi}与序列Sj={xi,x2,...,xj}的最长公共子序列。
2.状态转移方程
f[i][j]=0,i=0或者j=0(处理边界)f[i][j]=0,\qquad\qquad\qquad\qquad\qquad\qquad\qquad i=0 \quad或者 \quad j=0 \qquad (处理边界)f[i][j]=0,i=0或者j=0(处理边界)
f[i][j]=1+f[i−1][j−1],xi=yi(第i个位置和第j个位置字符相等,长度+1)f[i][j]=1+f[i-1][j-1],\qquad\qquad\qquad\quad x_i=y_i \qquad(第i个位置和第j个位置字符相等,长度+1)f[i][j]=1+f[i−1][j−1],xi=yi(第i个位置和第j个位置字符相等,长度+1)
f[i][j]=max{f[i−1][j], f[i][j−1]}, xi≠yif[i][j]=max\{f[i-1][j],\ f[i][j-1]\},\qquad\ \ x_i \ne y_if[i][j]=max{f[i−1][j], f[i][j−1]}, xi=yi
3.代码
#include<cstdio>
#include<iostream>
using namespace std;
const int MAXN = 1000 + 10;
char s1[MAXN], s2[MAXN];
int f[MAXN][MAXN];
int main() {
int n, m;
cin.getline(s1, MAXN);
cin.getline(s2, MAXN);
n = strlen(s1);
m = strlen(s2);//获取俩字符串长度
//状态转移方程
for (int i = 1; i <= n; i++) { //数组初始化全为0,故边界值默认已处理
for (int j = 1; j <= m; j++) {
if (s1[i - 1] == s2[j - 1]) {//i,j位置字符相等,字符串索引从0开始,i,j从1开始
f[i][j] = 1 + f[i - 1][j - 1];
}
else {
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
}
cout << f[n][m] << endl;
}
最长公共子串问题
1. 设计状态
f[i][j]表示的是字符串Si={xi,x2,...,xi}与字符串Sj={xi,x2,...,xj}的最长公共子串长度。f[i][j]表示的是字符串S_i=\{x_i,x_2,...,x_i\}与字符串S_j=\{x_i,x_2,...,x_j\}的最长公共子串长度。f[i][j]表示的是字符串Si={xi,x2,...,xi}与字符串Sj={xi,x2,...,xj}的最长公共子串长度。
2.状态转移方程
f[i][j]=0,i=0或者j=0(处理边界)f[i][j]=0,\qquad\qquad\qquad\qquad\qquad\qquad\qquad i=0 \quad或者 \quad j=0 \qquad (处理边界)f[i][j]=0,i=0或者j=0(处理边界)
f[i][j]=1+f[i−1][j−1],xi=yi(第i个位置和第j个位置字符相等,长度+1)f[i][j]=1+f[i-1][j-1],\qquad\qquad\qquad\quad x_i=y_i \qquad(第i个位置和第j个位置字符相等,长度+1)f[i][j]=1+f[i−1][j−1],xi=yi(第i个位置和第j个位置字符相等,长度+1)
f[i][j]=0,xi≠yif[i][j]=0,\qquad\qquad\qquad\qquad\qquad\qquad\qquad x_i \ne y_if[i][j]=0,xi=yi
3.代码
#include<cstdio>
#include<iostream>
using namespace std;
const int MAXN = 1000 + 10;
char s1[MAXN],s2[MAXN];
int f[MAXN][MAXN];
int getStatusMartix(int &n,int &m ,int &maxLength,int &maxIndex) {
int flag = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s1[i - 1] == s2[j - 1]) {
flag = 1;
f[i][j] = 1 + f[i - 1][j - 1];
if (maxLength < f[i][j]) {
maxLength = f[i][j]; //记录最大长度,以及索引,用于追踪最大子串
maxIndex = i - 1;
}
}
}
}
return flag;
}
void getLongestSubstring(int maxLength, int maxIndex) {
int minIndex = maxIndex - maxLength + 1;
for (int i = minIndex; i <= maxIndex; i++) {
cout << s1[i];
}
cout << endl;
}
int main() {
int n, m;
int maxLength,maxIndex;
int flag;
cin.getline(s1, MAXN);
cin.getline(s2, MAXN);
n = strlen(s1);
m = strlen(s2);
//获取状态矩阵
flag = getStatusMartix(n, m, maxLength, maxIndex);
//先判断有无公共子串,若由则打印子串,无则打印'NULL'
if (flag == 0) {
cout << "NULL" << endl;
}
else {
getLongestSubstring(maxLength, maxIndex);
}
return 0;
}
矩阵连乘最佳计算次序问题
n个矩阵相乘,试确定矩阵的乘法顺序,使得元素相乘的总次数最少
矩阵A:i行j列,B:j行k列
结果矩阵的一个元素cts(0<t<i ,0<s<k) 要做 j 次乘法,j - 1次加法,结果矩阵共有i * k
个
P向量表示方情况
P = <10,100,5,50>
A1:10 * 100, A2:100 * 5, A3:5 * 50
一、优化递推方程
m[i,j]:得到Ai...j的最少的相乘次数m[i,j]: 得到A_{i...j}的最少的相乘次数m[i,j]:得到Ai...j的最少的相乘次数
m[i,j]={0i=jmini≤k<j{m[i,k]+m[k+1,j]+Pi−1PkPj}i<j
m[i,j] =
\begin{cases}
0 & i = j \\
\underset{i\leq k<j}\min \{m[i,k] + m[k+1,j] + P_{i-1}P_kP_j\} & i<j
\end{cases}
m[i,j]=⎩⎨⎧0i≤k<jmin{m[i,k]+m[k+1,j]+Pi−1PkPj}i=ji<j
部分伪代码
算法1RecurMatrixChain(P,i,j)算法1\quad RecurMatrixChain(P,i,j)算法1RecurMatrixChain(P,i,j)
1.m[i,j]←∞1.\quad m[i,j]\leftarrow \infty1.m[i,j]←∞
2.s[i,j]←i2.\quad s[i,j]\leftarrow i2.s[i,j]←i
3.for k←i to j−1 do3.\quad for \ k\leftarrow i \ to \ j-1 \ do3.for k←i to j−1 do
4.q←RecurMartrixChain(P,i,k) + RecurMartrixChain(P,k+1,j) + Pi−1PkPj4.\qquad\quad q \gets RecurMartrixChain(P,i,k) \ +\ RecurMartrixChain(P,k+1,j) \ +\ P_{i-1}P_kP_j4.q←RecurMartrixChain(P,i,k) + RecurMartrixChain(P,k+1,j) + Pi−1PkPj
5.if q < m[i,j]5. \qquad\quad if \ q \ < \ m[i,j]5.if q < m[i,j]
6.then m[i,j]←q6. \qquad\quad then \ m[i,j] \leftarrow q6.then m[i,j]←q
7. s[i,j]←k7. \qquad\qquad\quad\ s[i,j]\gets k7. s[i,j]←k
8.return m[i,j]8.\quad return \ m[i,j]8.return m[i,j]
结论
递归实现动态规划效率不高,原因:同一子问题多次重复出现,每次出现都要重新计算一遍
采用空间换时间策略,记录每个子问题首次计算结果,后面再用时就直接取值,每个子问题只计算一次
二、迭代实现
关键
- 每个子问题只计算一次
- 迭代过程
- 从最小的子问题算起
- 考虑计算顺序,以保证后面用到的值前面已经计算好
- 存储结构保存计算结果——备忘录
- 解的追踪
- 设计标记函数标记每步的决策
- 考虑根据标记函数追踪解的算法
代码
算法2MatrixChain(P,n)算法2\quad MatrixChain(P,n)算法2MatrixChain(P,n)
1.令所有的m[i,i]初值为0//长度为1的矩阵列1.\quad 令所有的m[i,i]初值为0 \quad //长度为1的矩阵列1.令所有的m[i,i]初值为0//长度为1的矩阵列
2.for r←2 to n do//r为链长2.\quad for \ r\leftarrow 2 \ to \ n \ do \quad //r为链长2.for r←2 to n do//r为链长
3.for i←1 to n−r+1 do//左边界i3.\qquad for \ i\leftarrow 1 \ to \ n-r+1 \ do\quad //左边界i3.for i←1 to n−r+1 do//左边界i
4.j←i+r−1//右边界j4.\qquad\quad j \leftarrow i+r-1 \quad //右边界j4.j←i+r−1//右边界j
5.m[i,j]←m[i+1,j]+Pi−1PkPj//k=i5. \qquad\quad m[i,j] \leftarrow m[i+1,j]+P_{i-1}P_kP_j \quad //k=i5.m[i,j]←m[i+1,j]+Pi−1PkPj//k=i
6.s[i,j]←i//记录k6. \qquad\quad s[i,j] \leftarrow i\quad //记录k6.s[i,j]←i//记录k
7.for k←i+1 to j−1 do7. \qquad\quad for \ k\leftarrow i+1 \ to \ j-1 \ do7.for k←i+1 to j−1 do
8.t←m[i,k]+m[k+1,j]+Pi−1PkPj8.\qquad\qquad t \leftarrow m[i,k]+m[k+1,j]+P_{i-1}P_kP_j8.t←m[i,k]+m[k+1,j]+Pi−1PkPj
9.if t<m[i,j]9.\qquad\qquad if \ t<m[i,j]9.if t<m[i,j]
10.then m[i,j]←t//更新解10.\qquad\qquad then\ m[i,j]\leftarrow t\quad //更新解10.then m[i,j]←t//更新解
11. s[i,j]←k11.\qquad\qquad\qquad\ s[i,j]\leftarrow k11. s[i,j]←k
无最终序列的算法(无s记录表)
#include <bits/stdc++.h>
using namespace std;
const int MAX = 1005;
int p[MAX];//矩阵行列数数组
int m[MAX][MAX];//矩阵链最小元素乘法次数
int n;
//迭代矩阵链
void matrix()
{
int i,j,r,k;
memset(m,0,sizeof(m));
for(r = 2; r<=n; r++) //r表示链长 链长为1没有乘法
{
for(i = 1; i<=n-r+1; i++)//矩阵链左边界i
{
j = i+r-1; //矩阵链右边界i
//k=i的情况矩阵链只有一个矩阵乘法次数为0
m[i][j] = m[i+1][j]+p[i-1]*p[i]*p[j];
for(k = i+1; k<j; k++) //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;
}
}
}
}
}
int main()
{
cin>>n;
//记录输入的数字,注意个数比n多1
for(int i=0; i<n+1; i++)
cin>>p[i];
matrix();
cout<<m[1][n]<<endl;//输出第一个矩阵到第n个矩阵最少的乘法次数
return 0;
}
最大子段和问题(暂定)
背包问题
一个旅行者随身携带一个背包。可以放入背包的物品有n\textbf nn种,每种物品的重量和价值分别为wi,vi\textbf w_i,\textbf v_iwi,vi。如果背包的容量是c\textbf cc,每种物品可以放多个。怎么样选择放入背包的物品以使得背包的价值最大?不妨设上述wi,vi,c都是正整数。\textbf w_i,\textbf v_i,\textbf c 都是正整数。wi,vi,c都是正整数。
1.建模
解是<x1,x2,...,xn><x_1,x_2,...,x_n><x1,x2,...,xn>,其中xix_ixi是装入背包的第iii种物品个数
目标函数max∑i=1nvixi 约束条件∑i=1nwixi≤b,xi∈N\begin{matrix}
目标函数
\quad \max \sum_{i=1}^{n}v_ix_i \qquad\qquad\ \ \\ \\
约束条件
\quad \sum_{i=1}^nw_ix_i\le b,\quad x_i \in N
\end{matrix}
目标函数max∑i=1nvixi 约束条件∑i=1nwixi≤b,xi∈N
线性规划问题:由线性条件约束的线性函数取最大或最小的问题
整数规划问题:线性规划问题的变量xix_ixi都是非负整数
2.子问题界定和计算顺序
子问题界定:由参数kkk和yyy界定
k:考虑对物品1,2,3...,k的选择k:考虑对物品1,2,3...,k的选择k:考虑对物品1,2,3...,k的选择
y:背包总重量不超过yy:背包总重量不超过yy:背包总重量不超过y
原始输入:k=n,y=ck = n,y = ck=n,y=c
子问题计算顺序:
k=1,2,3...,nk=1,2,3...,nk=1,2,3...,n
对于给定的k; y=1,2,...,c对于给定的k;\ y=1,2,...,c对于给定的k; y=1,2,...,c
3.优化函数的递推方程
Fk(y):装前k种物品,总重不超过y,背包达到的最大价值Fk(y)=max{Fk−1(y),Fk(y−wk)+vk}F0(y)=0,0≤y≤c,Fk(0)=0,0≤k≤nF1(y)=⌊yw1⌋v1,,Fk(y)=−∞y<0 \begin{aligned} F_k(y):装前k种物品,总重不超过y,背包达到的最大价值 \\ F_k(y)=max\{F_{k-1}(y),F_k(y-w_k) + v_k\} \\ F_0(y)=0, \quad 0\le y\le c, \quad F_k(0)=0, \quad 0\le k\le n \\ F_1(y) = \lfloor \frac {y}{w_1} \rfloor v_1, \quad ,F_k(y) = -\infty \quad y < 0 \end{aligned} Fk(y):装前k种物品,总重不超过y,背包达到的最大价值Fk(y)=max{Fk−1(y),Fk(y−wk)+vk}F0(y)=0,0≤y≤c,Fk(0)=0,0≤k≤nF1(y)=⌊w1y⌋v1,,Fk(y)=−∞y<0
4.标记函数(追踪解)
ik(y):装前k种物品,总重不超过y,背包达到最大价值时装入物品的最大标号ik(y)={ik−1(y)Fk−1(y)>Fk(y−wk)+vkkFk−1(y)≤Fk(y−wk)+vkik(y)={0y<w11y≥w1 \begin{matrix} i_k(y): 装前k种物品,总重不超过y,背包达到最大价值时装入物品的最大标号 \\ i_k(y)= \begin{cases} i_{k-1}(y) & F_{k-1}(y)>F_k(y-w_k)+v_k \\ k & F_{k-1}(y)\le F_k(y-w_k)+v_k \end{cases} \\ i_k(y)= \begin{cases} 0 & y < w_1 \\ 1 & y \ge w_1 \end{cases} \qquad\qquad\qquad\qquad\qquad\quad \end{matrix} ik(y):装前k种物品,总重不超过y,背包达到最大价值时装入物品的最大标号ik(y)={ik−1(y)kFk−1(y)>Fk(y−wk)+vkFk−1(y)≤Fk(y−wk)+vkik(y)={01y<w1y≥w1
5.追踪算法
算法Track Solution算法\quad Track\ Solution算法Track Solution
输入:ik(y)表,k=1,2,...,c输入: i_k(y)表,k=1,2,...,c输入:ik(y)表,k=1,2,...,c
输出:x1,x2,...,xn;n种物品的装入量输出: x_1,x_2,...,x_n; \quad n种物品的装入量输出:x1,x2,...,xn;n种物品的装入量
1.forj←1 to ndoxj←01.\quad for \quad j\leftarrow 1\ to \ n \quad do \quad x_j\leftarrow 01.forj←1 to ndoxj←0
2.y←c,j←n2.\qquad y\leftarrow c,j\leftarrow n2.y←c,j←n
3.j←ij(y)3.\qquad j \leftarrow i_j(y)3.j←ij(y)
4.xj←14. \qquad x_j \leftarrow 14.xj←1
5.y←y−wj5.\qquad y\leftarrow y - w_j5.y←y−wj
6.whileij(y)=jdo6. \qquad while \quad i_j(y)=j \quad do6.whileij(y)=jdo
7.y←y−wj7.\qquad\qquad y \leftarrow y - w_j7.y←y−wj
8.xj←xj+18.\qquad\qquad x_j \leftarrow x_j + 18.xj←xj+1
9.ifij(y)≠0thengoto39.\qquad\qquad if \quad i_j(y)\neq 0 \quad then \quad goto \quad 39.ifij(y)=0thengoto3
推广 0-1背包问题
代码(无追踪函数)
#include<iostream>
using namespace std;
//无序物品序列,故不需标记追踪函数
int n, c, * v, * w, ** F;
void FindMax()
{
int i, j;
//根据状态转移方程填写价值表
for (i = 1; i <= n; i++)//0种物品不考虑,为0
{
for (j = 1; j <= c; j++)//0容量不考虑,为0
{
if (j < w[i]) //如果第i个物品质量超出容量
{
F[i][j] = F[i - 1][j]; //最大价值取 i-1物品的同容量
}
else//取装第i个物品的背包价值与取装第i-1个物品的背包价值大的那个
{
if (F[i - 1][j] > F[i - 1][j - w[i]] + v[i])
{
F[i][j] = F[i - 1][j];
}
else
{
F[i][j] = F[i - 1][j - w[i]] + v[i];
}
}
}
}
}
int main()
{
int i;
cin >> n >> c; // 输入物品数量n,背包容量c
v = new int[n + 1];//价值数组
w = new int[n + 1];//重量数组
F = new int* [n + 1];//价值矩阵
for (i = 0; i <= n; i++)
*(F + i) = new int[c + 1];
for (i = 1; i <= n; i++)
cin >> w[i] >> v[i]; //输入每件物品的重量和价值
for (i = 0; i <= c; i++)
F[0][i] = 0;
for (i = 1; i <= n; i++)
F[i][0] = 0;
FindMax();
cout << F[n][c];//最大价值
return 0;
}