zoukankan      html  css  js  c++  java
  • 背包九讲

    一般的背包问题:

    每种物品有一个价格W和体积V,你现在有一个容积为V的背包。问你怎么装使背包的价值和最大。

    01背包

    多种物品,每个物品只有一个,求能或得的最大总价值。

    如果我们不选第i件物品,那我们就相当于用 i-1 件物品,填充了体积为V的背包所得到的最优解。

    当我们选第i件物品时,就相当于用i-1的物品,填充v - c[i]/的背包所得到的最优解。

    用f[i][v]表示用i件物品填充体积为V的背包所得到的价值。

    那么方程式就是:

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

    一维的就是

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

    显而易见,我们f[j] 只会被以前的状态影响。

    如果我们顺序枚举,就可能会被前面的状态影响掉。

    那我们考虑倒叙枚举,这样f[j] 不会被之前的状态影响,且我们更新的话也不会影响其他位置的状态

    代码

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<cmath>
     4 using namespace std;
     5 int f[1001],w[1001],v[1001];
     6 int main(){
     7     int n,m;
     8     scanf("%d%d",&n,&m);
     9     for(int i = 1;i <= n; i++){
    10         scanf("%d%d",&w[i],&c[i]);
    11     }
    12     for(int i = 1; i <= n; i++){
    13         for(int j = m; j >= w[i]; j--){
    14             f[j] = max(f[j],f[j-w[i]]+v[i]);
    15         }
    16     }
    17     prinf("%d",f[m]);
    18     return 0;
    19 } 

    水题——>采药

    完全背包

    每个物品有无限个,可重复选取;

    转移方程就是

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

    跟01背包类似,我们可以把它给成一维的,只不过要正着枚举。

    1     for(int i = 1;i <= n; i++){
    2         for(int j = w[i]; j <= m; j++){
    3             f[j] = max(f[j] , f[j-w[i]]+v[i]);
    4         }
    5     }

    多重背包

    跟01背包类似,只不过他每种物品有k个,而不是1个

    1.转化为01背包

     我们可以把每件物品选k次,转换为有k个相同的物品,每个物品选一次

    然后就可以套用01背包

    时间复杂度O(nwΣki)

    2.二进制优化

    我们仍考虑转化为01背包 ,我们可以对k的拆分入手

    对于一个数k 可以根据二进制拆成2^j相加的形式。

    但我们要保留1个物品,然后在对其他的k-1个进行拆分

    例如

    1. 6 = 1 + 2 + 3
    2. 8 = 1 + 2 + 4 +1
    3. 18 = 1 + 2 + 4 + 8 + 3
    4. 31 = 1 + 2 + 4 + 8 +16
     1 cnt = 0;//当前的物品数
     2 for(int i = 1; i <= m; i++){
     3     int c = 1;
     4     scanf("%d%d",&p,&vv,&k);
     5     while(k - c > 0){//打包k-1个物品
     6           k -= c;
     7           w[++cnt] = c * p;
     8           v[cnt  = c *  vv;
     9            c *= 2;
    10     }
    11     w[++cnt] = p * k;//打包剩下的那1个物品
    12     v[cnt] = k * vv;
    13 }
    二进制拆分
    另一种方法
    1     for(int i = 1; i <= n; i++){
    2         scanf("%d%d%d",&val,&p,&k);
    3         for(int j = 1; j <= k; j<<=1){
    4             k -= j;
    5             v[++cnt] = j * p;
    6             w[cnt] = j * val;
    7         }
    8         if(k != 0) v[++cnt] = k * p,w[cnt] = k * val;
    9     

    3.单调队列优化

     我不会QAQ,但也不常考。

    有兴趣的可以看一下链接

    习题 : P1776  宝物筛选

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<algorithm>
     4 using namespace std;
     5 int n,m,p,val,k,cnt = 1;
     6 int w[1000100],v[1000100],f[1000100];
     7 int main(){
     8     scanf("%d%d",&n,&m);
     9     for(int i = 1; i <= n; i++){
    10         scanf("%d%d%d",&val,&p,&k);
    11         for(int j = 1; j <= k; j<<=1){
    12             k -= j;
    13             v[++cnt] = j * p;
    14             w[cnt] = j * val;
    15         }
    16         if(k != 0) v[++cnt] = k * p,w[cnt] = k * val;
    17     }
    18     for(int i = 1; i <= cnt; i++){
    19         for(int j = m; j >= v[i]; j--){
    20             f[j] = max(f[j],f[j-v[i]] + w[i]); 
    21         }
    22     }
    23     printf("%d\n",f[m]);
    24     return 0; 
    25 }
    宝物筛选

    二维费用背包

    例题 P1855 榨取kkksco3(普及减的水题)

    很明显的01背包问题,但选一个物品会消耗两种价值。

    我们可以考虑再开一维数组,同时转移两个价值(其他背包同理)

    但不能够再开一维数组存物品编号,因为容易MLE

    1 for (int k = 1; k <= n; k++) {
    2   for (int i = m; i >= mi; i--)    // 对经费进行一层枚举
    3     for (int j = t; j >= ti; j--)  // 对时间进行一层枚举
    4       dp[i][j] = max(dp[i][j], dp[i - mi][j - ti] + 1);
    5 }

    三倍经验

    1. L国的战斗之间谍
    2. NASA的食物计划

    分组背包

    水题 ——> P1757 通天之分组背包

    就是讲物品分组,每组物品只能选一个。

    我们可以把在所有物品中选一件,变成从当前组中选一件,直接跑01背包就okk了

    1 for (int k = 1; k <= ts; k++)          // 循环每一组
    2   for (int i = m; i >= 0; i--)         // 循环背包容量
    3     for (int j = 1; j <= cnt[k]; j++)  // 循环该组的每一个物品
    4       if (i >= w[t[k][j]])
    5         dp[i] = max(dp[i],  dp[i - w[t[k][j]]] + c[t[k][j]]);  // 像0-1背包一样状态转移

    进阶:P3961 黄金矿工

    我们可以一次求出每条直线的斜率,在排个序(方便判断斜率是否相等)

    相等的分在一组物品中,再跑一遍01背包就AC了

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<algorithm>
     4 using namespace std;
     5 int n,T,cnt,maxn;
     6 int t[210][210],v[210][210],num[210];
     7 int f[40010];
     8 struct node{
     9     int x,y,v,t;
    10     double k;
    11 }e[210];
    12 int comp(node a,node b){
    13     if(a.k == b.k) return a.y < b.y;
    14     return a.k < b.k;
    15 }
    16 int main(){
    17     scanf("%d%d",&n,&T);
    18     for(int i = 1; i <= n; i++){
    19         scanf("%d%d%d%d",&e[i].x,&e[i].y,&e[i].t,&e[i].v);
    20         e[i].k = e[i].y *(1.0) / e[i].x * (1.0);//求斜率
    21     }
    22     sort(e+1,e+n+1,comp);//排序
    23     for(int i = 1; i <= n; i++){
    24         if(e[i].k != e[i-1].k || i == 1) {//找到一条斜率不同的直线
    25             cnt++;
    26         }
    27         if(num[cnt] == 0){//第一件物品
    28             num[cnt]++;
    29             t[cnt][1] = e[i].t;
    30             v[cnt][1] = e[i].v;
    31         }
    32         else{//其他物品
    33             num[cnt]++;
    34             t[cnt][num[cnt]] = t[cnt][num[cnt]-1] + e[i].t;
    35             v[cnt][num[cnt]] = v[cnt][num[cnt]-1] + e[i].v;
    36         }
    37     }
    38     f[0] = 0;
    39     for(int i = 1; i <= cnt; i++){//分组背包
    40         for(int j = T; j >= t[i][1]; j--){
    41             maxn = f[j];
    42             for(int k = 1; k <= num[i]; k++){
    43                 if(j > t[i][k]){
    44                     maxn = max(maxn,f[j-t[i][k]] + v[i][k]);
    45                 }
    46             }
    47             f[j] = maxn;
    48         }
    49     }
    50     printf("%d",f[T]);
    51     return 0;
    52 } 

    有依赖的背包(树形背包)

    例题——>金明的预算方案

    我们称不依赖别的物品的为主件,依赖于于其他物品的为附件;

    包含一个主件和若干个附件有一下可能;

    1. 只选附件
    2. 选完主件后再选一个附件
    3. 选完主件后再选两个附件
    4. 。。。。。

    可以将这几种可能性转换为一件物品,因为这几种可能性只能选一种,最后在跑一边分组背包

    如果是多叉树的集合,先算子节点的集合,最后再算父亲节点的集合。

     1 #include<algorithm>
     2 #include<cstdio>
     3 #include<iostream>
     4 using namespace std;
     5 int n,m,c,p,q,t;
     6 int w[65][3],v[65][3],f[65][3200]; 
     7 int main(){
     8     scanf("%d%d",&n,&m);
     9     n/=10;
    10     for(int i = 1; i <= m; i++){
    11         scanf(“%d%d%d”,&c,&p,&q);
    12         c = c/10;
    13         if(q == 0){//主件
    14             w[i][0] = c;
    15             v[i][0] = c*p;
    16         }
    17         else if(w[q][1] == 0){//第一件附件
    18             w[q][1] = c;
    19             v[q][1] = c*p;
    20         }
    21         else {//第二件附件
    22             w[q][2] = c;
    23             v[q][2] = c*p;
    24         }
    25     }
    26     for(int i = 1; i <= m; i++){
    27         for(int j = 0; j <= n; j++){
    28             f[i][j] = f[i-1][j];//一个都不选的情况
    29             if(j >= w[i][0]){//选主件的情况
    30                 t = f[i-1][j-w[i][0]] + v[i][0];
    31                 if(t > f[i][j]){
    32                     f[i][j] = t;
    33                 }
    34             }
    35             if(j >= w[i][0]+w[i][1]){//选主件和第一个附件的情况
    36                 t = f[i-1][j-w[i][0]-w[i][1]] + v[i][0]+v[i][1];
    37                 if(t > f[i][j]){
    38                     f[i][j] = t;
    39                 }
    40             }
    41             if(j >= w[i][0]+w[i][2]){//选主件和第二个附件的情况
    42                 t = f[i-1][j-w[i][0]-w[i][2]] + v[i][0]+v[i][2];
    43                 if(t > f[i][j]){
    44                     f[i][j] = t;
    45                 }
    46             }
    47             if(j >= w[i][0]+w[i][1]+w[i][2]){//选主件和所有附件的情况
    48                 t = f[i-1][j-w[i][0]-w[i][1]-w[i][2]] + v[i][0]+v[i][1]+v[i][2];
    49                 if(t > f[i][j]){
    50                     f[i][j] = t;
    51                 }
    52             }
    53         }
    54     }
    55     cout<<f[m][n]*10<<endl;
    56     return 0;
    57 }

     树形背包(进阶)

    一般的形式就是 给定一个n个节点的点权树,要求你从中选出m个节点,使这些选出的节点点权和最大,一个节点

    能被选择时,当且仅当父亲节点被选择时、

    O(n^3)解法

    考虑 f[u][i] 表示在u的子树中,选择i个节点(包括他本身的贡献)则方程式为

     1 f[u][i] = max(f[u][i]+f[v][i-j] + d[v] 

    其中d[v] 表示v的点权,i-j表示在子树中选i-j个节点

    例题——选课

     f[i][j] 表示在i的子树中选j个能得到的最大的学分。 一个要注意的点是,我们开了一个虚拟节点,那么我们在统计答案是应该是f[0][m+1] 而不是f[0][m],因为我们这个节点算上去。

    转移方程直接套用上面的就行了。

    附上完整代码

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<algorithm>
     4 using namespace std;
     5 int n,m,ans,tot,k;
     6 int head[310],f[310][310],size[310];//f[i][j] 表示在i的子树中选j个物品所得到的最大的学分数 
     7 struct node
     8 {
     9     int to;
    10     int net;
    11 }e[10100];
    12 void add(int x,int y)
    13 {
    14     tot++;
    15     e[tot].to = y;
    16     e[tot].net = head[x];
    17     head[x] = tot;
    18 }
    19 void dp(int x,int fa){
    20     size[x] = 1;
    21     for(int i = head[x]; i; i = e[i].net)
    22     {
    23         int  to = e[i].to;
    24         if(to == fa) continue;
    25         dp(to,x);
    26         for(int j = m+1; j >= 1; j--)//枚举x的子树中选j个物品 
    27         {
    28             for(int k = 0; k < j; k++)//枚举在 to的子树中选k个物品 
    29             {
    30                 f[x][j] = max(f[x][j] , f[x][j-k] + f[to][k]);
    31             }
    32         }
    33     }
    34 }
    35 int main(){
    36     scanf("%d%d",&n , &m);
    37     for(int i = 1; i <= n; i++)
    38     {
    39         scanf("%d%d",&k , &f[i][1]);
    40         add(k,i);
    41     }
    42     dp(0 , 0);
    43     printf("%d\n", f[0][m+1]);
    44     return 0;
    45 }

    背包求方案数

    给定一个背包容量,物品费用,和其他关系,求装到一定容量的方案数

    这种问题把求最大值改为求和就ok了

     1 dp[i] = Σ(dp[i] ,dp[i-c[i]) 

    初值 dp[o] = 1;

    因为 当容量为0时:一种方案为什么也不装。

    背包输出方案数

    我们用g[i][v] 表示体积为V时第i件物品选没选。

    int v = V;  // 记录当前的存储空间
    for (
        从最后一件循环至第一件)  // 因为最后一件物品存储的是最终状态,所以从最后一件物品进行循环
    {
      if (g[i][v]) {
        选了第 i 项物品;
        v -= 第 i 项物品的价值;
      } else
        未选第 i 项物品;
    }

    背包前k优解

    裸题——>多人背包

    这道题,我们可以对n件物品求一个01背包,然后k个人取前k优解就是答案。

    那么我们考虑怎么求前k优解

    我们设 f[i][j][k] 表示用i件物品,花费j的体积,得到的第k优解

    那么我们观察一下原来的转移方程

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

    我们可以发现 f[i][v] 只能由 f[i-1][v] , f[i-1][v-c[i]] + w[i] 转移过来,那么前k优解一定存在于着f[i-1][v][1~k] 以及f[i-1][v-c[i]][1~k] + w[i]当中。

    我们可以把这·f[i-1][v]的前k优解 和 f[i-1][v-c[i]]+w[i] 抽象成两个队列,

    那么这两个队列肯定是两个单调队列,因此,我们参考归并排序的思想,去这两个队列的前k大,那么这前k大的数,就是f[i][v]的前k优个解

    但考虑到三维数组肯定会MLE,因此我们可以像01背包那样抹去第一维。

    那么最后的代码就是

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    using namespace std;
    int k,V,n,ans;
    int v[255],w[255],f[5010][55],now[55];
    inline int read()
    {
        int s = 0, w = 1; char ch = getchar();
        while(ch < '0' || ch > '9'){ if(ch == '-') w = -1; ch = getchar();}
        while(ch >= '0' && ch <= '9'){s = s * 10 + ch-'0'; ch = getchar();}
        return s * w;
    }
    int main()
    {
        k = read(); V = read(); n = read();
        for(int i = 0; i <= V; i++) for(int j = 0; j <= k; j++) f[i][j] = -2333333;//初始化
        for(int i = 1; i <= n; i++)
        {
            v[i] = read(); w[i] = read();
        }
        f[0][1] = 0;//零体积的最优解为0
        for(int i = 1; i <= n; i++)
        {
            for(int j = V; j >= v[i]; j--)
            {
                int a1 = 1, b1 = 1,cnt = 0;//a1,b1分别为两个队列的队头,cnt为当前选了几个数即第k优解
                while(cnt <= k)
                {
                    if(f[j][a1] > f[j-v[i]][b1] + w[i])//选第一个队列的队头的时候
                    {
                        now[++cnt] = f[j][a1];
                        a1++;
                    }
                    else//选另一个队列队头的时候
                    {
                        now[++cnt] = f[j-v[i]][b1] + w[i];
                        b1++;
                    }
                }
                for(int u = 1; u <= k; u++) f[j][u] = now[u];//now存当前的第k优解
            //    for(int u = 1; u <= k; u++) cout<<now[u]<<endl;
            }
        }
        for(int i = 1; i <= k; i++) ans += f[V][i];
        printf("%d\n",ans);
        return 0;
    } 

    不懂得童鞋出门右转 ——> 顾Z大佬的背包九讲

    例题 ——>P3983 赛斯石

    这个题最大的难点在于物品可以拆开,但船不能拆

    如载重7的船,可以装4si和3si 也可以装1si和6si

    所以 1-10载重船的最大利益可以用背包求出来;

    最后选一些船走,船的收益已固定,可以跑完全背包求质量为n的最大收益

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<algorithm>
     4 using namespace std;
     5 long long n,b[15],v[15],w[15];
     6 long long ans[100010];
     7 long long a[12] = {0,1,3,5,7,9,10,11,14,15,17}; 
     8 int main(){
     9     scanf("%ld",&n);
    10     for(int i = 1; i <= 10; i++){
    11         scanf("%ld",&b[i]);
    12         v[i] = i; 
    13     }
    14     for(int i = 1; i <= 10; i++){//每条船的收益 
    15         for(int j = i; j <= 10; j++){
    16             w[j] = max(w[j],w[j-i] + b[i]);
    17         }
    18     }
    19     for(int i = 1; i <= 10; i++) w[i] -= a[i];//利润 
    20     ans[0] = 0;
    21     for(int i = 1; i <= 10; i++){//怎样租船利润最大 
    22         for(int j = v[i]; j <= n; j++){
    23             ans[j] = max(ans[j] , ans[j- v[i]] + w[i]);
    24         }
    25     }
    26     printf("%ld",ans[n]);
    27     return 0;
    28 } 

    例2 背包dp

    我们第一次想可能会想到 f[m] 表示 达到m种方案的最少钱数,跑一边多重背包,可是一看m的范围,完了炸了,数组开不了10^17那么大

    那我们换一种思路f[i][j]  表示用i件物品 花费j个Q币能达到的方案数。那这不就相当于一个分组背包了吗?

    为什么呢? 我们可以把它当成有n组,每组可以选1-num[i] 种情况,但每种情况只能选1次

    所以我们可以推出方程式来

    1 f[i][j] = max(f[i][j] , f[i-1][j-p[i]*k] * k

    p[i]表示第i个物品买一个要花多少钱,k表示你选了多少件物品

    我们在想一下,二维的开不了这么大,那我们可以像背包那样抹去第一维,给他来个滚动数组

    于是有了下面的方程

     1 f[j] = max(f[j] , f[j-p[i]*k] * k) 

    最后一定要注意的点是,要开long long 不然你就会炸

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<algorithm>
     4 using namespace std;
     5 #define LL long long
     6 LL n,m,tot;
     7 LL num[1000100],p[1000100],f[1000100];//不开long long 两行泪
     8 int main(){
     9     scanf("%lld%lld",&n,&m);
    10     for(LL i = 1; i <= n; i++) scanf("%lld",&num[i]);//每组最多选几个
    11     for(LL i = 1; i <= n; i++){
    12         scanf("%lld",&p[i]);//每件物品的价钱
    13         tot += p[i] * num[i];//计算最大花多少钱
    14     } 
    15     f[0] = 1;
    16     for(LL i = 1; i <= n; i++){//枚举组数
    17         for(LL j = tot; j >= p[i]; j--){//枚举体积
    18             for(LL k = 1; k <= num[i]; k++){//枚举每组选几个
    19                 if(p[i] * k <= j)  f[j] = max(f[j] , f[j-p[i]*k] * k);
    20             }
    21         }
    22     }
    23     for(LL i = 0; i <= tot; i++){//找到第一个大于等于m的方案数
    24         if(f[i] >= m){
    25             printf("%lld\n",i);
    26             return 0;
    27         }
    28     }
    29 } 
    完整代码

    例三: 粉刷匠

    我们可以维护两个数组 f[i][j][k] 表示第i个木块,刷k次,刷前j块,能刷对的最大格子数

                                       g[i][j]  表示 前i个木块,刷k次能刷对的最大格子数。

    那么f数组怎么转移呢

    我们考虑枚举每次刷的起点s 那么从s刷到j的贡献就是从s到j 1的个数 和0的个数取个max

    那么方程就可以写成

     1 f[i][j][k] = max(f[i][j][k] , f[i][s-1][k-1]+calc(i , s ,j)); 

    因为每个点最多被刷一次,所以要从s-1开始枚举

    这是我们想象,我们得到了一些刷着颜色段的木块,每一块木板有k种选择,可以刷1次或2次或。。。。,但每种情况只能选一种。(那这不就是妥妥的分组背包吗 QAQ

    对于每种情况,他们的体积是他们要粉刷的次数,他们的贡献就是f[i][m][k] 一共有n组,这样我们就可以套用分组背包来求。

    最后附上代码

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<algorithm>
     4 using namespace std;
     5 int n,m,t;
     6 int sum[55][55],a[55][55];
     7 int f[55][55][55],dp[55][2510];
     8 int calc(int i,int s,int j)//计算涂一还是涂零的分多 
     9 {
    10     return max(sum[i][j] - sum[i][s-1],j-s+1 -sum[i][j] + sum[i][s-1]);
    11 }
    12 int main(){
    13     scanf("%d%d%d",&n,&m,&t);
    14     for(int i = 1; i <= n; i++)
    15     {
    16         for(int j = 1; j <= m; j++)
    17         {
    18             scanf("%1d",&a[i][j]);
    19             sum[i][j] = sum[i][j-1] + a[i][j];//维护一个前缀和
    20         }
    21     }
    22     for(int i = 1; i <= n; i++)//枚举每个木板 
    23     {
    24         for(int j = 1; j <= m; j++)//枚举前几块
    25         {
    26             for(int k = 1; k <= m; k++)//枚举涂多少次 ,每块木板最多涂m次
    27             {
    28                 for(int s = 1; s <= j; s++)//枚举从哪开始涂 
    29                 {
    30                     f[i][j][k] = max(f[i][j][k] , f[i][s-1][k-1]+calc(i , s ,j));
    31                 }
    32             }
    33         }
    34     }
    35     for(int i = 1; i <= n; i++)//枚举每块木板 (每组)
    36     {
    37         for(int j = t; j >= 1; j--)//枚举体积 
    38         {
    39             for(int k = 0 ; k <= min(j,m); k++)//枚举涂多少次 ,但要保证k小于总共要涂得个数
    40             {
    41                 dp[i][j] = max(dp[i][j] , dp[i-1][j-k] + f[i][m][k]);
    42             }
    43         }
    44     }
    45     printf("%d\n",dp[n][t]);
    46     return 0;
    47 }

     

  • 相关阅读:
    分页小算法
    幻影粒子游戏开发
    X文件的导出系列1——静态模型
    JavaScript 强行弹出窗口 与 无提示关闭页面
    SqlHelper中使用事务
    一个不错的WEB打印解决方案!
    【转载】3层架构应用AspNetPager分页 GridView分页
    c#操作在word指定书签插入文字
    多数据库事务处理[改写]
    rdlc报表使用技巧一(转)
  • 原文地址:https://www.cnblogs.com/genshy/p/13287770.html
Copyright © 2011-2022 走看看