介绍
参考:
背包问题第二种形式的递推方程–Wuuconix
动态规划算法典型应用之投资问题–Wuuconix
动态规划算法典型应用之背包问题–Wuuconix
动态规划解最长公共子序列(LCS)(附详细填表过程)
对于投资问题
一、问题描述:
一般性描述:设m元钱,n项投资项目,函数f_i(x)表示将x元投入第i项项目所产生的效益, i = 1 , 2 , ⋅ ⋅ ⋅ , n ; i=1,2,···,n; i=1,2,⋅⋅⋅,n;
问:如何分配这 m m m元钱,使得投资总效益最高?
组合优化问题:假设分配给第 i i i个项目的钱数是 x i x_i xi,
目标函数: m a x ( f 1 ( x 1 ) + f 2 ( x 2 ) + ⋅ ⋅ ⋅ + f n ( x n ) ) ; max(f_1(x_1)+ f_2(x_2)+···+ f_n(x_n)); max(f1(x1)+f2(x2)+⋅⋅⋅+fn(xn));
约束条件: x 1 + x 2 + x 3 + ⋅ ⋅ ⋅ + x n = m , x i ∈ n x_1+x_2+x_3+···+x_n=m,x_i∈n x1+x2+x3+⋅⋅⋅+xn=m,xi∈n;有m元钱,n项投资,函数 f i ( x ) f_i(x) fi(x)表示将x元投入第i项
-
递推关系:
-
F k ( x ) = max 0 ⩽ x k ⩽ x ( f k ( x k ) + F k − 1 ( x − x k ) ) \displaystyle F_k(x)=\displaystyle\max_{0⩽x_k⩽x}( {f_k(x_k)+F_{k−1}(x−x_k)}) Fk(x)=0⩽xk⩽xmax(fk(xk)+Fk−1(x−xk))
可以看到这里有一个对 x k x_k xk的遍历,所以要用到三重循环
- 关于二维数组的赋值,必须要int声明和赋值同时进行。
int value[5][6]={
0,0,0,0,0,0,
0,11,12,13,14,15,
0,0,5,10,15,20,
0,2,10,30,32,40,
0,20,21,22,23,24
};
代码
#include <iostream>
#include <math.h>
using namespace std;
int main()
{
int money=5,num=4;
//5代表5个项目,6代表6种投资方式,0-5万元,
//为了表示好看,第一个参数取1-4
int value[5][6]={//效益函数
0,0,0,0,0,0,
0,11,12,13,14,15,
0,0,5,10,15,20,
0,2,10,30,32,40,
0,20,21,22,23,24
};
int i,j,k;
int F[5][6]={0};//F[k][x]表示x万元投入给前k个项目的最大效益
int x[5][6]={0};//用来储存当F[k][x]取得最大值时, 第k个项目被投资了多少万元钱
for(i=0;i<=5;++i)
{
F[1][i]=value[1][i];
x[1][i]=i;
}
for(i=2;i<=4;++i)//记录项目数,从子问题开始
{
for(j=0;j<=5;++j)//记录投资钱数,从0开始
{
int max=0;//记录i项目,j投资下最大收益
for(k=0;k<=j;++k)//遍历分别投资,递推关系表达
{
if((value[i][k]+F[i-1][j-k])>max)
{
max=value[i][k]+F[i-1][j-k];
x[i][j]=k;
}
}
F[i][j]=max;
}
}
cout << "max value=:" << F[4][5] << endl;
for(i=4;i>0;--i)
{
cout<<"the investment for "<<i<<" is:"<<x[i][money]<<endl;
money-=x[i][money];
}
}
对于背包问题
- 此类问题都有一个目标函数 max ∑ j = 1 n v j ∗ x j \displaystyle\max∑_{j=1}^n v_j*x_j maxj=1∑nvj∗xj
- 动态规划算法的前期准备步骤主要是如何确定子问题的规模,用什么参数来确定,以及用几个参数。和投资问题的两个参数(投资前k个,投x万元)相类似,背包问题也要用两个参数,分别是选择前k个物品,背包的空间为y 这两个参数决定。
- 我们用优化函数 F k ( y ) F_k ( y ) Fk(y)来表示只允许装前k种物品,背包总数不超过y时背包的最大价值。我们计算这个规模的问题时,分两种情况考虑,如果不选择第k个物品,只选择前k-1种物品,而背包仍然是y,也就是 F k − 1 ( y ) F_{k − 1}( y ) Fk−1(y);还有一种情况就是选择了第k个物品,那么剩下的背包空间就是y-w[k],而因为每个物品可以选择多个,所以剩下的背包空间仍然可以在前k个物品中进行选择,那么取得的最大值就是 F k ( y − v k ) F_k(y−v_k ) Fk(y−vk)
递推方程就是 F k ( y ) = m a x ( F k − 1 ( y ) , F k ( y − w k ) + v k ) F_k(y)=max({F_{k − 1}( y ) , F_k(y−w_k) + v_k }) Fk(y)=max(Fk−1(y),Fk(y−wk)+vk)
代码
#include <iostream>
using namespace std;
int main()
{
int v[5]={0,1,3,5,9};//5种物品的价值
int w[5]={0,2,3,4,7};//5种物品的重量
int number=4;//4种物品
int weight=10;//10的限重
int F[5][11]={0};//5表示有4个物品,第0号物品作废。11表示最大重量是10
//F[k][x]表示放入前k种物品,重量限制为x的最大的价值
int tag[5][11]={0};//用来表示F[k][x]用到的产品的最大标号
int i,j;
for(i=1;i<=10;++i)
{
F[1][i]=i/w[1]*v[1];//只装第一件物品的最大收益
if(i/w[1]>0)//能装进第一件物品
tag[1][i]=1;
else
tag[1][i]=0;
}
for(i=2;i<=4;++i)//开始2..3..4种物品
{
for(j=1;j<=10;++j)//此时j的取值范围已经实现了限重的约束条件
{
int tmp=0;
if(j-w[i]<0)//计算装入第i件物品重量是否使重量实际上为负值,也即装不下
tmp=-2222223;
else
tmp=F[i][j-w[i]]+v[i];
if(F[i-1][j]>tmp)//递推关系
{
F[i][j]=F[i-1][j];
tag[i][j]=tag[i-1][j];
}
else
{
F[i][j]=F[i][j-w[i]]+v[i];
tag[i][j]=i;
}
}
}
cout<<"the max value:"<<F[4][10]<<endl;
//========traceback====追溯解
int x[5]={0};//初始化各种放入物品的数量都是0
int y=weight;
j=number;
while(true)
{
j=tag[number][y];//初始追踪位置,倒序追踪
x[j]=1;
y-=w[j];
while(tag[j][y]==j)//依旧可以放下第j种物品
{
y-=w[j];
x[j]=x[j]+1;//第j种物品数量++
}
if(tag[j][y]!=0)//可以放下的最大物品标号不为0
{
j=tag[j][y];
x[j]=1;
continue;
}
else break;
}
for(i=1;i<=number;++i)
{
cout<<"the "<<i<<" 's number:"<<x[i]<<endl;
}
return 0;
}
对于背包问题的类投资问题的三重循环解法
- 用
x
k
x_k
xk表示第k种物品的数量。
此时递推方程就变化为:
F k ( y ) = max 0 ⩽ x k ⩽ y w k ( F k − 1 ( y − w k ∗ x k ) + v k ∗ x k ) F_k(y)=\displaystyle\max_{0⩽x_k⩽\frac{y}{w_k}}({ F_{k-1}(y−w_k*x_k) + v_k *x_k}) Fk(y)=0⩽xk⩽wkymax(Fk−1(y−wk∗xk)+vk∗xk) - 经测试,应该使用新的标记函数,
num[5][11]
含义是特定种类物品下,特定限重下的物品数目。
#include <iostream>
using namespace std;
int main()
{
int v[5]={0,1,3,5,9};//5种物品的价值,第0种忽略
int w[5]={0,2,3,4,7};//5种物品的重量,第0种忽略
int number=4;//4种物品
int weight=10;//10的限重
int F[5][11]={0};//5表示有4个物品,第0号物品作废。11表示最大重量是10
//F[k][x]表示放入前k种物品,重量限制为x的最大的价值
//int tag[5][11]={0};//用来表示F[k][x]用到的产品的最大标号
int num[5][11]={0};//第二种标记方法,标记数目
int i,j,k;
for(i=1;i<=10;++i)
{
F[1][i]=i/w[1]*v[1];//只装第一件物品的最大收益
num[1][i]=i/w[1];
// if(i/w[1]>0)//能装进第一件物品
// tag[1][i]=1;
// else
// tag[1][i]=0;
}
for(i=2;i<=4;++i)//开始2..3..4种物品
{
for(j=1;j<=10;++j)//此时j的取值范围已经实现了限重的约束条件
{
for(k=0;k<=(j/w[i]);++k)
{
int tmp=0;
if((j-k*w[i])>=0)
{
tmp=F[i-1][j-k*w[i]]+k*v[i];
}
else
{
tmp=0;
}
if(tmp>F[i][j])
{
num[i][j]=k;
//tag[i][j]=i;
F[i][j]=tmp;
}
}
}
}
cout<<"the max value:"<<F[4][10]<<endl;
for(i=4;i>=1;--i)//倒序输出
{
cout<<"the "<<i<<" 's number:"<<num[i][weight]<<endl;
weight-=num[i][weight]*w[i];
}
更加自动化的一般性代码
学习了二维数组初始化后,编写了新的代码如下
这里一般性对于输入的顺序不做要求,任意大小排序都可以计算出最大值。
/*
* @Description:
* @Author: dive668
* @Date: 2021-05-07 08:54:56
* @LastEditTime: 2021-05-17 09:41:14
*/
#include <iostream>
using namespace std;
int main()
{
int n;
cout << "背包数目:";
cin >> n;
int weight;
cout << "背包限重:";
cin >> weight;
int *v = new int[n + 1];
v[0] = 0;
int *w = new int[n + 1];
w[0] = 0;
int k;
for (k = 1; k <= n; ++k)
{
cout << "输入第" << k << "件背包的价值和体积";
cin >> v[k];
cin >> w[k];
}
// int v[5] = {0, 1, 3, 5, 9}; //5种物品的价值
// int w[5]={0,2,3,4,7};//5种物品的重量
// int number=n;//4种物品
// int weight=10;//10的限重
int **F=new int*[n+1];//5表示有4个物品,第0号物品作废。11表示最大重量是10
for (k = 0; k <= n;++k)
{
F[k] = new int[weight + 1]{};//或者()代替{},每个元素都初始化为0
}
//F[k][x]表示放入前k种物品,重量限制为x的最大的价值
//int tag[5][11] = {0}; //用来表示F[k][x]用到的产品的最大标号
int **tag=new int*[n+1];//5表示有4个物品,第0号物品作废。11表示最大重量是10
for (k = 0; k <= n;++k)
{
tag[k] = new int[weight + 1]{};
}
int i,j;
for(i=1;i<=n;++i)
{
F[1][i]=i/w[1]*v[1];//只装第一件物品的最大收益
if(i/w[1]>0)//能装进第一件物品
tag[1][i]=1;
else
tag[1][i]=0;
}
for(i=2;i<=n;++i)//开始2..3..n种物品
{
for(j=1;j<=weight;++j)//此时j的取值范围已经实现了限重的约束条件
{
int tmp=0;
if(j-w[i]<0)//计算装入第i件物品重量是否使重量实际上为负值,也即装不下
tmp=-2222223;
else
tmp=F[i][j-w[i]]+v[i];
if(F[i-1][j]>tmp)//递推关系
{
F[i][j]=F[i-1][j];
tag[i][j]=tag[i-1][j];
}
else
{
F[i][j]=F[i][j-w[i]]+v[i];
tag[i][j]=i;
}
}
}
cout<<"the max value:"<<F[n][weight]<<endl;
//========traceback====追溯解
int *x=new int[n+1]{};//初始化各种放入物品的数量都是0
int y=weight;
j=n;
while(true)
{
j=tag[n][y];//初始追踪位置,倒序追踪
x[j]=1;
y-=w[j];
while(tag[j][y]==j)//依旧可以放下第j种物品
{
y-=w[j];
x[j]=x[j]+1;//第j种物品数量++
}
if(tag[j][y]!=0)//可以放下的最大物品标号不为0
{
j=tag[j][y];
x[j]=1;
continue;
}
else break;
}
for(i=1;i<=n;++i)
{
cout<<"the "<<i<<" 's number:"<<x[i]<<endl;
}
return 0;
}
最长公共子列(字符串)
相关概念
-
子序列形式化定义:
-
给定一个序列 X = < x 1 , x 2 , x 3 , x 4 . . . , x m > X=<x_1,x_2,x_3,x_4...,x_m> X=<x1,x2,x3,x4...,xm>,另一个序列 Z = < z 1 , z 2 , z 3 , z 4 . . . , z k > Z=<z_1,z_2,z_3,z_4...,z_k> Z=<z1,z2,z3,z4...,zk>,若存在一个严格递增的X的下标序列 < i 1 , i 2 , i 3 , . . . , i k > <i_1,i_2,i_3,...,i_k> <i1,i2,i3,...,ik>对所有的1,2,3,…,k,都满足 x i k = z k x_{i_k}=z_k xik=zk,则称Z是X的子序列
-
比如Z=<B,C,D,B>是X=<A,B,C,B,D,A,B>的子序列
公共子序列定义:
- 如果Z既是X的子序列,又是Y的子序列,则称Z为X和Y的公共子序列
- 最长公共子序列(以下简称LCS):2个序列的子序列中长度最长的那个
解法
- 考虑动态规划算法,首先要思考如何界定子问题的边界。假设子问题的X和Y的初始位置都是从第一个元素开始,X的终止位置是第i个元素,Y的终止位置是第j个元素,那么
X
i
=
<
x
1
,
x
2
,
x
3
,
x
4
.
.
.
,
x
i
>
X_i=<x_1,x_2,x_3,x_4...,x_i>
Xi=<x1,x2,x3,x4...,xi>,
Y
i
=
<
y
1
,
y
2
,
y
3
,
y
4
.
.
.
,
y
i
>
Y_i=<y_1,y_2,y_3,y_4...,y_i>
Yi=<y1,y2,y3,y4...,yi>
就代表了由i和j共同界定的子问题的输入。
下面分析子问题的依赖性:
这是递推关系的基础:
X i = < x 1 , x 2 , x 3 , x 4 . . . , x i > X_i=<x_1,x_2,x_3,x_4...,x_i> Xi=<x1,x2,x3,x4...,xi>, Y i = < y 1 , y 2 , y 3 , y 4 . . . , y i > Y_i=<y_1,y_2,y_3,y_4...,y_i> Yi=<y1,y2,y3,y4...,yi>, Z k = < z 1 , z 2 , z 3 , z 4 . . . , z k > Z_k=<z_1,z_2,z_3,z_4...,z_k> Zk=<z1,z2,z3,z4...,zk>
那么: - (1)若 x i x_i xi= y j y_j yj,那么 z k z_k zk= x i x_i xi= y j y_j yj,且 Z k − 1 Z_{k-1} Zk−1是 X i − 1 X_{i-1} Xi−1和 Y j − 1 Y_{j-1} Yj−1的最长公共子序列。
- (2)若 x i x_i xi≠ y j y_j yj, z k z_k zk≠ x i x_i xi,那么 Z k − 1 Z_{k-1} Zk−1是 X i − 1 X_{i-1} Xi−1和 Y j Y_j Yj的最长公共子序列。
- (3)若 x i x_i xi≠ y j y_j yj, z k z_k zk≠ y j y_j yj,那么 Z k − 1 Z_{k-1} Zk−1是 X i X_i Xi和 Y j − 1 Y_{j-1} Yj−1的最长公共子序列。
如果
C
[
i
,
j
]
C[i,j]
C[i,j]表示
X
i
X_i
Xi和
Y
j
Y_j
Yj的最长公共子序列的长度。根据上述分析,得到优化函数如下:
C
[
i
,
j
]
=
{
C
[
i
−
1
,
j
−
1
]
+
1
if i,j>0,
x
i
=
y
j
m
a
x
{
C
[
i
,
j
−
1
]
,
C
[
i
−
1
,
j
]
}
if i,j>0,
x
i
!=
y
j
C[i,j] = \begin{cases} C[i-1,j-1]+1 & \text{if i,j>0,$x_i$=$y_j$} \\ max\{C[i,j-1],C[i-1,j]\} & \text{if i,j>0,$x_i$!=$y_j$} \end{cases}
C[i,j]={C[i−1,j−1]+1max{C[i,j−1],C[i−1,j]}if i,j>0,xi=yjif i,j>0,xi!=yj
C
[
0
,
j
]
=
C
[
i
,
0
]
=
0
C[0,j]=C[i,0]=0
C[0,j]=C[i,0]=0 如果
1
≤
i
≤
m
,
1
≤
j
≤
n
1≤i≤m,1≤j≤n
1≤i≤m,1≤j≤n(其中一个是空序列,那么公共子序列也只能是空序列)
得到算法迭代的伪码描述:
void LCS(X,Y,m,n)
{
void LCS(X,Y,m,n)
X[1,m],Y[1,n];
{
for(i=1;i<=m;++i)
C[i,0]=0;
for(i=1;i<=n;++i)
C[0,i]=0;
for(i=1;i<m;++i)
{
for(j=1;j<n;++j)
{
if(X[i]==Y[j])
{
C[i,j]=C[i-1,j-1]+1;
B[i,j]="lean";//↖微软输入法uujh获得
}
else if(C[i-1,j]>=C[i,j-1])
{
C[i,j]=C[i-1,j];
B[i,j]="up";//↑
}
else
{
C[i,j]=C[i,j-1];
B[i,j]="left";//←
}
}
}
}
Structure Sequence(B,i,j)
{
//B[i,j]
//X与Y的最长公共子序列
if(i=0||j=0)
{
return 0;
}
if(B[i,j]="lean")
{
cout<<X[i]
Structure Sequence(B,i-1,j-1);
}
else if(B[i,j]="up")
Structure Sequence(B,i-1,j);
else Structure Sequence(B,i,j-1);
}
}
C++代码
#include <iostream>
using namespace std;
void LCS(string s1,string s2)
{
int m=s1.size()+1;
int n=s2.size()+1;
int** C;
string** B;
int i,j;
C=new int* [m];//m行
B=new string* [m];
for(i=0;i<m;++i)
{
C[i]=new int [m];
B[i]=new string [n];
for(j=0;j<n;++j)
{
B[i][j]="0";//用于回溯
}
}
for(i=0;i<m;++i)
{
C[i][0]=0;
}
for(j=0;j<n;++j)
{
C[0][j]=0;
}
for(i=0;i<m-1;++i)//对原字符串处理,长度为m-1
{
for(j=0;j<n-1;++j)
{
if(s1[i]==s2[j])
{
C[i+1][j+1]=C[i][j]+1;
B[i+1][j+1]="lean";//↖微软输入法uujh获得
}
else if(C[i][j+1]>=C[i+1][j])
{
C[i+1][j+1]=C[i][j+1];
B[i+1][j+1]="up";//↑
}
else
{
C[i+1][j+1]=C[i+1][j];
B[i+1][j+1]="left";//←
}
}
}
stack<char> same; //栈,存LCS字符
stack<int> same1,same2; //存LCS字符在字符串1和字符串2中对应的下标,方便显示出来
for(int i = m-1,j = n-1;i >= 0 && j >= 0; )//倒序入栈,先进后出
{
if(B[i][j] == "lean")
{
i--;
j--;
same.push(s1[i]);
same1.push(i);//可以标记那些属于最长子列的元素
same2.push(j);//同上
}
else if(B[i][j] =="up")
i--;
else
j--;
}
while(!same.empty())
{
cout<<same.top();//输出栈顶
same.pop();//栈顶pop()掉
}
}
int main()
{
string s1="ABCPDSFJGODIHJOFDIUSHGD";
string s2="OSDIHGKODGHBLKSJBHKAGHI";
LCS(s1,s2);
return 0;
}