背包 学习笔记

背包 学习笔记

甚至到退役都没有系统地学习过这个东西,唉,草台班子SDZX。

01背包

到高中毕业也只会这一种。。

不过状态转移方程还是很好写,注意如果要滚掉一维,直接倒序枚举容量即可。

例题 P1048

    for(int i=1;i<=n;++i)
    {
        for(int w=m;w>=0;--w)
        {
            if(w-a[i]>=0)dp[w]=max(dp[w],dp[w-a[i]]+val[i]);
        }
    }

存在性背包

P1441 一点小变式,但是大概就是把01背包的转移改成了赋一个布尔值,回过头发现还没学的时候做出来一道新生赛的题竟然就是这玩意

//存在性01背包 P1441
#include<bits/stdc++.h>
using namespace std;
template<typename T>inline void re(T &x)
{
    x=0;int f=1;char c=getchar();
    while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
    while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
}
int MAXVAL;
int n,m,a[100],w[100];
bool tag[100];
int ans;
inline void dfs(int finished,int st)
{
    if(finished==m||st>n)
    {
        if(finished!=m)return ;
        int cnt=0;
        for(int i=1;i<=n;++i)if(!tag[i])w[++cnt]=a[i];
        vector<int>dp (MAXVAL+10,0);
        dp[0]=1;
        for(int i=1;i<=cnt;++i)
            for(int j=MAXVAL;j>=w[i];--j)
                if(dp[j-w[i]])dp[j]=1;
            
        ans=max(ans,accumulate(dp.begin(),dp.end(),-1));
        return ;
    }
    for(int i=st;i<=n;++i)
    {
        tag[i]=1;
        dfs(finished+1,i+1);
        tag[i]=0;
    }
}
int main()
{   
    re(n),re(m);
    for(int i=1;i<=n;++i)re(a[i]),MAXVAL+=a[i];
    dfs(0,1);
    cout<<ans;
    return 0;
}

完全背包

每个物品可以无限选,那么比较naive的想法就是再在最里面套一层枚举选取个数的循环,但奈何这种时间复杂度较高,想一想有没有更加优秀的做法。

为什么01背包不能正序枚举?因为可能会出现 \(m\) 容量时,我已经用 当前物品做了一次转移,也就是已经被用了,但是后面枚举 \(m+w_i\) 这个容量的时候,可能又会从 \(m\) 这个状态 再装一个当前物品进行转移,这样从物理意义上来说,就是用了两次同一个物品,在 01背包的意义下明显是不合法的。

但是这种不合法恰恰就正好是完全背包的要求所在啊!!!所以我们正序枚举就行。

    for(int i=1;i<=n;++i)
    {
        for(int j=w[i];j<=m;++j)
        {
            dp[j]=max(dp[j],dp[j-w[i]]+val[i]);
        }
    }

多重背包

不是无限个,可以选多次。

暴力地话就是完全背包的naive做法。复杂度是 \(O(nmk)\) 的。

    for(int i=1;i<=n;++i)
    {
        for(int j=m;j>=0;--j)
        {
            for(int cnt=0;cnt<=num[i]&&j-cnt*w[i]>=0;++cnt)
            {
                dp[j]=max(dp[j],dp[j-cnt*w[i]]+val[i]*cnt);
            }
        }
    }

二进制拆分可以减少无意义选取的次数,注意这里要求拆分的方式能够组合出所有数:复杂度 \(O(m\sum k_i)\)

    for(int i=1,tval,tw,tnum;i<=n;++i)
    {
        re(tval),re(tw),re(tnum);  
        for(int j=1;j<=tnum;j<<=1)
        {
            w[++cnt]=tw*j,val[cnt]=tval*j;
            tnum-=j;
        }
        if(tnum)w[++cnt]=tw*tnum,val[cnt]=tval*tnum;
    }
    for(int i=1;i<=cnt;++i)
    {
        for(int j=m;j>=w[i];--j)
        {
            dp[j]=max(dp[j],dp[j-w[i]]+val[i]);
        }
    }

还有一种比较优秀的单调队列优化,但是今晚害得做一做 cf ,挖个坑。
然而昨天晚上还是没有做 : (

//TODO

例题 P1776

分组背包

每组里面只能选取一件,直接枚举组数->枚举容量->枚举每个子物品就可以了。

    for(int i=1;i<=t;++i)
    {
        for(int j=m;j>=0;--j)
        {
            for(int k=1;k<=a[i].cnt;++k)
            {
                if(j-a[i].w[k]>=0)dp[j]=max(dp[j],dp[j-a[i].w[k]]+a[i].val[k]);
            }
        }
    }

费用背包

多了一维费用,实际上就是两维的背包。

    for(int i=1;i<=n;++i)
    {
        cin>>w[i]>>t[i];
        for(int j=m;j>=w[i];--j)
        {
            for(int k=T;k>=t[i];--k)
            {
                dp[j][k]=max(dp[j][k],dp[j-w[i]][k-t[i]]+1);
            }
        }
    }

例题 1855

依赖背包

言下之意就是存在某些物品 \(B\) ,如果要选它 ,那么必须先选另外一个指定的物品 \(A\)

发现这样的话无非就要么只买主件,要么就同时买其中若干个附件,但是由于这些情况又不能够同时成立,所以就转化成了一个分组背包的模型。

例题 P 1064

#include<bits/stdc++.h>
using namespace std;
const int N=4e4+10;
int m,n;
int weight[N],val[N],son[N][4],cnt[N],q[N];
int num=0;
struct group
{
    int w[10],v[10];
    int num;
    void display()
    {
        for(int i=1;i<=num;++i)
        {printf("%d %d\n",w[i],v[i]);}
    }
}a[N];
inline void pre()
{
    cin>>m>>n;
    for(int i=1;i<=n;++i)
    {
        cin>>weight[i]>>val[i];
        val[i]*=weight[i];
        cin>>q[i];
        if(q[i]==0)continue;
        son[q[i]][++cnt[q[i]]]=i;
    }
    //0的cnt可能大于2了,这样就会访问到别的内存!!
    for(int i=1;i<=n;++i)
    {
        if(q[i])continue;
        num++;
        a[num].num++;
        a[num].w[a[num].num]=weight[i],a[num].v[a[num].num]=val[i];
        if(cnt[i]>=1)
        {
            a[num].num++;
            a[num].w[a[num].num]=weight[i]+weight[son[i][1]],a[num].v[a[num].num]=val[i]+val[son[i][1]];
        }
        if(cnt[i]==2)
        {
            a[num].num++;
            a[num].w[a[num].num]=weight[i]+weight[son[i][2]],a[num].v[a[num].num]=val[i]+val[son[i][2]];
            a[num].num++;
            a[num].w[a[num].num]=weight[i]+weight[son[i][1]]+weight[son[i][2]],a[num].v[a[num].num]=val[i]+val[son[i][1]]+val[son[i][2]];
        }
    }
}
int main()
{
    pre();
    vector<int>dp(m+1,-1);
    dp[0]=0;
    for(int i=1;i<=num;++i)
    {
        for(int j=m;j>=0;--j)
        {
            for(int idx=1;idx<=a[i].num;++idx)
            {
                if(j-a[i].w[idx]>=0)dp[j]=max(dp[j],dp[j-a[i].w[idx]]+a[i].v[idx]);
            }
        }
    }
    int ans=0;
    for(int i=0;i<=m;++i)ans=max(ans,dp[i]);
    cout<<ans;
    return 0;
}

树形背包

顾名思义就是在数上面跑一个背包,以每一个节点为根节点,其子树都是一个独立的背包问题。
然后这个子树算出来的 dp 值作为新的物品价值 来当做这个节点的父节点的待选物,这样对于父节点来说又是一个新的dp问题
例题 P2014 选课

//P2014选课
#include<bits/stdc++.h>
using namespace std;
inline void ckmax(int &x,int y){if(x<y)x=y;}
const int N=400;
int n,m;
int dp[N][N],s[N];
vector<int> e[N];
inline void pre()
{
    cin>>n>>m;
    for(int i=1,fa;i<=n;++i)
    {
        cin>>fa>>s[i];
        e[fa].push_back(i);
    }
}
inline void dfs(int x)
{
    for(auto v:e[x])
        dfs(v);
    for(auto v:e[x])
    {
        for(int j=m;j>=1;--j)
        {
            for(int k=0;k<=j-1;++k)
            {
                ckmax(dp[x][j],dp[x][j-(k+1)]+dp[v][k]+s[v]);
            }
        }
    }
}
int main()
{
    pre();
    dfs(0);
    int ans=0;
    for(int i=0;i<=m;++i)ckmax(ans,dp[0][i]);
    cout<<ans;
    return 0;
}

例题 们

BJTU2038

1.分组背包,不一定要用结构体把所有情况构造出来,这启示我们可以直接从状态压缩的角度枚举二进制数来进行考虑
2.但是容量的转移并不是传统的加减,因为回合结束还有一个容量自动增加,所以还有一个增量的过程,要额外做一个映射再进行转移。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n;
const ll INF=1e15;
ll val[10],w[10];
pair<ll,ll> sum(int sta)
{
    ll ansval=0,answ=0;
    int cnt=1;
    while(sta)
    {   
        ansval+=(sta&1ll)*val[cnt];
        answ+=(sta&1ll)*w[cnt];
        sta>>=1;
        cnt++;
    }
    return {ansval,answ};
}
int las[1000];
//dp[j] 剩下j个金币的方案数
int main()
{
    cin>>n;
    vector< vector<ll> > dp(20000,vector<ll>(100,-INF));
    for(int i=1;i<=61;++i)las[i+i/10+2]=i;
    for(int i=0;i<=70;++i)if(!las[i])las[i]=999;
    //非常重要的一个地方,因为有些状态注定根本就没有前继状态,是不合法的,但是转移的时候会从0转移,但是0又是一个合法状态
    for(int j=1;j<=5;++j)cin>>val[j];
    for(int j=1;j<=5;++j)cin>>w[j];
    for(int j=24;j>=0;--j)
    {
        for(int sta=0;sta<32;++sta)
        {
            pair<ll,ll> p=sum(sta);
            ll nowval=p.first,noww=p.second;
            if(j+noww!=24)continue;
            dp[1][j]=max(dp[1][j],nowval);
        }
    }
    for(int i=2;i<=n;++i)
    {
        for(int j=1;j<=5;++j)cin>>val[j];
        for(int j=1;j<=5;++j)cin>>w[j];
        for(int j=70;j>=0;--j)
        {
            for(int sta=0;sta<32;++sta)
            {
                pair<ll,ll> p=sum(sta);
                ll nowval=p.first,noww=p.second;
		        if(j+noww>70)continue;
                if(j+noww<=69)
                	dp[i][j]=max(dp[i][j],dp[i-1][las[j+noww]]+nowval);
                else
                	for(
                        #include<bits/stdc++.h>
#define int long long
using namespace std;
int n,x,k;
const int N=1e4+10;
struct item
{
    int w,v,c;
    const bool operator<(item tmp){return c<tmp.c;}
}a[N];
//\sum w+ types*k
const int INF=1e9;
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>x>>k;
    for(int i=1;i<=n;++i)
    {
        cin>>a[i].w>>a[i].v>>a[i].c;
    }
    sort(a+1,a+n+1);
    // for(int i=1;i<=n;++i)cout<<a[i].w<<' '<<a[i].v<<' '<<a[i].c<<'\n';
    a[0].c=0;
    vector<vector<vector<int>>> dp(2,vector<vector<int>>(x+10,vector<int>(2,-INF)));
    int p=0;
    dp[0][0][0]=0;
    for(int i=1;i<=n;++i)
    {
        p^=1;
        for(int j=0;j<=x;++j)
        {
            if(a[i].c!=a[i-1].c)
            {
                if(j-a[i].w>=0)dp[p][j][1]=max(dp[p^1][j-a[i].w][0],dp[p^1][j-a[i].w][1])+a[i].v+k;
                dp[p][j][0]=max(dp[p^1][j][0],dp[p^1][j][1]);
            }
            else 
            {
                if(j-a[i].w>=0)dp[p][j][1]=max(dp[p^1][j-a[i].w][0]+a[i].v+k,dp[p^1][j-a[i].w][1]+a[i].v);
                dp[p][j][1]=max(dp[p][j][1],dp[p^1][j][1]);
                dp[p][j][0]=dp[p^1][j][0];
            }
            // cout<<"DP"<<i<<","<<j<<":"<<dp[i][j][0]<<' '<<dp[i][j][1]<<'\n';
        }
    }
    int ans=0;
    for(int i=0;i<=x;++i)ans=max(ans,max(dp[p][i][0],dp[p][i][1]));
    cout<<ans;
    return 0;
}int t=62;t<=70;++t)dp[i][j]=ma```c
                    ```x(dp[i][j],dp[i-1][t]+nowval);//62-70

            }
        }
    }
    ll ans=0;
    for(int i=0;i<=70;++i)ans=max(ans,dp[n][i]);
    cout<<ans;
    return 0;
}

ABC383F

很明显的一个背包问题,但是带有限制,也就是所谓的“颜色”。
由于物品的考虑顺序并不会影响最终的答案,所以可以把所有物品都按照颜色排序,这样就保证了 同一种颜色都相邻
这时候定义 \(dp[i][j][0/1]\) 为前 \(i\) 个达到 \(j\) 容量,当前这个颜色选过没有。
注意这个时候转移可能有些不同,比如 \(dp[i][j][1]\) 实际上可以是当前这个不选,之前选了一个的情况,所以转移方程可能有点不同。
然后不能开三维,但是直接kick一维似乎也不太行??那就把第一维滚掉吧。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,x,k;
const int N=1e4+10;
struct item
{
    int w,v,c;
    const bool operator<(item tmp){return c<tmp.c;}
}a[N];
//\sum w+ types*k
const int INF=1e9;
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>x>>k;
    for(int i=1;i<=n;++i)
    {
        cin>>a[i].w>>a[i].v>>a[i].c;
    }
    sort(a+1,a+n+1);
    // for(int i=1;i<=n;++i)cout<<a[i].w<<' '<<a[i].v<<' '<<a[i].c<<'\n';
    a[0].c=0;
    vector<vector<vector<int>>> dp(2,vector<vector<int>>(x+10,vector<int>(2,-INF)));
    int p=0;
    dp[0][0][0]=0;
    for(int i=1;i<=n;++i)
    {
        p^=1;
        for(int j=0;j<=x;++j)
        {
            if(a[i].c!=a[i-1].c)
            {
                if(j-a[i].w>=0)dp[p][j][1]=max(dp[p^1][j-a[i].w][0],dp[p^1][j-a[i].w][1])+a[i].v+k;
                dp[p][j][0]=max(dp[p^1][j][0],dp[p^1][j][1]);
            }
            else 
            {
                if(j-a[i].w>=0)dp[p][j][1]=max(dp[p^1][j-a[i].w][0]+a[i].v+k,dp[p^1][j-a[i].w][1]+a[i].v);
                dp[p][j][1]=max(dp[p][j][1],dp[p^1][j][1]);//这个地方容易考虑漏
                dp[p][j][0]=dp[p^1][j][0];
            }
            // cout<<"DP"<<i<<","<<j<<":"<<dp[i][j][0]<<' '<<dp[i][j][1]<<'\n';
        }
    }
    int ans=0;
    for(int i=0;i<=x;++i)ans=max(ans,max(dp[p][i][0],dp[p][i][1]));
    cout<<ans;
    return 0;
}
posted @ 2024-11-21 21:04  Hanggoash  阅读(4)  评论(0编辑  收藏  举报
动态线条
动态线条end