• 背包六讲(也不知道为啥就是六个 $QwQ$)


    浅谈

    对于本文来说,如果没有特殊声明,则题目描述的顺序就是输入的顺序,题目来源皆来自于AcWing;
    本文为了压缩文本,题目只给大意,寻找数据,还请到AcWing寻找。

    明确些东西

    (1.)容量 : 也就是这一个题中 , 我们怎样表示状态的数组
    (2.)体积(费用) : 也就是这一个题中 , 我们用来转移的数组,或者说,建立关系的数组
    (3.)价值 : 也就是我们维护的数组。

    1. 01背包

    【题目描述】:有(N)件物品和一个容量是(V)的背包。每件物品只能使用一次。第(i)件物品的体积是(v_i),价值是(w_i)。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
    (f_{i,j})表示总共拿第(i)种物品,总共花费(j)体积的最大价值
    第一层循环枚举每一个物品,看一下是否可以拿第(i)种物品,同时,第二层循环,枚举体积(j)
    【状态转移】
    当前背包的体积不够这个物品放的((j<v_i)),前(i-1)个物品最优解 (f_{i,j}=f_{i-1,j})
    当前背包的体积够的时候,面临两个选择:
    选的时候:(f_{i,j} =max(f_{i,j},f_{i-1,j-v_i+}w_i))
    不选的时候: (f_{i,j}=f_{i-1,j})
    然后就开始了:

    #include <iostream>
    #include <cstring>
    #include <algorithm>
    #include <cmath>
    using namespace std;
    int n,m;
    int w[1001],v[1001];
    int f[1001][1001];
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(register int i=1;i<=n;i++)
    	{
    		scanf("%d%d",&v[i],&w[i]);
    	}
    	for(register int i=1;i<=n;i++)
    	{
    		for(register int j=0;j<=m;j++)
    		{
    			f[i][j]=f[i-1][j];//这个初始化的意思就是,我们不选这个物体 
    			if(j>=v[i])
    			{
    				f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);//状态转移 
    			}
    		}
    	}
    	cout<<f[n][m]<<endl;
    	return 0;
    }
    

    然后发现这个是恶心的两维,然后复杂度是O((nm)),接下来我们考虑一下优化(滚动数组不会)

    1. 优化(二维压成一维的可行性)
      f_{i,j}=f_{i-1,j}$ (不含i的所有的选法的最大价值)
      (f_{i,j}=max(f_{i,j},f_{i-1,j-v_i}+w_i)) (包含物品i的所有选法的最大价值)
      然后发现,第(i)物品的状态取决于第(i-1)件物品的状态,那么我们就没必要保留第(i-2)的物品的状态了(具体问题具体说,这里是板子)

    2. 对于优化后的01背包为什么需要倒序枚举,一开始我也确实是很迷,在 (nyx)的帮助下理解了一下,经过 11.18号的学习,之后,也是明白了,说一下:
      优化完之后我们只有上一层的状态,更新值的时候也是只能原地滚动更改,我们在更新索引值较大的dp值的时候需要索引值较小的,也就是需要保证在更
      新索引值较大的dp值之前,必须保证索引值较小的上一层的dp值还在,且没被更新,所以我们需要从大到小枚举,换个意思说,如果我们从小到大枚举,
      我们现在更新体积为(j)的情况,我们需要用到(j-v_i)的,我们需要记录同时我们进行更改,而到了其他的体积(j_2),状态转移的时候需要前面的体积,会重新覆盖导致错误。
      然后代码:

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <cmath>
    using namespace std;
    int f[1001],n,m;
    int v[1001];
    int w[1001];
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(register int i=1;i<=n;i++)
    	{
    		scanf("%d%d",&v[i],&w[i]);
    	}
    	for(register int i=1;i<=n;i++)
    	{
    		for(register int j=m;j>=v[i];j--)
    		{
    			f[j]=max(f[j],f[j-v[i]]+w[i]);
    		}
    	}
    	cout<<f[m]<<endl;
    	return 0;
    } 
    

    2. 完全背包:

    【题目描述】:有(N)种物品和一个容量是(V)的背包,每种物品都有无限件可用。第(i)种物品的体积是&v_i&,价值是(w_i)。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
    【解题思路】
    第一层循环枚举物品的种类(i),第二层循环枚举体积(j),第三次循环则是枚举不大于体积(j)的最大的物品个数,
    状态转移方程:看一下 01 背包,也就十分清楚了
    (f_{i,j}=max(f_{i,j},f_{i-1,j-k*v_i}+k*w_i))

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <queue>
    #include <cmath>
    using namespace std;
    int n,m;
    int v[1001],w[1001];
    int f[1001][1001];
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(register int i=1;i<=n;i++)
    	{
    		scanf("%d%d",&v[i],&w[i]);
    	}
    	for(register int i=1;i<=n;i++)
    	{
    		for(register int j=0;j<=m;j++)
    		{
    			for(register int k=0;k*v[i]<=j;k++)
    			{
    				f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
    			}
    		}
    	}
    	cout<<f[n][m]<<endl;
    	return 0;
    }
    

    然后发现这个复杂度挺高的,O((nm^2)),很容易就会(TLE)掉,所以类比于01背包,然后就容易优化了
    状态转移方程
    (f_j=max(f_j,f_{j-v_i}+w_i))

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <cmath>
    using namespace std;
    int n,m;
    int v[1001],w[1001];
    int f[1001];
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(register int i=1;i<=n;i++)
    	{
    		scanf("%d%d",&v[i],&w[i]);
    	}
    	for(register int i=1;i<=n;i++)
    	{
    		for(register int j=v[i];j<=m;j++)//01背包从大到小。完全背包从小到大 
    		{
    			f[j]=max(f[j],f[j-v[i]]+w[i]); 
    		}
    	}
    	cout<<f[m]<<endl;
    	return 0;
    }
    

    同样的,我还是不会滚动数组,所以也就是优化到这了

    3.多重背包

    【题目描述】
    (N)种物品和一个容量是(V)的背包。第(i)种物品最多有(s_i)件,每件体积是(v_i),价值是(w_i)。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。
    【思路分析】
    对于这个题来说,我们可以直接将这个多重背包拆开,拆成(01)背包,可以理解到那个场面,无非也是多了一层循环,其余均一样,去掉第一维的优化过程也就没必要说了,值得一说的是多重背包的两种大优化(能过20000的),
    【Code】:类比01背包优化(非优化):

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <cmath>
    #include <queue>
    #include <stack>
    #include <set>
    using namespace std;
    const int maxn=1e6;
    inline int read()
    {
    	int x=0,f=1;char ch=getchar();
    	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
    	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
    	return x*f;
    }
    int n,m;
    int s[maxn],v[maxn],w[maxn];
    int f[maxn];
    int main()
    {
    	n=read(),m=read();
    	for(int i=1;i<=n;i++)
    	{
    		v[i]=read(),w[i]=read(),s[i]=read();
    	}
    	for(int i=1;i<=n;i++)
    	{
    		for(int j=m;j>=0;j--)
    		{
    			for(int k=0;k<=s[i];k++)
    			{
    				if(j>=k*v[i])
    				{
    					f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
    				}
    			}
    		}
    	}
    	printf("%d",f[m]);
    	return 0;
    } 
    

    【优化方案,二进制优化】
    我们十分容易发现,我们在没有优化的时候是把多重背包全部拆分成(01)背包,但如果数据大了, 光拆分时间复杂度就已经够了。
    再接下来我们想起来,如果我们不是一个个的拆分成(01)背包,而是拆分成一堆一堆的呢,利用二进制的思想进行拆分,在拆分的时候我们直接选择一堆一堆的,就例如,我们选3个物品(A),最优,如果是(01)背包的话,就是分成 1 ,1, 1;但是如果二进制优化了,就是 1 , 2,数据更大,则优化的更加明显,总而言之就是,拆分成二的倍数(如果拆到不能拆的时候,就是自己一个背包就可以了) 1,2,4,8,12,24,48,……之类的,反正就是可以凑出需要的背包数,就比如 11个物品(A),11在二进制下,是(1011=1000+10+1) 然后 (1000=4,10=2,1=1),只需要 物品个数为1,2,4的三堆就可以了。

    /*二进制优化多重背包*/
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <cmath>
    #include <queue>
    #include <stack>
    #include <set>
    #include <vector>
    using namespace std;
    inline int read()
    {
        int x=0,f=1;char ch=getchar();
        while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
        while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
        return x*f;
    }
    struct Node
    {    //定义结构体用它重新构建01背包 
        int volume;
        int value;
    };
    int f[2000],n,m;
    int main()
    {
        vector<Node>Goods;  //定义结构体数组 
        cin>>n>>m;          //输入物品数和背包大小 
        for(int i=1;i<=n;i++)
        {
            int volume,value,counts; 
            cin>>volume>>value>>counts;//现场输入物品的体积、价值、数量 
            for(int j=1;j<=counts;j<<=1)//开始拆解数量重新构建商品赋予价值和体积 
            {
                counts-=j;
                Goods.push_back({j*volume,j*value});
            }
            if(counts>0) //如果有剩余单独构建一个商品 
            {
                Goods.push_back({counts*volume,counts*value});
            }   
        }
        for(int i=0;i<Goods.size();i++)//接下来就是熟悉的01背包了遍历每件商品 
        {
            for(int j=m;j>=Goods[i].volume;j--)//体积从大到小 
            {
                f[j]=max(f[j],f[j-Goods[i].volume]+Goods[i].value);//状态转移 
            }
        }
        cout<<f[m]<<endl;//输出答案 
        return 0;
    }
    

    再优化,单调队列优化:请见 单调队列第三个

    4.分组背包

    【题目描述】:
    (N)组物品和一个容量是(V)的背包。每组物品有若干个,同一组内的物品最多只能选一个。每件物品的体积是(v_{i,j}),价值是(w_{i,j}),其中(i)是组号,(j)是组内编号。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。
    【思路分析】
    每一个分组都可以看成一个集合(A),那么我们只能从这里面挑出一个物品(k),也就是(k in A),所以可以在(k)和集合(A)中建立关系,可能会想到建图,但是当(A)中的编号为(k)的物品已经(OK)了,我们的状态转移也应该用到这个(k)要还是不要决定下一个,这样的话,你的写一个很难表示,所以直接用一个二维数组来表示就可以了。然后就可以开始了;
    【状态设计】
    还是 (f_{i,j})表示从前(i)组选,体积为(j)的最大价值
    【状态转移】
    我们考虑,在 (f_{i,j})可以由什么得到,它可以由同一组的得到,也可以从上一组的得到,那么状态转移的时候外层循环就需要枚举总共有多少组了(同时也就是说,我们也应该求出组数,例如link,就别忘了求),
    (f_{i,j}=max(f_{i-1,j}, maxlimits_{0leq kleq该组的所有物品数} f_{i-1,j - v_{i,k}}+w_{i,k}))表示从前(i)组选,但是我们有两个选择就是可以不选该该组中的物品或不选
    选,那么就是 (f_{i-1,j})
    不选对应的情况就是(maxlimits_{0leq kleq该组的所有物品数} f_{i-1,j - v_{i,k}}+w_{i,k})其中 (v_{i,k})表示的是在第(i)组中的第(k)个物品所应对的体积,那么自然,(w_{i,k})表示的就是价值了。
    【代码】

    #include<iostream>
    #include<algorithm>
    using namespace std;
    const int N=110;
    int f[N][N],v[N][N],w[N][N],s[N];
    int main()
    {
        int n,m;
        cin>>n>>m;
        for(int i=1;i<=n;i++)
        {
            cin>>s[i];
            for(int j=1;j<=s[i];j++)//第 i组背包的第 j个物品体积,价值 
            {
            	cin>>v[i][j]>>w[i][j];
    		}
        }
        for(int i=1;i<=n;i++)//n组,背包,枚举背包数目 
    	{
            for(int j=0;j<=m;j++) // 枚举体积 ,类比前面的背包 
    		{
    			f[i][j]=f[i-1][j]; 
                for(int k=1;k<=s[i];k++)//枚举每一组中的物品 
                {
                	if(j>=v[i][k]) //体积够才拿,不然你拿个锤子 
    				{
    					f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
    				} 
    			}
            }
        }
        cout<<f[n][m];
        return 0;
    }
    

    类比01背包优化一下,去掉第一维:
    【Code】:(上一个有注释,这个就不要了吧(QwQ)

    #include<iostream>
    #include<algorithm>
    using namespace std;
    const int N=110;
    int f[N],v[N][N],w[N][N],s[N];
    int main()
    {
        int n,m;
        cin>>n>>m;
        for(int i=1;i<=n;i++)
    	{
            cin>>s[i];
            for(int j=1;j<=s[i];j++)
            {
            cin>>v[i][j]>>w[i][j];
    		}
        }
        for(int i=1;i<=n;i++)
    	{
            for(int j=m;j>=0;j--)
    		{
                for(int k=1;k<=s[i];k++)
                {
                	if(v[i][k]<=j) 
    				{
    					f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
    				}
    			}     
            }
        }
        cout<<f[m];
        return 0;
    }
    

    【注意】
    1.倒序枚举 (j) 类比01背包的优化
    2.对于每一组内 (s_i)个物品的循环(k)应放在(j)的内层。从背包角度看,这是因为每组内至多选一个物品,若把(k)置于(j)的外层,就会类似于多重背包。每组背包在(f)数组上转移就会产生累积,最终可以选择超过1个物品。从动态规划角度看,(i)是阶段,(i)(j)共同组成“状态”,而(k)是决策——在第i组内使用哪一个物品。这三者的顺序绝对不能混淆。 ————李煜东《算法竞赛进阶指南》

    5.有依赖性的背包

    【题目分析】
    有 N 个物品和一个容量是 V 的背包。物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。如下图所示:

    如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。每件物品的编号是(i),体积是(v_i),价值是(w_i),依赖的父节点编号是(p_i)。物品的下标范围是 1…N。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。
    【输入格式】
    第一行有两个整数 N,V,用空格隔开,分别表示物品个数和背包容量。
    接下来有 N 行数据,每行数据表示一个物品。
    (i)行有三个整数(v_i,w_i,p_i)用空格隔开,分别表示物品的体积、价值和依赖的物品编号。
    如果 (p_i)=−1,表示根节点。 数据保证所有物品构成一棵树。
    【输出格式】
    输出一个整数,表示最大价值。
    【思路分析】
    很明显,所属关系是一棵树,那么我们要建树,同时考虑一下该节点的信息从何而来,树形DP的状态往往来自于子节点或者父亲节点。发现我们在分配背包空间时,不再像之前一样按每一个物品分配背包空间,发现,根节点必须选(你不选你啥都没有),然后根节点就可以分配到全部的背包空间,然后根节点的儿子节点记为(to),则(sum v_{to}=分配的空间-v_{根节点}),也就是说,他的儿子节点和他自己共同分享这个背包的空间,所以我们在状态转移的时候也就是要直接转移空间的大小,而不是枚举物品数目(儿子还有儿子,儿子的儿子还有儿子,子子孙孙无穷匮也),但是我们只要一遍Dfs(大风扇),就可以初始化选择这个点的全部价值,最后在回溯的时候就考虑(now)和它的儿子结点的转移即可。
    【状态设计】
    我们搞清楚是怎么分配的体积就可,方式就是按每一个子树分多少体积进行分组,每棵子树对应一组,所以状态也就是 (f_{i,j})表示的是节点i为子树分配(j)体积的最大价值,其中(i)为子树,等价于分组背包中的一组,结合上文yy一下;
    【状态转移】
    (f_{now,j}=maxlimits_{0 leq k leq j-v_{now}}(f_{now,j},f_{now,j-k}+f_{to,k}))表示now这颗子树,中分配(j)体积的最大值就是只选择这颗子树的根,或者就是选择这颗子树的某些节点,但是这些节点也不一定全选(全选还DP个锤子),给节点分大小为(k)的体积,(DP)一下,然后看一下最大值就行了,

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <cmath>
    using namespace std;
    const int maxn=1e6; 
    inline int read()
    {
    	int x=0,f=1;char ch=getchar();
    	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
    	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
    	return x*f;
    }
    struct node
    {
    	int nxt,to;
    }edge[maxn<<1];
    int n,m,number_edge,root;
    int v[maxn],w[maxn],head[maxn];
    void add_edge(int from,int to)//建树自然要用到邻接表,啊这,我不会vector 
    {
    	number_edge++;
    	edge[number_edge].nxt=head[from];
    	edge[number_edge].to=to;
    	head[from]=number_edge;
    }
    int f[1001][1001];//上面的状态设计就不必说了 
    void dfs(int now,int fa)//英文缩写简洁明了 
    {
    	for(int i=v[now];i<=m;i++)//既然到了 now这个点,那么就意味着,必须选这个点了,所以初始化 
    	{
    		f[now][i]=w[now];
    	}
    	for(int i=head[now];i;i=edge[i].nxt) 
    	{
    		int to=edge[i].to;
    		if(to==fa)//不能嗨皮地回去 
    		{
    			continue;
    		}
    		dfs(to,now);
    		//合并 
    		for(int j=m;j>=v[now];j--)// j的范围就是 m 到 v[now],如果小于,那么也就是不能选择 now这棵子树上的 
    		{
    			for(int k=0;k<=j-v[now];k++)//采取分空间,直接分给子树k的空间树 
    			{
    				f[now][j]=max(f[now][j],f[now][j-k]+f[to][k]);
    			}
    		}
    	}
    }
    int main()
    {
    	n=read(),m=read();
    	for(int i=1;i<=n;i++)
    	{
    		int fa;
    		v[i]=read(),w[i]=read(),fa=read();
    		if(fa==-1)//求出根节点 
    		{
    			root=i;
    		}
    		else //建立树边 
    		{
    			add_edge(i,fa);
    			add_edge(fa,i);
    		}
    	}
    	dfs(root,0);//开始树形DP 
    	printf("%d",f[root][m]);//ans,如果树上的一个节点存储多个信息,那么ans就不这么简单了 
    	return 0;
    }
    

    6.二维费用背包:

    【题目描述】
    (N)件物品和一个容量是(V)的背包,背包能承受的最大重量是(M)。每件物品只能用一次。体积是(v_i),重量是(m_i),价值是(w_i)。求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。输出最大价值。
    【思路分析】
    有点显而易见了,不再局限于体积,还要有价值,无非就是多了一层循环罢了

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <cmath>
    using namespace std;
    const int maxn=1e3; 
    inline int read()
    {
    	int x=0,f=1;char ch=getchar();
    	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
    	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
    	return x*f;
    }
    int n,m,s;
    int f[maxn][500][500],w[maxn],v[maxn],l[maxn];
    int main()
    {
    	n=read(),m=read(),s=read();
    	for(int i=1;i<=n;i++)
    	{
    		v[i]=read(),l[i]=read(),w[i]=read();
    	}
    	for(int i=1;i<=n;i++)
    	{
    		for(int j=0;j<=m;j++)
    		{
    			for(int k=0;k<=s;k++)
    			{
    				f[i][j][k]=f[i-1][j][k];//不拿 
    				if(k>=l[i] && j>=v[i])//先判断一下是否能拿,这个地方不能把这个判断放到循环里面 
    				{
    				f[i][j][k]=max(f[i-1][j-v[i]][k-l[i]]+w[i],f[i][j][k]);
    				}
    			}
    		}
    	}
    	printf("%d",f[n][m][s]);
    	return 0;
    } 
    

    再次优化后

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <cmath>
    #include <queue>
    using namespace std;
    int n,m,v;
    int f[1001][1001];
    int mi[1001],vi[1001],wi[1001];
    int main()
    {
    	scanf("%d%d%d",&n,&v,&m);
    	for(register int i=1;i<=n;i++)
    	{
    		scanf("%d%d%d",&vi[i],&mi[i],&wi[i]);
    	}
    	for(register int i=1;i<=n;i++)
    	{
    		for(int j=v;j>=vi[i];j--)
    		{
    			for(int k=m;k>=mi[i];k--)
    			{
    				f[j][k]=max(f[j][k],f[j-vi[i]][k-mi[i]]+wi[i]);
    			}
    		}
    	}
    	cout<<f[v][m]<<endl;
    	return 0;
    }
    
  • 相关阅读:
    rem布局原理
    vue引入bootstrap.min.css报错:Cannot find module "./assets/css/bootstrap.min.css"
    安装cnpm
    安装webpack出现警告: fsevents@^1.0.0 (node_moduleschokidar ode_modulesfsevents):
    npm run dev报错,events.js:160 throw er; // Unhandled 'error' event
    让webstorm支持新建.vue文件
    电脑上已经安装mysql之后安装wamp,wamp中的mysql无法启动的解决办法
    Hibernate
    C和指针
    如何测试一个杯子
  • 原文地址:https://www.cnblogs.com/Zmonarch/p/14000279.html
Copyright © 2020-2023  润新知