洛谷专题-更要技巧的动规与记忆化

P1064金明的预算方案(有依赖的背包问题)

题意:

金明有n元钱,每个物件有对应的价值与重要程度,金明希望购物物品的价值乘上重要程度和最大,请求出最大和,现在有些物品为一些其他物品的附属品,如果要买归类为附件的物品,必须先买该附件所属的主件

思路:

带有附件的背包问题,它属于01背包的变式。

即然物品分为主件和附件两类,且每个主件最多包含两个附件,那么我们不妨枚举所有的主件。那么,对于每次枚举,会有五种情况:

  • 什么都不买
  • 只买主件
  • 买主件和第一个附件
  • 买主件和第二个附件
  • 买主件和两个附件

只要把这五种情况最终的价值算出来,取最大值就可以了

#include <iostream>
#define maxn 32005
using namespace std;
int n,m;
int v,p,q;
int main_item_w[maxn];
int main_item_c[maxn];
int annex_item_w[maxn][3];
int annex_item_c[maxn][3];
int f[maxn];
int main(){
    cin >> n >> m;
    for (int i=1;i<=m;i++){
        cin >> v >> p >> q;
        if (!q){
            main_item_w[i] = v;
            main_item_c[i] = v * p;
        }
        else{
            annex_item_w[q][0]++;
            annex_item_w[q][annex_item_w[q][0]] = v;
            annex_item_c[q][annex_item_w[q][0]] = v * p;
        }
    }

    for (int i=1;i<=m;i++)
        for (int j=n;main_item_w[i]!=0 && j>=main_item_w[i];j--){
            f[j] = max(f[j],f[j-main_item_w[i]]+main_item_c[i]);

            if (j >= main_item_w[i] + annex_item_w[i][1])
                f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][1] ] + main_item_c[i] + annex_item_c[i][1]);

            if (j >= main_item_w[i] + annex_item_w[i][2])
                f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][2] ] + main_item_c[i] + annex_item_c[i][2]);

            if (j >= main_item_w[i] + annex_item_w[i][1] + annex_item_w[i][2])
                f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][1] - annex_item_w[i][2] ] + main_item_c[i] + annex_item_c[i][1] + annex_item_c[i][2]);

         }
     cout << f[n] << endl;
     return 0;
}
View Code

 P1541 乌龟棋

题意:

有四种牌,代表分别可向前跳1,2,3,4步,并且有一定的数量限制,得分为路径点的权值和,问从起点跳到终点的得分最大值是多少

思路:

一共有四张卡片,代表所能走的不同的步数,也就是当前数的取得,有可能是在前一个数的基础上,走了一步/两步/三步/四步而来,因而考虑用一个四维的状态数组f[i][j][k][h]分别表示卡片的张数,DP列出所有可能的状态即可。

要注意的细节:

第一格是唯一的起点,所以f[0][0][0][0]=a[1],也就是一张卡片都没有用的时候,取得的初值。

如果用了第一张卡片,则用f[1][0][0][0]表示,有f[1][0][0][0]=f[0][0][0][0]+a[2],也就是从第一格开始,走了一格,实际上要取的数是a[2],也就是a[t+1],其中t为卡片所规定的步数。

每张卡片只能用一次,但每一种卡片不只一张,因此在输入的时候,需要做个统计。

注意动规的初值,循环要从0开始,表示没有用到相应的卡片。

写法一:DP

#include<iostream>
#include<algorithm>
#include<cstdio>
 using namespace std;
 const int maxn=41;
 int dp[maxn][maxn][maxn][maxn],a[1000],b[5];
 int main()
 {
     int n,m,x;
     scanf("%d%d%",&n,&m);
     for(int i=1;i<=n;i++) scanf("%d",&a[i]);
     for(int i=1;i<=m;i++){
         scanf("%d",&x);
         b[x]++;
     }
    dp[0][0][0][0]=a[1];
    for(int i=0;i<=b[1];i++){
        for(int j=0;j<=b[2];j++){
            for(int k=0;k<=b[3];k++){
                for(int l=0;l<=b[4];l++){
                    int t=a[i+2*j+3*k+4*l+1];
                    if(i!=0) dp[i][j][k][l]=max(dp[i-1][j][k][l]+t,dp[i][j][k][l]);
                    if(j!=0) dp[i][j][k][l]=max(dp[i][j-1][k][l]+t,dp[i][j][k][l]);
                    if(k!=0) dp[i][j][k][l]=max(dp[i][j][k-1][l]+t,dp[i][j][k][l]);
                    if(l!=0) dp[i][j][k][l]=max(dp[i][j][k][l-1]+t,dp[i][j][k][l]);
                }
            }
        }
    }
    cout<<dp[b[1]][b[2]][b[3]][b[4]]<<endl;
 }
View Code

写法二:记忆化搜索

include<bits/stdc++.h>
using namespace std;
int f[41][41][41][41];
int n;
int s[351];
int tot[5];
inline int dfs(int a,int b,int c,int d)
{
    if(f[a][b][c][d]!=0) return f[a][b][c][d];
    if(a)f[a][b][c][d]=max(f[a][b][c][d],dfs(a-1,b,c,d));
    if(b)f[a][b][c][d]=max(f[a][b][c][d],dfs(a,b-1,c,d));
    if(c)f[a][b][c][d]=max(f[a][b][c][d],dfs(a,b,c-1,d));
    if(d)f[a][b][c][d]=max(f[a][b][c][d],dfs(a,b,c,d-1));
    f[a][b][c][d]+=s[a+2*b+3*c+4*d+1];
    return f[a][b][c][d];
}
int main()
{
    int n,m;
    cin>>n>>m;
    for(register int i=1;i<=n;i++)
    cin>>s[i];
    int q;
    for(register int i=1;i<=m;i++)
    {
        cin>>q;
        tot[q]++;
    }
    f[0][0][0][0]=s[1];
    cout<<dfs(tot[1],tot[2],tot[3],tot[4])<<endl;
    return 0;
}
View Code

 P1026统计单次个数(Hash+DP)

题意:

给你一段字符串,已经一些单次,现在让你把字符串分成k段,使得每段的单次个数和最大

思路:

定义DP[i][j]表示前i个字母分成j段得到的最大单词数,答案是DP[len][k],可以初始化一下F[i][i]和F[i][1]. 方程F(i,j)=max{ F(r,j-1)+w(r+1,i) (r=j...i-1) }. 意思就是把1..r的字母先分成j-1段,剩下的r+1..i的字母分成另一段。

先预处理出w[i][j],表示从i到j的单词数。可以倒着推,w[i][j]=w[i+1][j];(如果存在从A[i]字母开始的单词,则w[i][j]=w[i+1][j]+1.出现同一字母开头的多个单词也还是加1就够了.)

至于怎么判断一段里有多少个单次,我就选择了用哈希来处理

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
 using namespace std;
 const int ran=233;
 const int mod=1e9+7;
 int n,k,s;
 char ch[300],f[10][300];
 int ha[300],sum[300][300],dp[300][300];
 int hash(char *c)
 {
     int len=strlen(c);
     int ans=0;
     for(int i=0;i<len;i++)
         ans=(ans*ran+c[i])%mod;
     return ans;
 }
 int judge(int x,int y)
 {
     char t[300];
     for(int i=1;i<=s;i++){
         memset(t,0,sizeof(t));
         int len=strlen(f[i]);
         strncpy(t,ch+x,min(len,y-x+1));
         if(hash(t)==ha[i])return 1;
     }
    return 0;
 }
 int main()
 {
     scanf("%d%d",&n,&k);
     for(int i=1;i<=n;i++)
         scanf("%s",ch+(i-1)*20);
     scanf("%d",&s);
     for(int i=1;i<=s;i++) scanf("%s",f[i]);
     for(int i=1;i<=s;i++) ha[i]=hash(f[i]);    
     int len=strlen(ch);
     for(int i=len-1;i>=0;i--){
         for(int j=i;j>=0;j--){
             sum[j][i]=sum[j+1][i];
             if(judge(j,i)) sum[j][i]++;
         }
     }
    
    for(int i=0;i<len;i++){
        for(int j=1;j<=k;j++){
            if(j==1) dp[i][j]=sum[0][i];
            else{
                for(int l=0;l<i;l++){
                    if(dp[l][j-1])
                        dp[i][j]=max(dp[i][j],dp[l][j-1]+sum[l+1][i]);
                }
            }
        }
    }
    cout<<dp[len-1][k]<<endl;
    return 0;
 }
View Code

 P1063能量项链(区间DP)

思路:

重点就是将整体划分为区间,小区间之间合并获得大区间

状态转移方程的推导如下

一、将珠子划分为两个珠子一个区间时,这个区间的能量=左边珠子*右边珠子*右边珠子的下一个珠子

二、区间包含3个珠子,可以是左边单个珠子的区间+右边两珠子的区间,或者左边两珠子的区间右边+单个珠子的区间 。即,先合并两个珠子的区间,释放能量,加上单个珠子区间的能量(单个珠子没有能量。。) ,Energy=max(两个珠子的区间的能量+单个珠子区间的能量,单个珠子的区间的能量+两个珠子的区间的能量 )

三、继续推4个珠子的区间,5个珠子的区间。 于是可以得到方程:Energy=max(不操作的能量,左区间合并后的能量+右区间合并后的能量+两区间合并产生能量)

两区间合并后产生的能量=左区间第一个珠子*右区间第一个珠子*总区间后面的一个珠子

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
 using namespace std;
 const int maxn=205;
 struct node{
     int x,y;
 }a[maxn];
 int dp[maxn][maxn];
 int main()
 {
     int n;
     scanf("%d",& n);
     for(int i=1;i<=n;i++){
         scanf("%d",&a[i].x);
         a[i-1].y=a[i].x;
     }
    a[n].y=a[1].x;
    for(int i=1;i<n;i++) a[i+n]=a[i];
    memset(dp,0,sizeof(dp));
    for(int len=1;len<=n;len++){
        for(int i=1;i<=2*n-len+1;i++){
            for(int k=i;k<i+len;k++){
                int j=i+len;
                int t=a[i].x*a[k].y*a[j].y;
                dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+t);    
            }
        }
    }
    int ans=0;
    for(int i=1;i<=n;i++)    ans=max(ans,dp[i][i+n-1]);
    cout<<ans<<endl;
    return 0;
  } 
View Code

 P1156 垃圾陷阱(01背包,记忆化搜索)

题意:

有一只牛在井底,一些时间会从天上丢下垃圾,牛可以选择吃多,多活f秒,或者堆起来增加h高度,问牛最快什么时候能出来,出不来的话最多能活多久

思路:

①记忆化搜索:没什么好说的,搜就完事了

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<map>
#define inf 0x3f3f3f3f
const int maxn=105;
using namespace std;
struct node{
    int t,f,h;
}a[105];
int D,G,ok,max_time=0,min_time=inf;
map<int,map<int,map<int,bool> > > ed;
void dfs(int cnt,int hi,int tim)
{
    if(ed[cnt][hi][tim]) return;
    ed[cnt][hi][tim]=1;
    if(cnt>G+1) return;
    if(tim-(a[cnt].t-a[cnt-1].t)<0){
        max_time=max(max_time,a[cnt-1].t+tim);
        return;
    }
    else tim-=(a[cnt].t-a[cnt-1].t);
    if(hi+a[cnt].h>=D){
        ok=1;
        min_time=min(min_time,a[cnt].t);
    }
    dfs(cnt+1,hi+a[cnt].h,tim);
    dfs(cnt+1,hi,tim+a[cnt].f);
    return;
}
int cmp(node a,node b){return a.t<b.t;}
  int main()
  {
      scanf("%d%d",&D,&G);
      for(int i=1;i<=G;i++) scanf("%d%d%d",&a[i].t,&a[i].f,&a[i].h);
      sort(a+1,a+1+G,cmp);
      a[G+1].t=inf;
      dfs(1,0,10);
      if(ok) cout<<min_time<<endl;
      else cout<<max_time<<endl;
    return 0;  
  }
View Code

②01背包:这个题非常的像0101背包,但是又有不同之处,因为0101背包中的“不装”就不对状态做修改,这里不装则是将垃圾堆起来

当不选用这个垃圾来当垫子时:f[i+1][j]=max(f[i+1][j],f[i][j]+num[i].f)

当选用这个垃圾来当垫子时:f[i+1][j+h[i]]=max(f[i+1][j+num[i].h],f[i][j])

num[].h表示垃圾可以垫高的高度,num[].f表示吃垃圾可以维持的生命多少

对于爬不出的情况:ans=max(ans,f[i][0]) (1<=i<=n)

初始化:f[0][0]=10

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#define inf 0x3f3f3f3f 
 using namespace std;
 const int maxn=105;
 struct node{
     int t,f,h;
 }a[105];
 int dp[maxn][maxn];
 int cmp(node a,node b){return a.t<b.t;}
 int main()
 {
     int h,n;
     scanf("%d%d",&h,&n);
     for(int i=1;i<=n;i++) scanf("%d%d%d",&a[i].t,&a[i].f,&a[i].h);
     sort(a+1,a+1+n,cmp);
     dp[0][0]=10;
     for(int i=0;i<n;i++){
         for(int j=0;j<=h;j++){
             if(dp[i][j]>=a[i+1].t){
                int hight=j+a[i+1].h;
                 if(hight>=h){
                     cout<<a[i+1].t<<endl;
                     return 0;
                 }
                 dp[i+1][j]=max(dp[i+1][j],dp[i][j]+a[i+1].f);
                 dp[i+1][hight]=max(dp[i+1][hight],dp[i][j]);
             }            
         }
     }
    int ans=-inf;
    for(int i=1;i<=n;i++) ans=max(ans,dp[i][0]);
    cout<<ans<<endl;
    return 0;
 }
View Code

 P1052过河(路径压缩)

题意:

青蛙每次可以跳[S,T]步,在一些位置上有石头,问青蛙从起点跳到终点,最少要跳到多少个石头上

思路:

仔细分析题目,会发现,石头的最大值只有100个,也就是说,在一个很长的数轴上,只有很少的点,这是一个很稀疏的图, 根据离散的思维,可以把两点之间的距离大幅地缩减。

路径压缩分析

压缩的范围可以从题目中找方法,题目说青蛙每次只能跳1<=s<=t<=10的距离,所以对1-10求最小公倍数,答案是2520,也就是说,如果从a点出发,一定能跳到 a+2520 点和 a+x*2520的点

设当前是从 a 点跳到 b 点, b 点可能有石头(说明 a 点和 b 点之间,一定没有石头,怎么跳都不影响答案);

按照上面的分析:可以分情况讨论:

1 如果 a < b< a+2520,则正常操作; 

2.如果 a + 2520 < b,则让 a 先跳到 a+2520 的点上,再重复 1 的操作;

3 依次类推,如果 a + x * 2520 < b,可以看作 a 可以直接飞过中间的x个区间,到离 b 最近的一个位置,在按照1的操作执行;

根据以上的操作,可以把任意两个点之间的距离d,压缩到 2520 以内,因此 f 数组的空间就可以从原来 10^9^ 缩小到 50W以内,然后就可以正常跑DP了。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#define inf 0x3f3f3f3f
 using namespace std;
 const int maxn=1e6+10;
 const int mod=2520;
 int a[maxn],b[maxn],dp[maxn],flag[maxn];
 int main()
 {
     int l,s,t,m,n;
     cin>>l>>s>>t>>m;
     memset(flag,0,sizeof(flag));
     for(int i=1;i<=m;i++) scanf("%d",&a[i]);
     sort(a+1,a+1+m);
     for(int i=1;i<=m;i++) b[i]=(a[i]-a[i-1])%mod;
     for(int i=1;i<=m;i++){
         a[i]=a[i-1]+b[i];
         flag[a[i]]=1;
     }
    n=a[m];
    memset(dp,inf,sizeof(dp));
    dp[0]=0;
    for(int i=0;i<=n+t;i++){
        for(int j=s;j<=t;j++)
            if(i-j>=0)
                dp[i]=min(dp[i],dp[i-j]+flag[i]);
    }
    int ans=inf;
    for(int i=n;i<=n+t;i++) ans=min(dp[i],ans);
    cout<<ans<<endl;
    return 0;
 }
View Code

 

posted @ 2020-02-22 14:22  overrate_wsj  阅读(206)  评论(0编辑  收藏  举报