动态规划

动态规划的递归写法

1、应理解动态规划是如何记录子问题的解,来避免下次遇到相同的子问题时的重复计算的。又称记忆化搜索。

2、为了避免重复计算,可以开一个一维数组,用以保存已经计算过的结果,其中dp[n]记录F[n]的结果,并用dp[n]=-1表示F[n]当前还没有计算过。

例如斐波拉契数列,一个问题必须要有重叠子问题,才能使用动态规划去解决。

int F(int n)
{
	if(n==0||n==1)
		return 1;
	if(dp[n]!=-1)
		return dp[n];
	else
	{
		dp[n]=F(n-1)+F(n-2);//计算F(n),并保存至dp[n];
		return dp[n];//返回F[n]的结果
	}
}

动态规划的递推写法

1、经典数塔问题:dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+f[i][j],把dp[i][j]称问题的状态,把这个式子称为状态转移方程

状态dp[i][j]只与第i+1层的状态有关,而与其他层的状态无关。数塔的最后一层的dp值总是等于元素本身,即dp[n][j]=f[n][j],把这种可以直接确定其结果的部分称为边界,而动态规划的递推写法总是从这些边界出发,通过状态转移方程扩散到整个dp数组

    从最底层各位置的dp值开始,不断往上求出每一层各位置的dp值,最后就可以得到dp[1][1]

#include <iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=1000;
int f[maxn][maxn],dp[maxn][maxn];
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {

        for(int j=1;j<=i;j++)
        {
            scanf("%d",&f[i][j]);//输入数塔
        }
    }
    //边界
    for(int j=1;j<=n;j++)
    {
        dp[n][j]=f[n][j];
    }
    //从第n-1层不断往上计算出dp[i][j]
    for(int i=n-1;i>=1;i--)
        for(int j=1;j<=i;j++)
    {
        //状态转移方程
        dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+f[i][j];
    }
    cout <<dp[1][1] << endl;
    return 0;
}

区别:使用递推写法的计算方式是自底向上,即从边界开始,不断向上解决问题,直到解决了目标问题;

           使用递归写法的计算方式是自顶向下,即从目标问题开始,将它分解成子问题的组合,直到分解到边界为止

一个问题必须拥有重叠子问题和最优子结构(一个问题的最优解可以由其子问题的最优解有效地构造出来),才能使用动态规划去解决

最大连续子序列和

1、给定一个数字序列A1,A2。。。An,求i,j(1<=i<=j<=n),使得Ai+Aj最大,输出这个最大和。

步骤1:令状态dp[i]表示以A[i]作为末尾的连续序列的最大和

步骤2:因为dp[i]要求是必须以A[i]结尾的连续数列,那么只有两种情况:

            ①这个最大和的连续数列只有一个元素,即以A[i]开始,以A[i]结尾

             ②这个最大和的连续数列有多个元素,即从前面某处A[p]开始(p<i),一直到A[i]结尾

   所以,状态转移方程为:  dp[i]=max(A{i],dp[i-1]+A[i]).


#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=10010;
int A[maxn],dp[maxn];//A[i]存放序列,dp[i]存放以A[i]结尾的最大数列
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=0;i<n;i++)
    {

        //读入序列
        scanf("%d",&A[i]);
    }
    //边界
    dp[0]=A[0];
    for(int i=1;i<n;i++)
    {
        //状态转移方程
        dp[i]=max(A[i],dp[i-1]+A[i]);
    }
    //dp[i]存放以A[i]结尾的连续数列最大和,需要遍历i得到最大的才是结果
    int k=0;
    for(int i=1;i<n;i++)
    {
        if(dp[i]>dp[k])
            k=i;
    }
    printf("%d\n",dp[k]);

    return 0;
}

最长不下降子序列(LIS)

例如:现有序列A={1,2,3,-1,-2,7,9}(下标从1开始),它的最长不下降子序列是{1,2,3,7,9},长度为5。另外,还有一些子序列是不下降子序列,比如{1,2,3},{-2,7,9}等,但都不是最长的

条件:A[i]>=A[j]&&dp[j]+1>dp[i]     则dp[i]=dp[j]+1

状态转移方程: dp[i]=max(1,dp[j]+1)  (j=1,2,3,,,,i-1&&A[j]<A[i])

#include <iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=100;
int A[N],dp[N];
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&A[i]);
    }
    int ans=-1;//记录最大的dp[i]
    for(int i=1;i<=n;i++)//按顺序计算出dp[i]的值
    {


    dp[i]=1;//边界初始条件(即假设每个元素自成一格子序列)
    for(int j=1;j<i;j++)
    {
        if(A[i]>=A[j]&&(dp[j]+1>dp[i]))
        {
            dp[i]=dp[j]+1;//状态转移方程,用以更新dp[i]
        }

    }
    ans=max(ans,dp[i]);
    }
   printf("%d\n",ans);
    return 0;
}

最长公共子序列(LCS)

状态转移方程: ①dp[i]=dp[i-1][j-1]+1,  当A[i]=B[j]

                           ②max(dp[i-1][j],dp[i][j-1]),当A[i]!=B[j]

边界:dp[i][0]=dp[0][j]=0(0<=i<=n,0<=j<=m)

#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=100;
char A[N],B[N];
int dp[N][N];
int main()
{

   gets(A+1);//从下标为1开始读入
   gets(B+1);
   int lenA=strlen(A+1);//由于读入时从下标1开始,因此读取长度也从+1开始
   int lenB=strlen(B+1);
   //边界
   for(int i=0;i<=lenA;i++)
   {
       dp[i][0]=0;
   }
   for(int j=0;j<=lenB;j++)
   {
       dp[0][j]=0;
   }
   //状态转移方程
   for(int i=1;i<=lenA;i++)
   {
       for(int j=1;j<=lenB;j++)
       {
           if(A[i]==B[j])
            dp[i][j]=dp[i-1][j-1]+1;
           else
           dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
       }
   }
   //dp[lenA][lenB]是答案
   printf("%d\n",dp[lenA][lenB]);
   return 0;
}

最长回文子串

例如:字符串“PATZJUJZTACCBCC”的最长回文子串为“ATZJUJZTA”,长度为9

状态转移方程: dp[i][j]=dp[i+1][j-1],当S[i]=S[j]

                           dp[i][j]=0,当S[i]!=S[j]

边界:dp[i][j]=1,dp[i][i+1]=(S[i]==S[i+1])?1:0

#include <iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=1010;
char S[maxn];
int dp[maxn][maxn];
int main()
{
    gets(S);
    int len=strlen(S),ans=1;
    memset(dp,0,sizeof(dp));//dp数组初始化为0,是回文子串则为1,不是则为0
    //边界
    for(int i=0;i<len;i++)
    {

        dp[i][i]=1;
        if(i<len-1)
        {
            if(S[i]==S[i+1])
            {
                dp[i][i+1]=1;
                ans=2;//初始化时注意当前最长回文子串长度
            }
        }
    }
    //状态转移方程
    for(int L=3;L<=len;L++)//枚举子串的长度
    {
        for(int i=0;i+L-1<len;i++)//枚举子串的起始端点
        {
            int j=i+L-1;//子串的右端点
            if(S[i]==S[j]&&dp[i+1][j-1]==1)
            {
                dp[i][j]=1;
                ans=L;//更新最长回文子串长度
            }
        }
    }
    printf("%d\n",ans);
    return 0;
}

01背包问题

1、问题是这样的:有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包物品的总价值最大,其中每种物品都只有一件。

    令dp[i][v]表示前i件物品(1<=i<=n,0<=v<=V)恰好装入容量为v的背包中所能获得的最大价值

   ①不放第i件物品,那么问题转化为前i-1件物品恰好装入容量为v的背包中所能获得的最大价值,也即dp[i-1][v];

   ②放第i件物品,那么问题转化为前i-1件物品恰好装入容量为v-w[i]的背包中所能获得的最大价值,也即dp[i-1][v-w[i]]+c[i]

    则状态转移方程 dp[i][v]=max(dp[i-1][v],dp[i-1][v-w[i]]+c[i])  (1<=i<=n,w[i]<=v<=V)

for(int i=1;i<=n;i++)
{
	for(int v=w[i];v<=V;v++)
	{
		dp[i][v]=max(dp[i-1][v],dp[i-1][v-w[i]]+c[i]);
	}
}

2、因为dp[i][v]总是只需要dp[i-1][v]的数据,不妨开一个一维数组dp[v](即把一维省去),枚举方向改变为i从1到n,v从V到0(逆序!),这样状态转移方程改变为:  dp[v]=max(dp[v],dp[v-w[i]]+c[i])

for(int i=1;i<=n;i++)
{
	for(int v=V;v>=w[i];v--)//逆序枚举v
		dp[v]=max(dp[v],dp[v-w[i]]+c[i]);
}

特别说明:如果是用二维数组存放,v的枚举是顺序还是逆序都无所谓,如果使用一维数组存放,则v的枚举必须是逆序

完整的求解01背包问题

#include <iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=100;//物品最大件数
const int maxv=1000;//V的上限
int w[maxn],c[maxn],dp[maxn];
int main()
{
    int n,V;
    scanf("%d%d",&n,&V);
    for(int i=0;i<n;i++)
    {
        scanf("%d",&w[i]);
    }
    for(int i=0;i<n;i++)
    {
        scanf("%d",&c[i]);
    }
    //边界
    for(int v=0;v<=V;v++)
    {
        dp[v]=0;

    }
    for(int i=1;i<=n;i++)
    {
        for(int v=V;v>=w[i];v--)
        {
            //状态转移方程
            dp[v]=max(dp[v],dp[v-w[i]]+c[i]);
        }
    }
    //寻找dp[0...V]中最大的即为答案
    int max=0;
    for(int v=0;v<=V;v++)
    {
        if(dp[v]>max)
            max=dp[v];
    }
    printf("%d\n",max);

    return 0;
}

对于能够划分阶段的问题来说,都可以尝试把阶段作为状态的一维。

完全背包问题

1、问题叙述如下:有n种物品,每种物品的单位重量为w[i],价值为c[i],现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大,其中每种物品都有无穷件

状态转移方程:dp[i][v]=max(dp[i-1][v],dp[i][v-w[i]]+c[i])  (1<=i<=n,w[i]<=v<=V)

 边界:dp[0][v]=0;(0<=v<=V)

同样可以改写成一维形式,即状态转移方程为:dp[v]=max(dp[v],dp[v-w[i]]+c[i]) (1<=i<=n,w[i]<=v<=V)

边界:dp[v]=0(0<=v<=V)

完全背包与01背包问题类似,但这里v的枚举顺序是正向枚举

for(int i=1;i<=n;i++)
{
	for(int v=w[i];v<=V;v++)//正向枚举v
	{
		dp[v]=max(dp[v],dp[v-w[i]]+c[i]);
	}
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值