【动态规划】(上)投资问题,背包问题,最长子序列

介绍

参考:

背包问题第二种形式的递推方程–Wuuconix
动态规划算法典型应用之投资问题–Wuuconix
动态规划算法典型应用之背包问题–Wuuconix
动态规划解最长公共子序列(LCS)(附详细填表过程)

对于投资问题

一、问题描述:
一般性描述:设m元钱,n项投资项目,函数f_i(x)表示将x元投入第i项项目所产生的效益, i = 1 , 2 , ⋅ ⋅ ⋅ , n ; i=1,2,···,n; i=1,2n;
问:如何分配这 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=mxin;有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)=0xkxmax(fk(xk)+Fk1(xxk))

可以看到这里有一个对 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=1nvjxj
  • 动态规划算法的前期准备步骤主要是如何确定子问题的规模,用什么参数来确定,以及用几个参数。和投资问题的两个参数(投资前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 ) Fk1(y);还有一种情况就是选择了第k个物品,那么剩下的背包空间就是y-w[k],而因为每个物品可以选择多个,所以剩下的背包空间仍然可以在前k个物品中进行选择,那么取得的最大值就是 F k ( y − v k ) F_k(y−v_k ) Fk(yvk)

递推方程就是 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(Fk1(y),Fk(ywk)+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)=0xkwkymax(Fk1(ywkxk)+vkxk)
  • 经测试,应该使用新的标记函数,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} Zk1 X i − 1 X_{i-1} Xi1 Y j − 1 Y_{j-1} Yj1的最长公共子序列。
  • (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} Zk1 X i − 1 X_{i-1} Xi1 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} Zk1 X i X_i Xi Y j − 1 Y_{j-1} Yj1的最长公共子序列。

如果 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[i1,j1]+1max{C[i,j1],C[i1,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 1im,1jn(其中一个是空序列,那么公共子序列也只能是空序列)

得到算法迭代的伪码描述:
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;
}

在这里插入图片描述

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值