3.1 动态规划算法的概念
动态规划的问题要满足优化原则:一个最优决策序列的子问题本身一定是相对于子序列的初始和结束状态的最优决策序列。
优化函数的特点: 任何最短路径的子路径相对于子问题始终点最短。
动态规划设计要素:
1、问题建模,优化的目标函数是什么?约束条件是什么?
2、如何划分子问题(边界)?
3、问题的优化函数值与子问题的优化函数值存在着什么依赖关系?(递推方程)
4、是否满足优化原则?
5、最小子问题怎样界定?其优化函数值,即初值等于什么?
3.2 矩阵连乘问题
加括号的例子如下:
可以看出直接用蛮力算法的复杂度是十分高的,那么我们考虑用递归的思想来解决问题
可以看出再求长度为3时需要解决的1-3最小计算次数的子问题:1-2,2-3的计算次数已经在上一步长度为2的时候被计算过了。这说明在不同的长度上会产生相同的子问题,导致重复计算。
下面是相同的子问题被计算次数的统计:
采用空间换时间策略,记录每个子问题首次计算结果,后面再用时就直接取值,每个子问题只算一次。
迭代实现:
首先填写m[ i ][ j ],s[ i ][ j ],例子如下:
对于m[ i ][ j ],现从对角线开始填写赋初始值:链长为1,
P
0
P
1
(
m
[
1
]
[
1
]
)
,
P
1
P
2
(
m
[
2
]
[
2
]
)
,
.
.
.
,
\color{blue}P_0P_1(m[1][1]),P_1P_2(m[2][2]),...,
P0P1(m[1][1]),P1P2(m[2][2]),...,的乘法次数为0;
再从链长为2的开始填写(如蓝色斜线) P 0 − P 2 ( m [ 1 ] [ 2 ] ) , P 1 − P 3 ( m [ 2 ] [ 3 ] ) , . . . , \color{blue}P_0-P_2(m[1][2]),P_1-P_3(m[2][3]),..., P0−P2(m[1][2]),P1−P3(m[2][3]),...,不断地根据起点和链长获得终点求出对应的m(i,j):
m [ i ] [ j ] = m i n { m [ i ] [ k ] + m [ k + 1 ] [ j ] ) + P i − 1 P k P j } 其 中 i ≤ k < j 初 始 化 m [ i ] [ j ] 时 k = i , m [ i ] [ i ] = 0 \color{red}m[i][j]=min\{m[i][k]+m[k+1][j])+P_{i-1}P_{k}P_j\}\color{blue}其中i≤k<j初始化m[i][j]时k=i,m[i][i]=0 m[i][j]=min{m[i][k]+m[k+1][j])+Pi−1PkPj}其中i≤k<j初始化m[i][j]时k=i,m[i][i]=0。
如 m [ 1 ] [ 3 ] = m i n { m [ 1 ] [ k ] ) + m [ k + 1 ] [ 3 ] ) + P 0 P k P 3 } m[1][3]=min\{m[1][k])+m[k+1][3])+P_0P_kP_3\} m[1][3]=min{m[1][k])+m[k+1][3])+P0PkP3},先从k=1开始也就是 m [ 1 ] [ 3 ] = m [ 1 ] [ 1 ] + m [ 2 ] [ 3 ] + P 0 P 1 P 3 = m [ 2 ] [ 3 ] + P 0 P 1 P 3 m[1][3]=m[1][1]+m[2][3]+P_0P_1P_3=m[2][3]+P_0P_1P_3 m[1][3]=m[1][1]+m[2][3]+P0P1P3=m[2][3]+P0P1P3,再对k从2到j-1进行遍历找到最小的m[ i ][ j ],同时用s(i,j)记录m(i,j)的划分位置。
这样得到了下表:
代码
#include <iostream>
#include <cstring>
#include <bits/stdc++.h>
using namespace std;
void MatrixChain(int *p,int n,int m[100][100],int s[100][100])
{
int i,r,j;
//对角线赋成0
for (int i = 1; i <= n; i++) m[i][i] = 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 Print(int a[100][100],int i,int j)
{
if(i==j)
{
cout<<"A"<<i;
}
else
{
cout<<"(";
Print(a,i,a[i][j]);
Print(a,a[i][j]+1,j);
cout<<")";
}
}
int main()
{
int p[100];
int m[100][100],s[100][100];
int n;
cin>>n;
for(int i=0;i<n;i++)
{
cin>>p[i];
}
MatrixChain(p,n,m,s);
Print(s,1,n-1);
return 0;
}
/*
6
30 35 15 5 10 20
*/
3.3最大子段和问题
详情方法参考文章
最大字段和的起点必须是正数,当找到第一个正数时,不断地将a[i]加到tmpsum上,比较此时与maxsum的值,如果比maxsum大则maxsum=tmpsum;如果此时tmpsum<0,说明越加越小,使tmpsum=0,从新加起。
#include <iostream>
#include <cstring>
#include <bits/stdc++.h>
using namespace std;
int maxSum(int a[],int n,int *s,int *e)
{
int tmpsum=0,maxsum=0,f=1;
for(int i=0; i<n; i++)
{
tmpsum+=a[i];
if(tmpsum>maxsum)
{
if(f==1)
{
*s=i;
f=0;
}
maxsum=tmpsum;
*e=i;
}
else if(tmpsum<0)//重新确定起点
{
tmpsum=0;
f=1;
}
}
return maxsum;
}
int main()
{
int s,e,a[10];
int n;
cin>>n;
for(int i=0; i<n; i++)
{
cin>>a[i];
}
cout<<maxSum(a,n,&s,&e)<<endl;
for(int i=s;i<=e;i++)
{
if(a[i]>0)
(i==s)?printf("%d",a[i]):printf("+%d",a[i]);
else
cout<<a[i];
}
return 0;
}
/*
8
4 -3 5 -2 -1 2 6 -2
6
-2 11 -4 13 -5 -2
*/
3.4 最长公共子序列问题
详细请参考文章
首先按照下表填第一行第一列为0(当两字符串为0的时候,最长子序列为0),当匹配上的时候m[i][j]=m[i-1][j-1]+1,记录上一步的最长位置为左上,当没有匹配上的时候m[i][j]左边、上边的最大值,同时记录新的m[i][j]时从哪里来的“左”或“右”。
填表完成后,从两字符串的最后一个开始向前回溯找最长的公共子序列, 如果当前位置的s[i][j]==1表示上一步的最长是左上来的(即上一步匹配上了),s[i][j]==2表示上一步的最长是左边来的(即上一步没匹配上),s[i][j]==3表示上一步的最长是上边来的(即上一步没匹配上)。这样一直找直到i=0或j=0,递归结束,从头开始输出。
需要注意的是x、y字符串下标从
0
到
n
−
1
、
0
到
m
−
1
0到n-1、0到m-1
0到n−1、0到m−1,m[i][j]、s[i][j]都是从1开始才记录真实字符串的匹配情况,所以s[i][j]=1记录的是x[i-1]与y[j-1]匹配上了。
#include<iostream>
#include<cstring>
using namespace std;
int n, m;//n为x的长度,m为y的长度
int c[10][10],s[10][10];//c存储当前最大子序列数
void lcsLength(string x,string y)
{
int i,j;
for(i=0;i<m;i++)//填一列
c[i][0]=0;
for(i=0;i<n;i++)//填一行
c[0][i]=0;
for(i=1;i<m;i++)//从头开始匹配
{
for(j=1;j<n;j++)
{
if(x[i-1]==y[j-1])//因为x、y串从0开始,所以-1
{
c[i][j]=c[i-1][j-1]+1;//当前相等,长度+1
s[i][j]=1;//记录上一步的位置为左上
}
else//不匹配,取之前的最大长度
{
if(c[i-1][j]>=c[i][j-1])//上>左
{
c[i][j]=c[i-1][j];
s[i][j]=2;//记录上一步的位置为左
}
else
{
c[i][j]=c[i][j-1];
s[i][j]=3;//记录上一步的位置为上
}
}
}
}
}
void lcs(int i,int j,string x)
{
if(i==0 || j==0) //回到第一个时结束
return;
if(s[i][j]==1)//匹配则递归找下一个再输出
{
lcs(i-1,j-1,x);//左上走
cout<<x[i-1];
}
else if(s[i][j]==2)//上走
lcs(i-1,j,x);
else lcs(i,j-1,x);//左走
}
int main()
{
string x;//n
string y;//m
int i,j;
cin>>m>>n;
m+=1;n+=1;//多一行一列存0
cin>>x;
cin>>y;
lcsLength(x,y);
lcs(m-1,n-1,x);//从最后一个回溯查找
}
/*
4 5
abad
baade
4 4
abcb
bdca
*/
3.4 数字三角形(瑞格习题)
问题描述:
给定一个由n行数字组成的数字三角形,如下图所示,试设计一个算法,计算出从三角形的顶至底的一条路径,使该路径经过的数字总和最大。
算法设计:
对于给定的由n行数字组成的数字三角形,计算从三角形的顶至底的路径经过的数字和的最大值。
数据输入:
第1行是数字三角形的行数n,接下来n行是数字三角形各行中的数字。
结果输出:
数字和的最大值及最大值由哪些数字的和组成。
和“最长公共子序列”类似,第一行的最长路径=a[i][j]+下一行的最长路径,那么当问题划分为只有一个数时(即最底层)路径确定,所以需要从下往上填表,找到最大路径时根据方向从上往下找。
我们用 a [ i ] [ j ] a[i][j] a[i][j]存数据, d [ i ] [ j ] d[i][j] d[i][j]存当前位置的最大路径和, s [ i ] [ j ] s[i][j] s[i][j]记录能让下一步最大的路径方向。
当前的最大路径和为正下/右下的最大路径和+当前数字,同时记录当前到上一步最大路径的方向。当找到d[1][1]时按照从上往下的顺序,找到对应的最大路径方向,输出当前构成最大路径的数,然后递归继续下一步找即可。
#include<iostream>
#include<cstring>
using namespace std;
int d[100][100],s[100][100];//d存储a[i][j]到最后一层的最大路径
//s[i][j]记录上一步构成最大d的方向
int n;//三角形的行数
void solve(int a[][100],int n)
{
for(int j=1; j<=n; j++)
d[n][j]=a[n][j];//最后一行的路径和=当前位置数
for(int i=n-1; i>=1; i--)
for(int j=1; j<=i; j++)
{
//当前最大=上一步最大+当前位置数
//上一步只能从正下或者右下过来
if(d[i+1][j]>d[i+1][j+1])//记录当前到上一步最大的方向为下
s[i][j]=1;
else s[i][j]=2;//右下
d[i][j]=a[i][j]+max(d[i+1][j],d[i+1][j+1]);
}
}
void trace(int a[][100],int i,int j)
{
if(i==n)
return;
if(s[i][j]==1)
{
cout<<a[i+1][j]<<' ';
trace(a,i+1,j);
}
else
{
cout<<a[i+1][j+1]<<' ';
trace(a,i+1,j+1);
}
}
int main()
{
int a[100][100];
cin>>n;
for(int i=1; i<=n; i++)
for(int j=1; j<=i; j++)
cin>>a[i][j];
solve(a,n);
cout<<d[1][1]<<endl;//最大路径数
cout<<a[1][1]<<' ';//输出第一个在继续找下一个
trace(a,1,1);
}
/*
输入:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出:
30
7 3 8 7 5
*/
3.5 凸多边形的最优三角划分
实现和矩阵连乘问题只有一处不同,就是Pi-1PkPj改为权值函数w(i-1, k, j)
#include <iostream>
#include <cstring>
#include <bits/stdc++.h>
using namespace std;
void MatrixChain(int *p,int n,int m[100][100],int s[100][100])
{
int i,r,j;
//对角线赋成0
for (int i = 1; i <= n; i++) m[i][i] = 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] + w(i-1, k, j);
if (t < m[i][j])
{
m[i][j] = t;
s[i][j] = k;
}
}
}
}
void Print(int a[100][100],int i,int j)
{
if(i==j)
{
cout<<"A"<<i;
}
else
{
cout<<"(";
Print(a,i,a[i][j]);
Print(a,a[i][j]+1,j);
cout<<")";
}
}
int main()
{
int p[100];
int m[100][100],s[100][100];
int n;
cin>>n;
for(int i=0;i<n;i++)
{
cin>>p[i];
}
MatrixChain(p,n,m,s);
Print(s,1,n-1);
return 0;
}
/*
6
30 35 15 5 10 20
*/
3.5 0-1背包
具体过程如下:
这里用结构体表示物品,包含weights、value。首先画出m[i][j],从最后一行开始从下到上,从左到右填表。首先对最后一行初始化,再根据公式对上面的行进行填写。
- m [ i ] [ j ] m[i][j] m[i][j]:表示从i到n个物品中的最大价值
- m [ i + 1 ] [ j ] m[i+1][j] m[i+1][j]:表示从i+1到n个物品中的最大价值
- m [ i + 1 ] [ j − w i ] + v i m[i+1][j-w_i]+v_i m[i+1][j−wi]+vi:表示从背包重量-第i个物品重量而产生的最大价值+当前物品的最大价值
- 如果装入第i个物品,那么m[i][j]=重量为 j − w i j-w_i j−wi的背包从i+1到n的最大价值 + i的价值
- 如果不装入第i个物品,那么m[i][j]=重量为 j j j的背包从i+1到n的最大价值
#include <iostream>
#include <cstring>
#include <bits/stdc++.h>
using namespace std;
#define MAX(a,b) a<b?b:a
struct goods{
int weight;//物品重量
int value;//物品价值
};
struct goods a[100];
int n,c;//物品种类、背包容量
int x[100];//最终的所带物品,x[i]=0表示不带
int m[100][100];//m[i][j]:背包容量为j时的最大价值
int KnapSack()
{
for(int j=1;j<=c;j++)//填最后一行
{
if(j<a[n].weight)
m[n][j]=0;
else m[n][j]=a[n].value;
}
for(int i=n-1;i>=1;i--)//从下到上,从左到右填表
{
for(int j=1;j<=c;j++)
{
if(j<a[i].weight)
m[i][j]=m[i+1][j];
else
m[i][j]=MAX(m[i+1][j],m[i+1][j-a[i].weight]+a[i].value);
}
}
}
void TraceBack()
{
int j=c;
for(int i=1;i<=n-1;i++)
{
if(m[i][j]==m[i+1][j])
x[i]=0;
else
{
x[i]=1;//装入第i个物品,背包重量减少
j-=a[i].weight;
}
}
x[n]=m[n][j]?1:0;//处理最后一个物品(最后一行)
//当前背包剩余重量的最大价值为m[n][j],如果m[n][j]>0,则x[n]=1,取到
}
int main()
{
printf("输入物品的种类n=");
cin>>n;
printf("\n输入背包容量c=");
cin>>c;
printf("输入物品的重量weight、价值value=\n");
for(int i=1;i<=n;i++)
{
cin>>a[i].weight>>a[i].value;
}
KnapSack();
TraceBack();
//输出m[i][j]
for(int i=1;i<=n;i++)
{
for(int j=1;j<=c;j++)
printf("%4d",m[i][j]);
cout<<endl;
}
//最后的选取物品
for(int i=1;i<=n;i++)
cout<<x[i]<<' ';
}
/*
输入:
5
10
2 6
2 3
6 5
5 4
4 6
输出:
m[i][j]表
1 1 0 0 1
*/