首页 > 技术文章 > 背包详解(从01到树型,从二维到一维)

29taorz 2021-08-16 15:26 原文

背包

背包指一类在有限制的条件下选取一些物品使物品总价值最大的问题。

每个物品有其价值 v[i] 代价 w[i] ,可承受的最大代价为 W。

背包问题的解题方法大致就是考虑选与不选两种情况哪种最优。

01背包

01背包顾名思义就是每种物品自由选(1)不选(0)两种情况。

状态设置:

f[i][j]表示在前i个物品中花了j的代价所取得的最大价值。

转移方程:

f[i-1][j]表示不取第i个物品。

f[i-1][j-w[i]]+v[i]表示取第i个物品。

显然 f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i])

标程 P1048采药:

完全的模板,没什么好说的。

#include<bits/stdc++.h>
using namespace std;
int W,n,m,v[1001],w[1001],f[1001][1001];
int main()
{
    cin>>W>>n;
    for(int i=1;i<=n;i++)
        cin>>w[i]>>v[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=W;j++)
        {
            if(j<w[i])
                f[i][j]=f[i-1][j];
            else
                f[i][j]=max(f[i-1][j-w[i]]+v[i],f[i-1][j]);
        }
    }
    cout<<f[n][W];
    return 0;
}
View Code

训练题:P1507 NASA的食物计划 P1064 金明的预算方案

完全背包

大致与01背包一样,只是每件物品有无限个。

转移方程

在01背包中i号物品的情况由i-1转移而来,但在完全背包中i就由i转移。(有点不好理解看看代码就好了)

j<w[i]时

01背包  f[i][j]=0

完全背包 f[i][j]=0

j=w[i]时

01背包  f[i][j]=max(f[i-1][j],v[i])

完全背包 f[i][j]=max(f[i-1][j],v[i])

j>w[i]时

01背包  f[i][j]=max(f[i-1][j],f[i-1][j]+v[i])这使得在计算f[i][j]时最多只会加一次v[i](f[i-1][j]里没有v[i]) 。

完全背包 f[i][j]=max(f[i-1][j],f[i][j]+v[i]) 由于f[i][j]里可能已经加过v[i]了,所以计算f[i][j]时可能会加多次v[i]。

f[i-1][j]表示不取第i个物品。

f[i][j-w[i]]+v[i]表示取第i个物品。

f[i][j]=max(f[i-1][j],f[i][j-w[i]]+v[i])

标程:P1616疯狂的采药

#include<bits/stdc++.h>
using namespace std;
int W,n,m,v[1001],w[1001],f[1001][1001];
int main()
{
    cin>>W>>n;
    for(int i=1;i<=n;i++)
        cin>>w[i]>>v[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=W;j++)
        {
            if(j<w[i])
                f[i][j]=f[i-1][j];
            else
                f[i][j]=max(f[i][j-w[i]]+v[i],f[i-1][j]);
        }///                     就这里有点区别
    }
    cout<<f[n][W];
    return 0;
}
View Code

这也是一道模板题然而上面的代码只能得30分,一看数据范围 n<=1e4 m<=1e7,如开f[1e4][1e7]那空间就炸了。所以要A掉这题需要更多优化技巧。

压维优化

不难发现,以上的所有转移都是由f[i-1]到f[i]的转移,f[i-2]以及之前的被浪费掉了,所以我们可以用一维数组替代二唯数组来优化空间。

///完全背包
#include<bits/stdc++.h>
using namespace std;
long long t,m,f[10000005],w[10005],v[10005];
int main()
{
    cin>>t>>m;
    for(int i=1;i<=m;i++)
        cin>>w[i]>>v[i];
    for(int i=1;i<=m;i++)
        for(int j=w[i];j<=t;j++)///j由小往大
            f[j]=max(f[j],f[j-w[i]]+v[i]);
    cout<<f[t];
    return 0;
}
完全背包
///01背包
#include<bits/stdc++.h>
using namespace std;
long long t,m,f[10000005],w[10005],v[10005];
int main()
{
    cin>>t>>m;
    for(int i=1;i<=m;i++)
        cin>>w[i]>>v[i];
    for(int i=1;i<=m;i++)
        for(int j=1;j<=w[i];j++)///j由大往小
            f[j]=max(f[j],f[j-w[i]]+v[i]);
    cout<<f[t];
    return 0;
}
01背包

用这个技巧疯狂的采药就可以解决了。

学会了这个技巧二维的写法就可以被舍弃了,一维完全可以替代二维。

多重背包

如果每件物品有有限个呢?显然可以把m个物品拆为m个1个的物品,但这样的复杂度往往无法接受。

这里可以将m像这样的拆分:

拆分过程:

m

m-2^0 2^0

m-2^0-2^1 2^0 2^1

. . . 

直到

m再被减就<0了为止

像这样拆分后由拆分后的数字相加便可组成1~m间的所有数组,这就意味着,这些数可以等价于m个1。

如10拆成 1 2 4 3

那么10件价值v代价w的物品就等价于

1件价值v代价w的

1件价值v*2代价w*2的

1件价值v*4代价w*4的

1件价值v*3代价w*3的

这样可以大大缩小拆分后的总物品量,降低空间和时间复杂度。

标程:P1776 宝物筛选

#include<bits/stdc++.h>
using namespace std;
int n,w,v[1005],u[1005],_m[1005],_v[20005],_u[20005],f[5000005],tot,m[1005];
int main()
{
    cin>>n>>w;
    for(int i=1;i<=n;i++)
        cin>>u[i]>>v[i]>>m[i],_m[i]=m[i];
    int i,j;
    for(i=1;i<=n;i++)
    {
        for(j=0;(1<<j)<=m[i];j++)
            m[i]-=(1<<j),_u[++tot]=u[i]*(1<<j),_v[tot]=v[i]*(1<<j);
        if(m[i])
            _u[++tot]=u[i]*m[i],_v[tot]=v[i]*m[i];
    }
    for(int i=1;i<=tot;i++)
        for(int j=w;j>=_v[i];j--)
                f[j]=max(f[j-_v[i]]+_u[i],f[j]);
    cout<<f[w];
    return 0;
}
View Code

 树上背包

顾名思义,在树上做背包,在树上每个节点有若干子节点,每个子节点有价值和代价,这时就可以用树上背包。

多叉树,在每个节点都做一次分组背包。每个子树能提供多种代价和价值供选择(f[son][i]在子节点son中获得i的价值所需的代价,每个i都是一种选择方案)在一个子树上只能取一种方案。

二叉树,枚举第一个儿子的代价,算出另一个儿子的代价 f[now][tot]=max(f[lson][k]+f[rson][tot-k])。

例题 P1272重建道路

转移方程

f[now][i]=min(max){f[now][i],f[now][i-j]+f[son][j]}///这里其实压缩了一维。

#include<bits/stdc++.h>
using namespace std;
const int MM=1500;
int nxt[MM],to[MM],head[MM],f[MM][MM],d[MM],n,m,ans,tot,u,v;
void add(int u,int v)
{
    nxt[++tot]=head[u];
    to[tot]=v;
    head[u]=tot;
}
void dfs(int now,int fat)
{
    f[now][1]=d[now];
    for(int i=head[now];i;i=nxt[i])
    {
        if(to[i]==fat)
            continue;
        dfs(to[i],now);
        for(int j=m;j>=1;j--)///枚举当前节点取到的价值
            for(int k=1;k<j;k++)///枚举i对应的子节点取到的价值
                f[now][j]=min(f[now][j],f[to[i]][k]+f[now][j-k]-2);
    }
}
int main()
{
    cin>>n>>m;
    memset(f,0x3f,sizeof(f));
    for(int i=2;i<=n;i++)
        cin>>u>>v,add(v,u),add(u,v),d[u]++,d[v]++;
    dfs(1,0);
    ans=0x3f3f3f3f;
    for(int i=1;i<=n;i++)
        ans=min(ans,f[i][m]);
    cout<<ans;
    return 0;
}
View Code

例题 P1270 “访问”美术馆

#include<bits/stdc++.h>
using namespace std;
int t,u[10000],v[10000],dp[10000][1000];
int build(int now)
{
    cin>>u[now]>>v[now];
    u[now]*=2;
    if(!v[now])
        build(now*2),build(now*2+1);
}
void dfs(int now)
{
    if(v[now])
    {
        for(int i=0;i<=t;i++)
            dp[now][i]=min((i/5),v[now]);
        return;
    }
    dfs(now*2),dfs(now*2+1);
    for(int i=0;i<=t;i++)
        for(int j=0;j<=i;j++)
            dp[now][i]=max(dp[now][i],dp[now*2][max(j-u[now*2],0)]+dp[now*2+1][max(i-j-u[now*2+1],0)]);
}
int main()
{
    cin>>t;t--;
    build(1);
    dfs(1);
    cout<<dp[1][t-u[1]];
    return 0;
}
View Code

练习题 P1273 有线电视网

P1064 NOIP006 提高组] 金明的预算

推荐阅读