<基础DP>『线性dp实练』

<前言>
两次dp实练考试的题目分析


<更新提示>

<第一次更新>更新前一次的三道考试题,剩下四道马上给出。
<第二次更新>后四道dp已经给出


<正文>

<例一>

1.跑步
[Neal Wu, 2007]
奶牛们打算通过锻炼来培养自己的运动细胞,作为其中的一员,贝茜选择的运动方式是每天进行N(1 <= N <= 10,000)分钟的晨跑。在每分钟的开始,贝茜会选择下一分钟是用来跑步还是休息。贝茜的体力限制了她跑步的距离。更具体地,如果贝茜选择在第i分钟内跑步,她可以在这一分钟内跑D_i(1 <= D_i <= 1,000)米,并且她的疲劳度会增加1。不过,无论何时贝茜的疲劳度都不能超过M(1 <= M <= 500)。如果贝茜选择休息,那么她的疲劳度就会每分钟减少1,但她必须休息到疲劳度恢复到0为止。在疲劳度为0时休息的话,疲劳度不会再变动。晨跑开始时,贝茜的疲劳度为0。还有,在N分钟的锻炼结束时,贝茜的疲劳度也必须恢复到0,否则她将没有足够的精力来对付这一整天中剩下的事情。
请你计算一下,贝茜最多能跑多少米。

程序名: cowrun

【输入格式】
第1行: 2个用空格隔开的整数:N 和 M
第2..N+1行: 第i+1为1个整数:D_i

【输入样例】 (cowrun.in)
5 2
5
3
4
2
10

【输出格式】
第1行: 输出1个整数,表示在满足所有限制条件的情况下,贝茜能跑的最大距离

【输出样例】 (cowrun.out)

9

【输出说明】
贝茜在第1分钟内选择跑步(跑了5米),在第2分钟内休息,在第3分钟内跑步(跑了4米),剩余的时间都用来休息。因为在晨跑结束时贝茜的疲劳度必须为0,所以她不能在第5分钟内选择跑步。

解析
对于简单的线性dp,设置状态最简单的方式就是求什么设什么,显然可以设f[i][j]为第i分钟疲劳值为j的最远距离。那么,根据题意,状态有三种转移方式:
1 f[i][0]=max(f[i-j][j],f[i][0]);(选择休息)
2 f[i][0]=max(f[i-1][0],f[i][0]);(在疲劳值为0时选择休息)
3 f[i][j]=max(f[i-1][j-1]+d[i],f[i][j]);(选择跑步)

f[i][0]={max(f[ij][j],f[i][0])max(f[i1][0],f[i][0])


f[i][j]=max(f[i1][j1]+d[i],f[i][j])

那么状态转移方程显然就是这样的,直接枚举i,j暴力转移状态即可。所以最后的答案即为f[n][0]。同时,这道题说明了dp需要善于根据题意变通的道理,如果一开始暴力转移f[i][j]而不转移f[i][0](像我),就0分了。
参考代码:

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
    int w=0,x=0;char ch;
    while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return w?-x:x;
}
int n,m,f[10080][800]={},dis[10080]={};
int main()
{
    n=read();m=read();
    for(int i=1;i<=n;i++)dis[i]=read();
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            f[i][j]=max(f[i][j],f[i-1][j-1]+dis[i]);//情况 3
            if(i-j>=0)f[i][0]=max(f[i][0],f[i-j][j]);//情况 1 
        }
        f[i][0]=max(f[i][0],f[i-1][0]);//情况 2 
    }
    cout<<f[n][0]<<endl;
    return 0;
}

<例二>

2.渡河问题
[Jeffrey Wang, 2007]
Farmer John以及他的N(1 <= N <= 2,500)头奶牛打算过一条河,但他们所有的渡河工具,仅仅是一个木筏。
由于奶牛不会划船,在整个渡河过程中,FJ必须始终在木筏上。在这个基础上,木筏上的奶牛数目每增加1,FJ把木筏划到对岸就得花更多的时间。
当FJ一个人坐在木筏上,他把木筏划到对岸需要M(1 <= M <= 1000)分钟。当木筏搭载的奶牛数目从i-1增加到i时,FJ得多花M_i(1 <= M_i <= 1000)分钟才能把木筏划过河(也就是说,船上有1头奶牛时,FJ得花M+M_1分钟渡河;船上有2头奶牛时,时间就变成M+M_1+M_2分钟。后面的依此类推)。那么,FJ最少要花多少时间,才能把所有奶牛带到对岸呢?当然,这个时间得包括FJ一个人把木筏从对岸划回来接下一批的奶牛的时间。

程序名: river

【输入格式】
第1行: 2个用空格隔开的整数:N 和 M
第2..N+1行: 第i+1为1个整数:M_i

【输入样例】 (river.in)

5 10
3
4
6
100
1

【输入说明】
FJ带了5头奶牛出门。如果是单独把木筏划过河,FJ需要花10分钟,带上1头奶牛的话,是13分钟,2头奶牛是17分钟,3头是23分钟,4头是123分钟,将5头一次性载过去,花费的时间是124分钟。

【输出格式】

第1行: 输出1个整数,为FJ把所有奶牛都载过河所需的最少时间

【输出样例】 (river.out)

50

【输出说明】
Farmer John第一次带3头奶牛过河(23分钟),然后一个人划回来(10分钟),最后带剩下的2头奶牛一起过河(17分钟),总共花费的时间是23+10+17 = 50分钟。

解析
对于这道题的思路是这样的:照样先设置状态,f[i]表示带上i头奶牛的最小花费。为了便于转移状态,我们需要对每增加一头奶牛的花费进行前缀和处理,记录在数组spend[i]中,当然spend[0]=m,即为FJ自己的花费。这样状态转移方程就可以写出了:

f[i]=min(f[i],f[j]+spend[ij]+m)(1<i<n+1)(j<0<i+1)

接下来枚举i,j转移状态即可。
参考代码:

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
    int w=0,x=0;char ch;
    while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return w?-x:x;
}
int n,m,spend[2580]={},f[3080]={}; 
int main()  
{  
     n=read();m=read();
     spend[0]=m;
     for(int i=1;i<=n;i++){spend[i]=read();spend[i]+=spend[i-1];}
     memset(f,10,sizeof(f));
     f[0]=0,f[1]=spend[1];
     for(int i=2;i<=n;i++)
     {
        f[i]=spend[i];
        for(int j=1;j<=i;j++)
        {
            f[i]=min(f[i],f[j]+spend[i-j]+m); //spend[i-j]表示加上i到j这些牛需要的花费
        }
     }
     cout<<f[n]<<endl;
     return  0;
}  

<例三>

3.数字平方和
(numsquare.pas/c/cpp)
这是一个很无趣的数字游戏,给你n个整数,再给一个目标和sum。你可以调整n个数中任意一个数的大小,但是要让调整好的n个整数的平方和和sum相等。
比如有3个数 1 3 3。目标和是6,那么我们可以把1变成2,把两个3都变成1,这样就完成调整。但是为了提高难度,我们规定假设某个数为A_i,调整为B_i,那么要花费的代价是|A_i-B_i|*|A_i-B_i|。
那么上面的算法花费的代价就是9.当然肯定会有更小代价的算法。
现在问题提出来,把n个数调整到位,最小花费的代价是多少?
如果存在无解的情况,输出”-1”。
【输入】
第一行两个空格隔开的整数:n (1<=n<=10) and sum (1<=sum<=10,000)
接下来为n行,每行一个整数A_i(1<=A_i<=100)
【输出】
一个整数,花费的最小代价

【输入输出样例1】
numsquare.in numsquare.out
3 6
3
3
1 5

解析
读题后,简要思考可以发现,处理开方很困难,所以在设置状态时尽量避免开方,使用平方处理。设f[i][j]表示前i个数,组成平方和是j的最小代价。那么枚举数字,枚举平方数,枚举事先计算好的花费就能实现三重循环对状态进行转移。方程详见代码,较为复杂不再列出。
参考代码:

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
    int w=0,x=0;char ch;
    while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return w?-x:x;
}
int n,sum,num[18]={};
inline int changespend(int last,int now)
{
    return abs(last-now)*abs(last-now);
}
int f[18][10080]={};
//f[i][j] 表示前i个数,组成平方和是j的最小代价
int spend[18][10080]={};
//spend[i][j]表示从num[i]变到某个数字[j]的花费 
int main()  
{  
     n=read();sum=read();
     for(int i=1;i<=n;i++)num[i]=read();
     for(int i=1;i<=n;i++)
     {
        for(int j=0;j<=200;j++)
        {
            spend[i][j]=changespend(num[i],j);
        }
     }
     //计算200以内从num i变到数字j的话费 
     memset(f,10,sizeof(f));
     int flag=f[0][0];
     //标记 
     f[0][0]=0;
     //dp
     for(int i=1;i<=n;i++)//枚举数字 
     {
        for(int j=0;j*j<=sum;j++)//找到小于sum的一个平方数 
         {
            for(int k=sum-j*j;k>=0;k--)//逆序枚举剩下的数 
            {
                f[i][k+j*j]=min(f[i][k+j*j],f[i-1][k]+spend[i][j]);
                //通过事先计算好的花费进行状态转移 
            }
        }
     }
     if(flag==f[n][sum])cout<<-1<<endl;//答案值不变说明该数字无法用平方和表示 
     else cout<<f[n][sum]<<endl;//否则输出最优解 
     return  0;
}  

<例四>
马拉松
Description
Unhappy with the poor health of his cows, Farmer John enrolls them in an assortment of different physical fitness activities. His prize cow Bessie is enrolled in a running class, where she is eventually expected to run a marathon through the downtown area of the city near Farmer John’s farm! The marathon course consists of N checkpoints (3 <= N <= 500) to be visited in sequence, where checkpoint 1 is the starting location and checkpoint N is the finish. Bessie is supposed to visit all of these checkpoints one by one, but being the lazy cow she is, she decides that she will skip up to K checkpoints (K < N) in order to shorten her total journey. She cannot skip checkpoints 1 or N, however, since that would be too noticeable. Please help Bessie find the minimum distance that she has to run if she can skip up to K checkpoints. Since the course is set in a downtown area with a grid of streets, the distance between two checkpoints at locations (x1, y1) and (x2, y2) is given by |x1-x2| + |y1-y2|.

在二维平面上有N个点,从(x1,y1)到(x2,y2)的代价为|x1-x2|+|y1-y2|。
求从1号点出发,按从1到N的顺序依次到达每个点的最小总代价。
你有K次机会可以跳过某个点,不允许跳过1号点或N号点。
Input
The first line gives the values of N and K. The next N lines each contain two space-separated integers, x and y, representing a checkpoint (-1000 <= x <= 1000, -1000 <= y <= 1000). The checkpoints are given in the order that they must be visited. Note that the course might cross over itself several times, with several checkpoints occurring at the same physical location. When Bessie skips such a checkpoint, she only skips one instance of the checkpoint – she does not skip every checkpoint occurring at the same location.

Output
Output the minimum distance that Bessie can run by skipping up to K checkpoints. In the sample case shown here, skipping the checkpoints at (8, 3) and (10, -5) leads to the minimum total distance of 4.

Sample Input
5 2

0 0

8 3

1 1

10 -5

2 2

Sample Output
4

解析
这是一道暴力线性dp,求什么设什么,使f[i][j]表示到第i个跳过了j个的最小值就可以对状态进行转移了。考虑转移的方法:枚举三参数进行转移:第一个和第二个显而易见枚举点(i)和跳过的次数(j),但是,题面中并没有说一次不能连续使用多次机会来跳过某些点。即可以从点1使用一次机会跳到点3,也同样可以使用两次机会直接跳到点4,而又因为机会有k次,所以我们需要在枚举一个数字l,表示连续使用的机会数,这样就能转移状态了。
故状态状态转移方程如下:

f[i][j]=min(f[i][j],f[il1][jl]+spend(il1,i));

参考代码:

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
    int w=0,x=0;char ch;
    while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return w?-x:x;
}
struct POINT
{
    int x,y;
};
int n,k,f[580][580]={};
//f[i][j]表示到了第i个点跳过了j次机会的最小总代价 
POINT Map[10080]={};
inline int spend(int a,int b)
{
    int x1=Map[a].x;
    int x2=Map[b].x;
    int y1=Map[a].y;
    int y2=Map[b].y;
    return (abs(x1-x2)+abs(y1-y2));
}
int main()
{
    n=read();k=read();
    for(int i=1;i<=n;i++)Map[i].x=read(),Map[i].y=read();
    memset(f,10,sizeof(f));
    f[1][0]=0;
    for(int i=1;i<=n;i++)f[i][i]=0;

    for(int i=2;i<=n;i++)
    {
        for(int j=0;j<=min(i-1,k);j++)
        {
            for(int l=0;l<=j;l++)
            {
                f[i][j]=min(f[i][j],f[i-l-1][j-l]+spend(i-l-1,i));
            }
        }
    }
    cout<<f[n][k]<<endl;
    return 0;
}

<例五>
养猪
Description
你有一个猪圈,有N头猪,每天你最多可以杀一头猪卖钱,收益就是猪的体重。但是每过一天猪的体重都会下降Pi,问K天内你的最大获利。
Input
第一行两个正整数N、K;
第二行N个数表示猪的初始重量A[i];
第三行N个数表示P[i];
Output
一行一个整数表示最大的获利。
Sample Input
2 2 10 10 1 2
Sample Output
19
Hint
20% 数据满足 1≤N≤20
100%数据满足1≤N≤1000, 体重≤105

解析
先思考一个贪心问题:如果第i头猪的p[i] 比 第j头猪 的p[j] 要大, 第i头猪肯定要比第j头猪先选。事实上,我们无需考虑他们的初值,这是可以用反证法证明的。这样,只要先对猪按p[i]排序,我们就能dp了。设f[i][j]代表前i头猪杀j头的最大价值,排序后,依次枚举每一头猪,在枚举杀猪的头数,状态的转移就只有两种情况:
1 新增加的这一头猪没杀
2 新增加的这一头猪杀了
状态转移方程就是这个,为了避免负数,只需对猪的价值再和0取一个max就好了。
状态转移方程如下:

f[i][j]=max(f[i1][j],f[i1][j1]+max(0,p[i].stp[i].se(j1)))(p[i].stp[i].se)

这时,在1至n直接枚举i,在1至min(i,k)中枚举j就能转移状态了。

参考代码:

using namespace std;
inline int read()
{
    int w=0,x=0;char ch;
    while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return w?-x:x;
}
int n,k,f[1080][1080]={};
//f[i][j]代表前i头猪杀j头的最大价值 
struct a
{
    int st,se;
}p[1080];
inline bool cmp(a x,a y)
{
    return x.se>y.se;
}
int main()
{
    n=read();k=read();
    for(int i=1;i<=n;i++)p[i].st=read();
    for(int i=1;i<=n;i++)p[i].se=read();
    sort(p+1,p+n+1,cmp);
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=min(i,k);j++)
        {
            f[i][j]=max(f[i-1][j],f[i-1][j-1]+max(0,p[i].st-p[i].se*(j-1)));
        }
    } 
    int ans=-1;
    for(int i=1;i<=k;i++)ans=max(ans,f[n][i]);
    cout<<ans<<endl;
    return 0;
}

<例六>

Description
Bessie has broken into Farmer John’s house again! She has discovered a pile of lemons and a pile of oranges in the kitchen (effectively an unlimited number of each), and she is determined to eat as much as possible.

Bessie has a maximum fullness of T (1≤T≤5,000,000). Eating an orange increases her fullness by A, and eating a lemon increases her fullness by B (1≤A,B≤T). Additionally, if she wants, Bessie can drink water at most one time, which will instantly decrease her fullness by half (and will round down).

Help Bessie determine the maximum fullness she can achieve!
奶牛Bessie潜入了农夫约翰的家,她发现这里有无穷无尽的柠檬派和橘子派。
Bessie的饱胀值一开始是0,且上限是T,每个柠檬派可以提供A点饱胀值,每个橘子派可以提供B点饱胀值。
Bessie可以不断地吃东西,如果她的饱胀值没有超出T的话。同时,Bessie有一次喝水的机会,喝完后,她的饱胀值将减少一半(往下取整)。
请计算出Bessie的饱胀值最多可以达到多少。
Input
The first (and only) line has three integers T, A, and B.
Output
A single integer, representing the maximum fullness Bessie can achieve.
Sample Input
8 5 6
Sample Output
8

解析
这道题的数据量看似很大,实则不必恐惧。可以将dp的状态写成一维的。设f[i][0/1]表示能否凑成i,0/1表示是否使用那个机会,再线性扫描即可。关于状态的转移,像递推一样一层一层推就行了,当然枚举一下0/1,拓展当前状态除以2的状态。
方程就没有啦,当递推推一下就行了。
参考代码:

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
    int w=0,x=0;char ch;
    while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return w?-x:x;
}
int t,a,b;
int f[5000800][2]={};
int main()
{
    t=read();a=read();b=read();
    f[0][0]=1;
    for(int j=0;j<=1;j++)
    {
        for(int i=0;i<=t;i++)
        {
            if(f[i][j])
            {
                if(i+a<=t)f[i+a][j]=1;
                if(i+b<=t)f[i+b][j]=1;
                if(!j)f[i/2][1]=1;
            }
        }
    }
    int ans=t;
    while(1)
    {
        if(f[ans][0]||f[ans][1])
        {
            cout<<ans<<endl;
            return 0;
        }
        else
        {
            ans--;
        }
    }
    return 0; 
}

<例五>
背包问题
问题描述
从T组物品中选出一些物品,放入背包中,求剩余空间的最小值。
限制条件:从每组物品中挑选物品必须要选取连续的一段。就是说,如果这组物品共有n个: 物品1、物品2、物品3、…、物品n,那么只能选取物品i、物品i+1、…、物品j,其中1<=i<=j<=n,或者不选。
输入
第一行为两个用空格隔开的正整数v和T。表示背包的空间和物品的组数。接下来有T行,每行先是一个正整数ni,表示这组物品有ni个,然后ni个正整数,表示每个物品的大小。
输出
仅一个数,表示剩余空间的最小值。
样例输入
100 3
3 7 6 8
2 80 70
4 101 108 103 150
样例输出
6

【样例说明】 第1组选6、8,第2组选80,第3组不选。

【限制】
60%的数据满足:1 <= ni <= 10
100%的数据满足:1 <= ni <= 100,1<=v<=5000,1<=T<=10

解析
仔细看题面,不难发现这个“背包问题”实际非常棘手。在dp的思路上看来,直接dp是几乎不可能的,我们需要预处理许多东西。先看dp,显而易见,状态使f[i,j]表示前T组物品,用了j体积装物品,能装载的物品的最大值。 但在枚举时j极难处理,我们尝试处理出b[i][j]表示第i组物品,是否出现连续一段总体积为j的情况看,然后怎么去做b[i,j]?用一个前缀和去统计,然后暴力去枚举判断就好了。 那就需要处理一个前缀和了。如果能实现的话,就可以用这个状态转移方程来转移:

f[i][j]=f[i1][j]if(b[i][k])f[i][j]=max(f[i][j],f[i1][jk]+k)(1<=i<=t,0<=j<=v,0<=k<=j)

在状态的转移中,我们不断记录更新最大值,就是背包能装的最大体积了。再用v减去最大体积,就是题目所求的剩余空间最小体积。这是一道很好的需要预处理一些数据的dp,仔细思考才能求解。
具体的思路已经放在代码注释中详解。
参考代码:

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
    int w=0,x=0;char ch;
    while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return w?-x:x;
}
int v,t,n[18]={},bag[18][180]={},sum[18][180]={},b[18][8080]={},f[18][8080]={};
//b[i,j]表示第i组物品,是否出现连续一段总体积为j的情况。
//f[i,j]表示前T组物品,用了j体积装物品,能装载的物品的最大值
int main()
{
    v=read(),t=read();
    for(int i=1;i<=t;i++)
    {
        n[i]=read();
        for(int j=1;j<=n[i];j++)
        {
            bag[i][j]=read();//输入 
            sum[i][j]=sum[i][j-1]+bag[i][j];//处理前缀和 
        }
    }
    for(int i=1;i<=t;i++)//枚举组 
    {
        for(int j=1;j<=n[i];j++)//枚举每一个 
        {
            for(int k=1;k<=j;k++)
            {
                for(int l=1;l<=k;l++)//k和l两次枚举第j个以前的某个物品 ,一个为左端点,一个为右端点,构成了一段连续的物品 
                {
                    if(sum[i][k]-sum[i][l-1]<=v)//如果该段物品的体积和小于总体积,记录 
                    b[i][sum[i][k]-sum[i][l-1]]=1;//表示在第i组中,存在着连续的一段物品体积为  s[i][k]-s[i][l-1]
                }
            }
        }
    }
    int Max=-1;
    //dp
    //状态转移方程:f[i][j]= max(f[i][j],f[i-1][j-k]+k);(k<=j) 
    for(int i=1;i<=t;i++)//枚举每一组 
    {
        for(int j=0;j<=v;j++)//枚举每一个可能的体积 
        {
            f[i][j]=f[i-1][j];//初值为上一组物品用了该体积的最大值 
            for(int k=0;k<=j;k++)//枚举k,对状态进行转移 
            {
                if(b[i][k])f[i][j]=max(f[i][j],f[i-1][j-k]+k);
                //通过b数组的预处理判断得知,有一段连续的物品体积和为k,使用k转移至下一阶段 
            }
            Max=max(Max,f[i][j]);//找到体积最大值 
        }
    } 
    cout<<v-Max<<endl;//v-Max即为剩余空间的最小值 
    return 0;
}

<后记>
七道线性dp就到此结束了,对基础dp的思维需要更深的了解。
预计下一个专题:
<基础DP>『区间dp实练』或<基础DP>『资源分配实练』


<废话>

猜你喜欢

转载自blog.csdn.net/prasnip_/article/details/79983706