• 背包问题


    背包问题是程序设计与算法以及竞赛中的一类重要问题。其种类繁多,也是极其经典的考点


    例题:0-1背包

    有N件物品以及一个容量为V的背包。放入第i件物体的代价是C(i),得到的价值是W(i)。求在不超过容量的情况下获得的最大价值。

    数据范围: 1≤N≤100,1≤V≤10^6,1≤C(i)≤10000,1≤W(i)≤10000。

    输入格式:

    第1行两个正整数,分别表示N,V。

    接下来N行分别有2个正整数C(i),W(i)。

    输出格式:

    一行一个正整数,为最大价值总和。

    样例

    #in:
    4 20
    8 5
    9 6
    5 7
    2 3
    #out:
    16
    

    解析:

    动态规划解这类最优值问题,首先 定义状态 F{i,v}为前i件物品放入容量为V的背包中所可以取得的最大值 。那么很容易得到状态转移方程F{ i,v }=max( F { i,v },F{ i-1,v-C(i) } )

    可以这样理解:

    对于第i件物品,要么放,要么不放。那么可以把这个问题推到 只和前面i-1件物品放不放 有关系的问题,放即为F{ i-1,v },不放就F{ i-1,v-C(i)}加上放入i物品得到的价值W(i)。

    于是,时间复杂度 O( NV ),空间复杂度 O( NV )

    但是

    这里开二维数组极其容易爆炸 (第一次就RE了嘤嘤嘤

    并且我们可以利用滚动数组(即反复更新一个一维数组,由动态规划问题的无后效性可知状态只与上一个状态有关) 来稍微优化一下。

    最终程序:

    #include<bits/stdc++.h>
    using namespace std;
    int ci[4000],wi[4000];
    int f[138800]//下标要>=c;
    int main()
    {
    	int n,c;
    	cin>>n>>c;
    	int i,j;
    	for(i=1;i<=n;i++)
    	{
    		cin>>ci[i]>>wi[i];
    	}
    	for(i=1;i<=n;i++)
    	{
    		for(j=c;j>=ci[i];j--)
    		{
    			f[j]=max(f[j],f[j-ci[i]]+wi[i]);
    		}
    	}
    	cout<<f[c];
    	return 0;
    		
    }
    

    对于j循环条件,这里表示一个元素至多只取一次。

    若改成:

    for(j=ci[i];j<=c;j++)
    

    则是一个元素可以无限取。(即完全背包写法)

    原理: 从一维数组上区别0-1背包和完全背包差别就在循环顺序上,0-1背包必须逆序,因为这样保证了不会重复选择已经选择的物品,而完全背包是顺序,顺序会覆盖以前的状态,所以存在选择多次的情况,也符合完全背包的题意。状态转移方程为\(F[i] = max(F[i],dp[F-c[i]]+v[i])\)

    0-1背包:
    0-1背包
    完全背包:
    完全背包

    优化到这里,初等的题就完全不是问题了desu ~


    另一个简单有效的优化

    :若两件物品i、j满足\(c[i]=w[j]\),则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。

    这个优化可以简单的O(N^2)地实现,一般都可以承受。另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于V的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以O(V+N)地完成这个优化。
    详情--->背包九讲


    加个条件:不超过载重M(多维背包)

    其实不难,只需要将循环动一下手脚,数组增一维即可。

    for(int i=1;i<=n;i++)
        {
            for(int j=m;j>=k[i];j--)//每个元素最多只取一次 
            {
                for(int o=x;o>=z[i];o--)
                    f[j][o]=max(f[j][o],f[j-k[i]][o-z[i]]+p[i]);
            }
        }
    

    再来,每个物品有个数限制(即多重背包)

    这个时候再加个循环枚举就是了

    但是,很多时候(特别是物品多的时候)

    这样会超时(如P1833樱花

    所以,有一种优化方式叫做二进制优化

    基本思想:

    将物品的数量以二进制拆分,分成1,2,4,8,16,32,64 ....... 再把花费和价值乘以次数即可。

    例如一个物品可使用20次,则可拆为:1 , 2 , 4 , 8 , 5。

    //c[i]为代价,w[i]为价值,p[i]为使用次数
    for(int j=1;j<=p[i];j<<=1)/*二进制分堆,此时j相当于权重(应该是这样的 */
    {
    	for(int k=v;k>=j*c[i];k--)
    	{
    		f[k]=max(f[k],f[k-c[i]*j]+w[i]*j);
    	}
    	p[i]-=j;//算了的就减去了;
    }
    if(p[i]>0)//若c还有剩余,再来次背包 
    {
    		for(int k=v;k>=c[i]*p[i];k--)
    		{
    			f[k]=max(f[k],f[k-c[i]*p[i]]+w[i]*p[i]);
    		}
    }
    
    

    这样优化后求解单个状态时间能降到O(log(C/w[i]))


    分组背包

    这类问题也会非常普遍,例题洛谷P1757

    一般题目会告诉你,许许多多物品被分成几组,每组物品相互冲突。然后求最大价值。

    这个时候,就需要将同组别物品看做一个整体,第一层循环先遍历组别。然后在第三层循环遍历组内的每组物品,且保证每组物品都只取一次。

    例题AC代码:

    #include <bits/stdc++.h>
    using namespace std;
    int v,n,t;
    int w[1010],c[1010];
    int a[110][1010]/*记录组别所对应的i,a[p][0]为当前组别总数*/,f[1010];
    int main()
    {
    	ios::sync_with_stdio(0);
    	cin.tie(0);
    	cout.tie(0);
    	cin>>v>>n;
    	for(int i=1;i<=n;i++)
    	{
    		int p;
    		cin>>c[i]>>w[i]>>p;
    		a[p][++a[p][0]]=i;//记录组别 
    		t=p>t?p:t;//最大组号 
    	}
    	for(int k=1;k<=t;k++)//遍历组别
    	{
    		for(int j=v;j>=0;j--)//01背包写法,保证只取一个
    		{
    			for(int i=1;i<=a[k][0];i++)//遍历组内每个物品
    			{
    				if(j>=c[a[k][i]])//相当于01pack中j循环的 j>=c[i],因为在上面无法判断所以移下来了
    				{
    					f[j]=max(f[j],f[j-c[a[k][i]]]+w[a[k][i]]);
    				} 
    			}
    		}
    	}
    	cout<<f[v];
    	return 0;
    }
    

    这玩意卡了我一个上午 (抱头蹲防)


    暂时就先总结到这里

    (会时不时更新的)

  • 相关阅读:
    Tomcat建立会话流程
    springboot异步
    Optional类
    springboot项目打包成docker镜像
    第三章—Java NIO编程:(7)群聊系统
    第一章:(1)MongoDB简介与安装
    I第二章:(2)Eureka 服务注册与发现
    第一章:(2)MongoDB基本操作
    I第二章:(1)Eureka 服务注册与发现
    I第一章:(3)REST 微服务案例
  • 原文地址:https://www.cnblogs.com/IzayoiMiku/p/12699173.html
Copyright © 2020-2023  润新知