动态规划学习 2(背包问题及其变形)

背包问题的更深层次的动态规划,有各种变形,建议配合动态规划dp那一章先了解背包再食用

这节我估计要学习15个学时左右,还是挺重要的

day 01:

//动态规划学习记录二----背包问题
//0-1背包:只有一个物品,只能选择选或不选;
//多重背包:每个物品有次数限制;
//完全背包:可以选无限次,也是求最大价值,可以使用2的n次方求解
//具体问题看动态规划dp环节,有详细代码
//采药:https://www.luogu.com.cn/problem/P1048
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,t,w[N],v[N],res,f[N],m;
int main()
{
    cin>>t>>m;
    for(int i=0;i<m;i++) cin>>w[i]>>v[i];
    for(int i=0;i<m;i++)
        for(int j=t;j>=w[i];j--)
            f[j]=max(f[j],f[j-w[i]]+v[i]);
    cout<<f[t];
    return 0;
}
//装箱问题:https://www.luogu.com.cn/problem/P1049
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,v[N]={1},w[N],res,f[N];
int main()
{
    cin>>m>>n;
    for(int i=0;i<n;i++) cin>>w[i];
    for(int i=0;i<n;i++)
        for(int j=m;j>=w[i];j--)
            f[j]=max(f[j],f[j-w[i]]+w[i]);
    cout<<m-f[m];
    return 0;
}

 //从现在开始是多维背包问题 

//宠物小精灵之收服:https://oj.ytu.edu.cn/problem.php?cid=4219&pid=8
//该题类似于精卫填海,建议先食用精卫填海再食用这个:https://www.luogu.com.cn/problem/P1510
#include<bits/stdc++.h>
using namespace std;
const int N=2020;
int m,t,n,f[N][N],w[N],v[N],res;//f要用二维数组来存.分别是精灵球和体力值,算是0-2背包的样子?
int main()
{
    cin>>t>>m>>n;
    for(int i=0;i<n;i++) cin>>w[i]>>v[i];
    for(int i=0;i<n;i++)//这个点就充当一个输送数据的作用
        for(int j=t;j>=w[i];j--)
            for(int k=m;k>=v[i];k--) f[j][k]=max(f[j][k],f[j-w[i]][k-v[i]]+1);//0-1?0-2!
    cout<<f[t][m]<<' ';//这个就是最多的精灵;
    int x=m;//拷贝,x是血量
    while(x>0&&f[t][x-1]==f[t][m]) x--;//当我们的血量大于0并且精灵球等于最大精灵球时,我们的血量要减一下;
    //直到血量为0或者精灵球的数量小于最大精灵量的时候再停止循环,此时的总价值一定是最小的;
    //为什么时f t x-1 ,因为我们要看少一滴血的情况下精灵数是不是还是最大的,所以这里要x-1而不是x

    //为什么要这样统计呢?因为在那个循环里面我们已经将所有的状态全部算出来了,所以说中间是没有空余1状态量的
    //故此时可以用x--来统计
    cout<<m-x; //x可以减少的血量,也就是我们剩余的血量,m-x是被攻击的血量
    return 0;
}
//潜水员:http://ybt.ssoier.cn:8088/problem_show.php?pid=1271
//本题具有思维性,好好思考
//既然要求最小值,那么必然的我们的f数组要全部设为无穷
//不妨思考一下f数组的含义,在以往的题中,f数组代表什么?
//f代表 第i个背包,第j个氧气,第k个氮气的最小重量
//但是你仔细看看你题目要求,你会发现这是一种错误的的状态表示,正确的表示是第i个背包,至少为j个氧气,至少为k个氮气的最小重量

//状态计算:
//不选第 i 个物品:f[i-1,j,k]
//选第 i 个物品:f[i-1,j-vi,k-mi]+wi
//初始化:f[0,0,0] = 0,f[0,j,k]=INF 初始一个都不选,那么氧气含量和氮气含量就是 0,且总重量也是 0。其它由于需要求最小值,所有全部初始化为 INF
//以上两个状态转移和初始化方式,适用于上面两个状态表示…那么肯定是有问题的
//在有问题的状态表示中,状态转移时,f[i-1,j-vi,k-mi]+wi 需要保证 j>=vi,k>=mi,因为不能为负数。体积恰好是负数时的状态是不成立的
//在正确的状态表示中,状态表示的是体积至少是 j,那针对于当前物品的体积大于当前枚举的 j 的时候,该物品仍是可以使用的。
//例如,当前状态 f[2][3] 代表氧气含量至少是 2, 氮气含量至少是 3 的最小气缸重量,同时满足这两个条件才能发生状态转移。
//若现在有一个物品,氧气含量为 3,氮气含量为 2,那么它至少需要 3 的氧气容量, 2 个氮气容量,它也是一种合法状态,只不过现在是多占了一个氧气体积没有使用罢了.
//但是它在该状态定义下确确实实是一种合法状态。所以,如果用该物品的话,状态转移方程为:f[0][1]+w,表示氧气不再需要,再填补至少一个单位的氮气进来就行了。
//故,当氧气或者氮气枚举其体积一方小于 0 的时候,将该状态转移到 0 上去就行了。指其已经不需要再添进来该种气体了

//状态转移方程: f[i,j,k]=min(f[i-1,j,k],f[i-1,max(0,j-vi),max(0,k-mi)]
//优化到二维: f[j][k] = min(f[j][k], f[max(0, j - a)][max(0, k - b)] + w);

//答案:
//氧气含量至少为 n 氮气含量至少为 m 的最小气缸重量 f[n][m]
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int n,m,t,f[N][N],w[N],v[N],res=0x3f3f3f,p[N];
int main()
{
    memset(f,0x3f,sizeof f);
    f[0][0]=0;//f[0][0]代表从0个气缸里选,氧气需求为0,氮气需求为0的时候,所携带的气缸的最小重量,很明显是0;
    cin>>m>>t>>n;
    for(int i=0;i<n;i++) cin>>w[i]>>v[i]>>p[i];
    for(int i=0;i<n;i++)
        for(int j=m;j>=0;j--)//为什么枚举到0?
        //不要忘记f数组表示的是什么,f数组表示的是氧气至少为j,氮气至少为k的时候,所以说
        //当我们氧气至少为j的时候,如果j-w[i]<0,那是不是意味着这个罐子的氧气比所需的大,是不是合法的?
        //因为我们是枚举至少所需要的体积,从至少需要0体积一步一步的递增到至少需要m体积
        //所以说我们枚举的时候,不像普通的背包问题那样,背包的容量必须大于物体的体积

        //可以举个更清楚的例子,我们至少需要0个氧气(因为已经有罐子给氧气填满了),1个氮气(那些罐子的氮气少,氧气可能多)
        //那我们的状态是不是这个? -> f[0][1]+w?这就很明了了
            for(int k=t;k>=0;k--) f[j][k]=min(f[j][k],f[max(0,j-w[i])][max(0,k-v[i])]+p[i]);
    cout<<f[m][t];
    return 0;
}
//榨取kkksc03:https://www.luogu.com.cn/problem/P1855
#include<bits/stdc++.h>
using namespace std;
const int N=2020;
int n,m,t,f[N][N],w[N],v[N];
int main()
{
    cin>>n>>m>>t;
    for(int i=0;i<n;i++) cin>>w[i]>>v[i];
    for(int i=0;i<n;i++)
        for(int j=m;j>=w[i];j--)
            for(int k=t;k>=v[i];k--) f[j][k]=max(f[j][k],f[j-w[i]][k-v[i]]+1);
    cout<<f[m][t];
    return 0;
}
//NASA的食物计划:https://www.luogu.com.cn/problem/P1507
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int h,t,n,w[N],v[N],p[N],f[N][N];
int main()
{
    cin>>h>>t>>n;
    for(int i=0;i<n;i++) cin>>w[i]>>v[i]>>p[i];
    for(int i=0;i<n;i++)
        for(int j=h;j>=h[i];j--)
            for(int k=t;k>=v[i];k--) f[j][k]=max(f[j][k],f[j-w[i]][k-v[i]]+p[i]);
    cout<<f[h][t];
    return 0;
}

 day 02:

 1 //利用背包问题求解方案数: 

 

//数字组合:https://www.acwing.com/problem/content/280/
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
int n,m,res,a[N],f[N];
int main()
{
    f[0]=1;//初始化
    //这里用二维数组来解释:f[i][j]表示第i个物品,此时的值是j
    //f[0][0]就是一个数字都不选,那自然是0,所以这算是一种方案,f[0][0]=1,f[0]=1;
    //同理,f[0][1],f[0][2]...f[0][n]=0;因为我不选数字,我的值永远都是0,不会增加,所以为0
    cin>>n>>m;
    for(int i=0;i<n;i++) cin>>a[i];
    for(int i=0;i<n;i++)
        for(int j=m;j>=a[i];j--) f[j]+=f[j-a[i]];//这里分成两步
    //j此时的方案是不包含a[i]和包含a[i]的,不包含a[i]的不就是f[j],包含的不就是f[j-a[i]]嘛;
    //别忘了这是求方案数,所以j的方案是这两个数量相加,因为这两个都符合此时价值等于j
    cout<<f[m];
    return 0;
}

 

//买书:http://ybt.ssoier.cn:8088/problem_show.php?pid=1293
//完全背包模板
#include<bits/stdc++.h>
using namespace std;
const int N=6020;
int n,m,t,a[4]={10,20,50,100},f[N];
int main()
{
    f[0]=1;
    for(int i=0;i<4;i++)
        for(int j=a[i];j<=1010;j++) f[j]+=f[j-a[i]];
    cin>>n;
    cout<<f[n];
    return 0;
}
//找零钱:https://oj.ytu.edu.cn/problem.php?cid=4209&pid=9
//对曾经的我来说是个困难题哈哈哈,不过现在好像没那么吃力了
//这个和买书一样的类型,也是完全背包求出来所有的情况然后依次输入输出
//这里要注意,有票数限制,所以我们要用二维数组来储存
//f[i][j]是i元用了j张票子,然后呢,由于i元的情况可能有很多种对应的j情况
//所以我们要开一个数组把所有的情况全部加起来,ans[i]就是i元中,从0到j的所有情况的总和
#include<bits/stdc++.h>
using namespace std;
const int N=6020;
int n,m,t,a[7]={1,2,5,10,20,50,100},f[N][N],ans[N];
int main()
{
    f[0][0]=1;
    for(int i=0;i<7;i++)
        for(int j=a[i];j<=255;j++)
            for(int k=1;k<=100;k++)
                if(j>=a[i]) f[j][k]+=f[j-a[i]][k-1];
    for(int i=1;i<=255;i++)
        for(int j=0;j<=100;j++) ans[i]+=f[i][j];
    while(cin>>n&&n) cout<<ans[n]<<endl;
    return 0;
}

 

//背包问题求具体方案
#include<bits/stdc++.h>
using namespace std;
const int N=2020;
int n,m,w[N],v[N],res,f[N][N];
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>w[i]>>v[i];
    for(int i=n;i>=1;i--)//既然要求按照最小字典序输出放案,那就使用和搜索一样的讨论,贪心策略
    //每一步都选择最小编号的背包就可以,那么可以知道,我们第i个背包的状态是由第i+1个来决定的
    //所以这里要从前往后推,逆序,这样最终f[1][m]就是表示的是前n件物品体积为m下的最大价值
        for(int j=0;j<=m;j++)
        {
            f[i][j]=f[i+1][j];
            if(j>=w[i]) f[i][j]=max(f[i][j],f[i+1][j-w[i]]+v[i]);
        }
    //f[1][m]是前n件物品体积为m下的最大价值
    res=m;
    for(int i=1;i<=n;i++)
        if(res>=w[i]&&f[i][res]==f[i+1][res-w[i]]+v[i]) cout<<i<<" ",res-=w[i];
    return 0;
}

 

// //分组背包问题:https://www.acwing.com/problem/content/submission/code_detail/25898617/
#include<bits/stdc++.h>
using namespace std;
const int N=2020;
int n,m,res,f[N],w[N][N],v[N][N],s[N];
int main()
{
    cin>>n>>m;
    for(int i=0;i<n;i++)
    {
        cin>>s[i];//输入每个组有多少背包数
        for(int j=0;j<s[i];j++) cin>>w[i][j]>>v[i][j];//记录背包
    }
    for(int i=0;i<n;i++)
        for(int j=m;j>=0;j--)
            for(int k=0;k<s[i];k++)//枚举这个组的所有背包,注意,这里可以不选择,所以k可以为0
                if(j>=w[i][k]) f[j]=max(f[j],f[j-w[i][k]]+v[i][k]);
    cout<<f[m];
    return 0;
}
//机器分配:https://www.luogu.com.cn/problem/P2066
//因为要输出状态,所以我们这里用二维,注意字典序最小,逆着推
//将每个公司看成一个组,那就是分组背包的输出问题了,每个组的背包数都是m,并且是从1到2到3到...m
#include<bits/stdc++.h>
using namespace std;
const int N=4020;
int n,m,a[N],f[N][N],s[N],w[N][N],v[N][N],tmp,res,x[N];
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++) cin>>v[i][j],w[i][j]=j;
    for(int i=n;i>=1;i--)
        for(int j=m;j>=0;j--)
            for(int k=0;k<=j;k++)
                f[i][j]=max(f[i][j],f[i+1][j-w[i][k]]+v[i][k]);
    cout<<f[1][m]<<endl;
    int j=m;
    for(int i=1;i<=n;i++)
        for(int k=0;k<=j;k++)
            if(f[i][j]==f[i+1][j-w[i][k]]+v[i][k]) {x[i]=w[i][k],j-=w[i][k];break;}
    for(int i=1;i<=n;i++) cout<<i<<" "<<x[i]<<endl;
    return 0;
}
//货币系统:http://ybt.ssoier.cn:8088/problem_show.php?pid=1273
//完全背包而已
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
long long n,a[N],f[N],m;
int main()
{
    cin>>n>>m;
    f[0]=1;
    for(int i=0;i<n;i++) cin>>a[i];
    for(int i=0;i<n;i++)
        for(int j=a[i];j<=m;j++) f[j]+=f[j-a[i]];
    cout<<f[m];
    return 0;
}
//[NOIP2018 提高组] 货币系统:https://www.luogu.com.cn/problem/P5020
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int f[N],a[N],n,t,m;
int main()
{
    cin>>t;
    while(t--)
    {
        memset(f,0,sizeof f);
        cin>>n;
        f[0]=1;
        for(int i=0;i<n;i++) cin>>a[i];
        sort(a,a+n);//贪心部分,一定要从最小的开始遍历,不然的话先遍历大的小的就遍历不到了
        for(int i=0;i<n;i++)
            for(int j=a[i];j<=25010;j++)//所有的情况
                f[j]+=f[j-a[i]];
        int res=n;
        for(int i=0;i<n;i++) if(f[a[i]]>1) res--;
        cout<<res<<endl;
    }
    return 0;
} 

day03:(终于算是基本上学完了)

先用oj的一道背包题试试手:

 

//动态规划进阶题目之货币面值:https://oj.ytu.edu.cn/problem.php?cid=4227&pid=0
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,a[N],f[N],res;
int main()
{
    while(cin>>n)
    {
        memset(f,0,sizeof f);
        for(int i=0;i<n;i++) cin>>a[i];
        sort(a,a+n);
        f[0]=1;
        for(int i=0;i<n;i++)
            for(int j=N;j>=a[i];j--) f[j]+=f[j-a[i]];
        for(int i=0;i<N;i++) if(f[i]==0) {cout<<i<<endl;break;}
    }
    return 0;
}

  //依赖背包(树上背包) 

//依赖背包:https://www.acwing.com/problem/content/10/
//每个节点都要选择一个,从父节点开始的话,两边只能选一个
//可以这样理解,也就是第一个父节点有两个子节点,所以说给父节点选的背包数就两个
//如此,这种类型的就可以转换为分组背包问题,关于父节点问题可以用邻接表来储存
#include<bits/stdc++.h>
using namespace std;
const int N=2020;
int f[N][N],a[N],w[N],v[N],h[N],e[N],ne[N],idx,n,root,m;
void add(int a,int b)//add函数储存邻接表
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u)//u是节点
{
    for(int i=h[u];~i;i=ne[i])//遍历父节点的每一个子节点
    {
        int son=e[i];
        dfs(e[i]);//这里与递归顺序相反,我们要先把父节点的子节点全部的状态寻找完,然后
        //最后再加上父节点才是最大价值  因为root节点是必须要选的,所以背包得先空出v[root]来装root节点
        for(int j=m-w[u];j>=0;j--) //j是体积,由于一开始不加入父节点,所以是m-w[u]开始,枚举体积
        //对于节点son来说
        //这个节点在不同的体积下(每种体积都有选与不选两种情况)有不同的最优解,在root体积为j的情况下,循环测试这些体积
        //最终算出root体积为j的最优解
            for(int k=0;k<=j;k++) f[u][j]=max(f[u][j],f[u][j-k]+f[son][k]);
        //枚举组数,由于不清楚具体的节点数,所以我们的决策就是,遍历所有小于j的背包数,然后循环递增
    }
    for(int i=m;i>=w[u];i--) f[u][i]=f[u][i-w[u]]+v[u];//加上父节点的体积
    for(int i=0;i<w[u];i++) f[u][i]=0;//如果我们装不下父节点,那么就没意义,全部变成0
}
int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);
    for(int i=1;i<=n;i++)
    {
        int p;
        cin>>w[i]>>v[i]>>p;
        if(p==-1) root=i;//p是我们的节点,如果此时=-1的话就是父节点,其余的都要接在父节点上面
        else add(p,i);//
    }
    dfs(root);//从父节点开始遍历;
    cout<<f[root][m];//输出从父结点开始,体积不小于m的最大价值
    return 0;
}
//[CTSC1997] 选课:https://www.luogu.com.cn/problem/P2014
//本题略有变形,多加思考
#include<bits/stdc++.h>
using namespace std;
const int N=2020;
int f[N][N],w[N]={1},v[N],h[N],e[N],ne[N],idx,res,n,m,root;
void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u)
{
    for(int i=h[u];~i;i=ne[i])
    {
        int son=e[i];
        dfs(son);
        for(int j=m;j>=0;j--)//这里为什么要从m开始呢?
    //如标题所示,这个是树状dp的问题,但是按照题意来的话,画完图之后很明显是森林
    //这个时候我们就需要多建立一个0节点来使他们形成树
            for(int k=0;k<=j;k++) f[u][j]=max(f[u][j],f[u][j-k]+f[son][k]);
    }
    for(int i=m+1;i>=1;i--) f[u][i]=f[u][i-1]+v[u];
    f[u][0]=0;
}
int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);
    for(int i=0;i<=m+1;i++) f[0][i]=0;
    for(int i=1;i<=n;i++)
    {
        int p;
        cin>>p>>v[i];
        //f[i][1]=v[i];
        add(p,i);
    }
    dfs(0);
    cout<<f[0][m+1];//这里的m+1就是因为多建了个节点,0节点是必须选的,但是呢0节点的价值是0;
    return 0;
}

 

//https://www.luogu.com.cn/problem/P1833
//混合背包问题 
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10,M=2040;
int hh,tt,mm,n;
struct node
{
    int t,c,p;
}tr[N];
string t1,t2;
int dp[M];
int main()
{
    cin>>t1>>t2>>n;
    int hh1=0,hh2=0,mm1=0,mm2=0;
    bool f=false,ff=false;;
    for(int i=0;i<t1.size();i++){
        if(t1[i]==':') f=true;
        else if(!f) hh1=hh1*10+t1[i]-'0';
        else if(f) mm1=mm1*10+t1[i]-'0';
    }
    for(int i=0;i<t2.size();i++){
        if(t2[i]==':') ff=true;
        else if(!ff) hh2=hh2*10+t2[i]-'0';
        else if(ff) mm2=mm2*10+t2[i]-'0';
    }
    int pos=(hh2-hh1)*60+mm2-mm1;
    for(int i=1;i<=n;i++){
        int a,b,c; cin>>a>>b>>c;
        tr[i].t=a,tr[i].c=b,tr[i].p=c;
    }
    for(int i=1;i<=n;i++)
        if(tr[i].p==0) for(int j=tr[i].t;j<=pos;j++) dp[j]=max(dp[j],dp[j-tr[i].t]+tr[i].c);
        else
            for(int k=1;k<=tr[i].p;k++)
                for(int j=pos;j>=k*tr[i].t;j--) //这里存在一个优化,即枚举的次数 
                    dp[j]=max(dp[j],dp[j-tr[i].t]+tr[i].c);
    cout<<dp[pos];
    return 0;    
}

//https://www.luogu.com.cn/problem/P2340

//每个物品只有选或者不选两种状态,若要搜索 2^400,太大
//基本上存在: 选或不选的首先考虑背包dp,然后得出取一次就没,即01背包
//由于题目存在负数,要把-400000-400000整体向右移,0-800000;
//然后定义dpi, i是智商数,dpi是情商数,已经保证了智商数大于0,dp情商即可 
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,m,res;
struct node
{
    int l,r;
}cow[N];
int dp[N];
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>cow[i].l>>cow[i].r;
    memset(dp,-0x3f,sizeof dp);
    dp[400000]=0;
    for(int i=1;i<=n;i++){
        if(cow[i].l>=0) //对于大于0的,反向dp推状态 
            for(int j=800000;j>=cow[i].l;j--) dp[j]=max(dp[j],dp[j-cow[i].l]+cow[i].r);
        else //对于小于0的,正向推,推的是上一个用它的背包 
            for(int j=0;j<=800000+cow[i].l;j++) dp[j]=max(dp[j],dp[j-cow[i].l]+cow[i].r);
    }
    for(int i=400000;i<=800000;i++){
        if(dp[i]>0) res=max(res,i+dp[i]-400000);
    }
    cout<<res;
    return 0;
}

 

posted @ 2023-06-08 22:57  o-Sakurajimamai-o  阅读(198)  评论(0)    收藏  举报
-- --