信奥集训营笔记:Day 4

动态规划

Posted by CR on August 7, 2018

动态规划

##一、定义

动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。

动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

与贪心的区别:我们在得到当前最优解的时候,需不需要使用前面的阶段中的得到的值,如果不需要,那就是贪心;如果需要,就是动归。

例1:Luogu P1216

#include <iostream>
#define MAX 1005
using namespace std;
int tower[MAX][MAX],n;
int main()
{

    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            cin>>tower[i][j];
    for(int i=n-1;i>=1;i--)
        for(int j=1;j<=i;j++)
            tower[i][j]+=max(tower[i+1][j],tower[i+1][j+1]);
    cout<<tower[1][1]<<endl;
}

例2:HDU P1176

#include <iostream>
#include <cstdio>
#include <cstring>
#define MAX 100005
using namespace std;
int n,x,T,dp[MAX][15],maxt;
inline int max3(int a,int b,int c)
{
    return max(max(a,b),c);
}

int main()
{
    while(~scanf("%d",&n))
    {
        memset(dp,0,sizeof(dp));
        maxt=-1;
        if(n==0)
            break;
        for(int i=1;i<=n;i++)
        {
            scanf("%d%d",&x,&T);
            if(maxt<T)
                maxt=T;
            dp[T][x]++;
        }
        for(int i=maxt-1;i>=1;i--)
        {
            dp[i][0]+=max(dp[i+1][0],dp[i+1][1]);
            dp[i][10]+=max(dp[i+1][10],dp[i+1][9]);
            for(int j=1;j<=9;j++)
                dp[i][j]+=max3(dp[i+1][j+1],dp[i+1][j],dp[i+1][j-1]);


        }
        printf("%d\n",max3(dp[1][4],dp[1][5],dp[1][6]));

    }
    return 0;
}

二、背包问题

1.01背包

定义

01背包是在M件物品取出若干件放在空间为W的背包里,每件物品的体积为W1,W2至Wn,与之相对应的价值为P1,P2至Pn。

01背包是背包问题中最简单的问题。

01背包的约束条件是给定几种物品,每种物品有且只有一个,并且有权值和体积两个属性。

在01背包问题中,因为每种物品只有一个,对于每个物品只需要考虑选与不选两种情况。

如果不选择将其放入背包中,则不需要处理。如果选择将其放入背包中,由于不清楚之前放入的物品占据了多大的空间,需要枚举将这个物品放入背包后可能占据背包空间的所有情况。

做法

用子问题定义状态:f[i][v]表示在前 i 件物品中选择若干件放在已用空间为 v 的背包里所能获得的最大价值

状态转移方程:

f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}(c[i]表示第i件商品的体积,w[i]表示第i件商品的价值)

例1:Luogu P2871(代码与题目略有不同)

#include <iostream>
#define MAX 12885
int tot_v,n,v[MAX],c[MAX],dp[MAX][MAX];
using namespace std;
int main()
{
    cin>>tot_v>>n;
    for(int i=1;i<=n;i++)
        cin>>v[i];
    for(int i=1;i<=n;i++)
        cin>>c[i];
    for(int i=1;i<=n;i++)
        for(int j=v[i];j<=tot_v;j++)
            dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+c[i]);
    cout<<dp[n][tot_v];
}

空间优化:

#include <iostream>
#define MAX 12885
using namespace std;
int dp[MAX],v[MAX],c[MAX],tot_v,n;
int main()
{
    cin>>tot_v>>n;
    for(int i=1;i<=n;i++)
        cin>>v[i];
    for(int i=1;i<=n;i++)
        cin>>c[i];
    for(int i=1;i<=n;i++)
        for(int j=tot_v;j>=v[i];j--)//每个物品只能拿一次,如果从前往后就会买多次
            dp[j]=max(dp[j],dp[j-v[i]]+c[i]);
    cout<<dp[tot_v];
}

例2:HDU P1248

#include <iostream>
using namespace std;
int n,dp[100005],t,res[105];
int main()
{

    const int price[4]={0,150,200,350};
    cin>>t;

    for(int k=1;k<=t;k++)
    {
        cin>>n;
        for(int i=1;i<=3;i++)
            for(int j=price[i];j<=n;j++)
                dp[j]=max(dp[j],price[i]+dp[j-price[i]]);
        cout<<n-dp[n]<<endl;
    }
    return 0;
}

例3:HDU P1203

法一:

#include <iostream>
#include <cstdio>
#define MAX 10005
using namespace std;
int n,m,a[MAX];
double b[MAX],dp[MAX],res;
int main()
{

    while(~scanf("%d%d",&n,&m))
    {
        if(n==0&&m==0)
            break;
        for(int i=1;i<=m;i++)
            cin>>a[i]>>b[i];
        for(int i=0;i<=n;i++)//注意从零开始赋初始值!!!
            dp[i]=1;//或用memset(dp,1,sizeof(dp));
        for(int i=1;i<=m;i++)
            for(int j=n;j>=a[i];j--)
                dp[j]=min(dp[j],dp[j-a[i]]*(1-b[i]));

        printf("%.1lf%%\n",(1-dp[n])*100);
    }
}

法二:

#include <iostream>
#include <cstdio>
#include <cstring>
#define MAX 10005
using namespace std;
int n,m,a[MAX];
double b[MAX],dp[MAX],res;
inline double work(double a,double b)
{
    return 1-(1-a)*(1-b);
}
int main()
{

    while(~scanf("%d%d",&n,&m))
    {
        if(n==0&&m==0)
            break;
        for(int i=1;i<=m;i++)
            cin>>a[i]>>b[i];
        memset(dp,0,sizeof(dp));//注意归零!!!
        for(int i=1;i<=m;i++)
            for(int j=n;j>=a[i];j--)
                dp[j]=max(dp[j],work(dp[j-a[i]],b[i]));
        printf("%.1lf%%\n",dp[n]*100);
    }
}

2.完全背包

定义

有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的体积是c,价值是w。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。

做法

思路: 1.将完全背包转换成01背包(空间复杂度大,不推荐)

用总容积除以每种物品的体积,为n,将每种物品拆分成有相同价值的n件物品

2.直接用完全背包做

状态转移方程:

f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]}(c[i]表示第i件商品的体积,w[i]表示第i件商品的价值)

注意:max函数内第二个参数是f[i][v-c[i]]+w[i],因为可以重复拿同一件物品

例1:Luogu P2722

未经优化(会超内存)

#include <iostream>
#define MAX 10005
using namespace std;
int m,n,score[MAX],time[MAX],dp[MAX][MAX];
int main()
{

    cin>>m>>n;
    for(int i=1;i<=n;i++)
        cin>>score[i]>>time[i];
    for(int i=1;i<=n;i++)
        for(int j=time[i];j<=m;j++)
            dp[i][j]=max(dp[i-1][j],dp[i][j-time[i]]+score[i]);
    cout<<dp[n][m]<<endl;
}

#include <iostream>
#define MAX 10005
using namespace std;
int m,n,time[MAX],score[MAX],dp[MAX];
int main()
{
    cin>>m>>n;
    for(int i=1;i<=n;i++)
        cin>>score[i]>>time[i];
    for(int i=1;i<=n;i++)
        for(int j=time[i];j<=m;j++)
            dp[j]=max(dp[j],dp[j-time[i]]+score[i]);
    cout<<dp[m]<<endl;
}

延伸阅读:崔添翼-背包问题九讲