背包问题

OI复建,自娱产物

1.01背包

dp的要素:最优子结构、边界、状态转移方程。

01背包的二维状态转移方程:f [ i ] [ j ] = max ( f [ i - 1 ] [ j ] , f [ i - 1 ] [ j - c [ i ] ] + w [ i ] )

v可以从c[ i ] 开始正向循环(i-1保证了不会重复)

 

再解释一下状态转移方程:

对于第 n 件物品,实现最优解时,只有选与不选第 n 件物品两种方案。

如果不选,f [ n ] [ v ] = f [ n - 1 ] [ v ]

如果选,f [ n ] [ v ] = f [ n - 1 ] [ v - cn ] + wn,要让这个方案最优,显然需要 f [ n - 1 ] [ v - cn ] 最优

 

空间优化:

f [ j ] = max ( f [ j ] + f [ j - c [ i ] ] + w [ i ] )

为了避免一件物品被计入多次,空间从后向前更新

 

要求背包必须装满的初始化:将 f 初始化为 - ∞ ,f [ 0 ] = 0

整个背包装满,要求每个子背包必须装满

#include<bits/stdc++.h>//01
#define N 30001
using namespace std;
int m,n,w[N],c[N],f[N];
int main(){
    scanf("%d%d",&m,&n);
    for(int i=1;i<=n;i++)
        scanf("%d%d",&c[i],&w[i]);
    for(int i=1;i<=n;i++)
        for(int j=m;j>0;j--)
            if(j>=c[i]) f[j]=max(f[j],f[j-c[i]]+w[i]);
    printf("%d",f[m]);
}

2.完全背包

循环空间时改为正向循环即可。

洛谷似乎没有模板题?

洛谷P1616:

#include<bits/stdc++.h>
#define N 10001
#define M 10000001
using namespace std;
long long n,t,c[N],w[N];
long long f[M];
int main(){
    memset(f,0,sizeof(f));
    scanf("%ld%ld",&t,&n);
    for(int i=1;i<=n;i++)
        scanf("%ld%ld",&c[i],&w[i]);
    for(int i=1;i<=n;i++)
        for(int j=c[i];j<=t;j++)
            f[j]=max(f[j],f[j-c[i]]+w[i]);
    printf("%ld",f[t]);
}

优化:

显然,如果 c [ i ] > c [ j ] 且 w [ i ] < w [ j ],j 物品永远优于 i 物品。( 注意只在物品可无限次选取时适用,也就是说01和多重背包不能这样优化)

可以有效降低随机数据的时间复杂度

(背包九讲上说可以用类似计数排序的方式写,但是我不会计数排序,代码暂略)

3.多重背包

两种思路:

1.转化为01背包问题

#include<bits/stdc++.h>
#define N 101
using namespace std;
int n,v,c[N],w[N],s[N],f[N];
int main(){
    memset(f,0,sizeof(f));
    scanf("%d%d",&n,&v);
    for(int i=1;i<=n;i++)
        scanf("%d%d%d",&c[i],&w[i],&s[i]);
    for(int i=1;i<=n;i++)
        for(int k=1;k<=s[i];k++)
            for(int j=v;j>=c[i];j--)
                f[j]=max(f[j],f[j-c[i]]+w[i]);
    printf("%d",f[v]);
}

 2.针对1的二进制优化:

对于有n件的物品,可以将其分为1、2、4...2^n件物品。

 模板:洛谷P1776

(怎么加个二进制优化就变成了绿题)

#include<bits/stdc++.h>
#define N 100001
#define M 40001
using namespace std;
int n,v,c[N],w[N],s[N],f[N];
int main(){
    memset(f,0,sizeof(f));
    scanf("%d%d",&n,&v);
    for(int i=1;i<=n;i++)
        scanf("%d%d%d",&w[i],&c[i],&s[i]);
    for(int i=1;i<=n;i++){
        int t=1;
        bool test=true;
        while(t){
            int ci=c[i]*t,wi=w[i]*t;
            for(int j=v;j>=ci;j--)
                f[j]=max(f[j],f[j-ci]+wi);
            if(!test) break;
            s[i]-=t,t*=2;
            if(s[i]<=t) t=s[i],test=false;
        }
    }
    printf("%d",f[v]);
}

感觉写得不太好看

3.单调队列优化

等学到单调队列动态规划再写(鸽了)

4.混合背包

原来混合背包也算绿题?

洛谷P1833:

#include<bits/stdc++.h>
#define N 10001
#define M 1001
using namespace std;
char ch;
int h1,h2,t1,t2,n,v,c[N],w[N],s[N],f[M];
int main(){
    memset(f,0,sizeof(f));
    scanf("%d%c%d%d%c%d%d",&h1,&ch,&t1,&h2,&ch,&t2,&n);
    v=60*(h2-h1);
    if(t2>=t1) v+=(t2-t1);
    else v-=(t1-t2);
    for(int i=1;i<=n;i++)
        scanf("%d%d%d",&c[i],&w[i],&s[i]);
    for(int i=1;i<=n;i++){
        if(!s[i])
            for(int j=c[i];j<=v;j++)
                f[j]=max(f[j],f[j-c[i]]+w[i]);
        else{
            int t=1;
            bool test=true;
            while(true){
                int ci=c[i]*t,wi=w[i]*t;
                for(int j=v;j>=ci;j--)
                    f[j]=max(f[j],f[j-ci]+wi);
                if(!test) break;
                s[i]-=t,t*=2;
                if(t>=s[i]) t=s[i],test=false; 
            }
        }
    }
    printf("%d",f[v]);
}

5.多维背包

限制条件增加时,状态转移方程维度也增加即可。

所有维度均要逆向循环避免重复。

#include<bits/stdc++.h>//二维背包,洛谷P1855
#define N 201
using namespace std;
int n,m,t,c1[N],c2[N],f[N][N];
int main(){
    scanf("%d%d%d",&n,&m,&t);
    for(int i=1;i<=n;i++)
        scanf("%d%d",&c1[i],&c2[i]);
    for(int i=1;i<=n;i++)
        for(int j=m;j>0;j--){
            if(j<c1[i]) break;
            for(int k=t;k>0;k--)
                if(k>=c2[i]) f[j][k]=max(f[j][k],f[j-c1[i]][k-c2[i]]+1);
                else break;
        }
    printf("%d",f[m][t]);
}

 6.分组背包

本质和01背包类似,关键是如何实现“每组中只能选一件”。

01背包可以理解为每组物品有放或不放两种选择,分组背包可以理解为每组物品有 k [ i ] + 1 种选择。

伪代码:

for 1...k (循环组数)

  for v...0 (循环空间)

    for 1...k [ i ] (循环组中每个个体)

      状态转移方程

先循环空间再循环组中个体,使得每次在处理一组中不同物品时,都只更改了一个 f [ v ] 的值,实现了每组中只选一件

(感觉还需要再理解一下)

模板:

洛谷U280369

#include<bits/stdc++.h>
#define N 10001
using namespace std;
int main(){
    int n,v,cnt=0,s[N],c[N],w[N],f[N];
    scanf("%d%d",&n,&v);
    for(int i=1;i<=n;i++){
        scanf("%d",&s[i]);
        for(int j=1;j<=s[i];j++){
            cnt++;
            scanf("%d%d",&c[cnt],&w[cnt]);
        }
    }
    int t=0;
    for(int i=1;i<=n;i++){
        for(int j=v;j>0;j--){
            for(int k=1;k<=s[i];k++){
                t++;
                if(j>=c[t]) f[j]=max(f[j],f[j-c[t]]+w[t]);
            }
            t-=s[i];        //重新从本组开始循环
        }
        t+=s[i];            //从下一组开始循环
    }
    printf("%d",f[v]);
}

洛谷P1757:

采用链式前向星的方法存储了组(还是链表?)还是不太习惯这种存图

#include<bits/stdc++.h>
#define N 1001
using namespace std;
int n,m,c[N],w[N],f[N],cnt=0;     //类似前向星
int head[N],to[N];                //head[i]表示第i组第一个
int main(){                       //to[i]表示第i个下一个同组
    memset(head,-1,sizeof(head));
    memset(to,-1,sizeof(to));
    memset(f,0,sizeof(0));
    scanf("%d%d",&m,&n);
    for(int i=1;i<=n;i++){
        int t;
        scanf("%d%d%d",&c[i],&w[i],&t);
        if(head[t]!=-1) to[i]=head[t];
        head[t]=i;
        cnt=max(cnt,t);
    }
    for(int i=1;i<=cnt;i++)
        for(int j=m;j>0;j--){
            int k=head[i];
            do{
                if(j>=c[k]) f[j]=max(f[j],f[j-c[k]]+w[k]);
                k=to[k];
            }while(k!=-1);
        }
    printf("%d",f[m]);
}

 

posted @ 2023-10-10 14:38  key4127  阅读(29)  评论(0)    收藏  举报