背包九讲笔记

good link:

第一讲 01背包问题

f [ i ][ v ]表示前i个物品,放入容量为v的背包中可以获得的最大价值
状态转移方程:
f[i][v] = max( f[i-1][v], f[i-1][v-c[i]] + w[i])
优化空间复杂度:

for i = 1.. N
	for v = V..0
		f[v] = max(f[v],f[v-c[i]]+w[i])

进一步优化:

for i = 1.. N
	for v = V..cost
		f[v] = max(f[v],f[v-c[i]]+w[i])
初始化的细节问题

在求解最优解背包问题中有两种问法

①恰好装满背包的最优解
②没有要求必须把背包装满

这两种情况的区别就是在初始化的时候做些不同的处理

对于第一种问法,在初始化时除了f[0]=0,其余f[1…V]均设为-∞
对于第二种问法,只要将f[0…V]全设为0

一个常数优化
由于只需要最后f[v]的值,倒推前一个物品,其实只要知道f[v-w[n]]即可。以此类推,对以第j个背包,其实只需要知道到f[v-sum{w[j…n]}]即可,即代码中的

for i=1..N
    for v=V..0

可以改成

for i=1..n
    bound=max{V-sum{w[i..n]},c[i]}
    for v=V..bound

这对于V比较大时是有用的。

第二讲 完全背包问题

例题:https://www.acwing.com/problem/content/3/
该问题与01背包不同的是每种物品都可以无限件可用。
如果按照解01背包时的思路,仍可以得出状态转移方程:
f[i][v]=max{f[i-1][v-kc[i]]+kw[i]|0<=k*c[i]<=v}

01背包一维代码上的改进后的 code :

#include <bits/stdc++.h>

using namespace std;
#define ll long long 
const int N=1100;

int n,m;
int v[N];
int w[N];
int f[N];



int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&v[i],&w[i]);
	}
	memset(f,0,sizeof(f));
	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=v[i];j--)
		{
			for(int k=0;k*v[i]<=j;k++)
				f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
			
		}
	}
	printf("%d\n",f[m]);
	
	
	
	return 0;
}

另一种 : 二维实现code:

#include <bits/stdc++.h>

using namespace std;
#define ll long long 
const int N=1100;

int n,m;
int v[N];
int w[N];
int f[N][N];



int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&v[i],&w[i]);
	}
	memset(f,0,sizeof(f));
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<=m;j++)
		{
			
			if(j<v[i])
				f[i][j]=f[i-1][j];
			else
			{
				f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]);
			}
		}
		//一维代码  ,和01背包的一维代码不同之处在j遍历的方向变了
		/*
		for(int j=v[i];j<=m;j++)
			f[j]=max(f[j],f[j-v[i]]+w[i]);
		*/
	}
	printf("%d\n",f[n][m]);
	
	
	
	return 0;
}

第三讲 多重背包问题

与完全背包不同的地方就是每种背包的个数有个上限。

例题:多重背包问题 I

Code :代码类似完全背包的第一份代码

#include <bits/stdc++.h>

using namespace std;
#define ll long long 
const int N=1100;

int n,m;
int v[N];
int w[N];
int f[N];
int mx[N];



int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d%d",&v[i],&w[i],&mx[i]);
	}
	memset(f,0,sizeof(f));
	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=v[i];j--)
		{
			for(int k=1;k<=mx[i]&&k*v[i]<=j;k++)
				f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
			
		}
	}
	printf("%d\n",f[m]);
	
	
	
	return 0;
}

例题二:多重背包问题 II
二进制优化:将每种物品的上限s,将其传化成01背包,就是将s拆成log(s)向上取整个(kv[i],kw[i])的背包,然后这个几个背包直接的组合可以表示0~s的所有个数

s=7, 拆成 k=1,2,4三种背包,这三种背包可以表示0~7的每个数
0= 0
1=1;
2=2
3=1+2
4=4
5=1+4
6=2+4
7=1+2+4
s=13,log2(13)=4(向上取整)拆成k=1,2,4,6(最后一个6,是通过13-1-2-4的来的)
因为1,2,4,可以表示0~7,要使能表示的只有0~13,故最后一种为13-7=6;
拆完之后,就是01背包了
复杂度:n*log2(s)+n*log2(s)*m =O(n*m*log(s))
code:

#include <bits/stdc++.h>

using namespace std;
#define ll long long 
const int N=2100;

int n,m;
int v,w,s;
int f[N];

struct Good
{
	int v,w;
};
vector<Good> goods;

int main()
{
    goods.clear();
	scanf("%d%d",&n,&m);
	memset(f,0,sizeof(f));
	for(int i=1;i<=n;i++)
	{
	    scanf("%d%d%d",&v,&w,&s);
		for(int k=1;k<=s;k*=2)
		{
			s-=k;
			goods.push_back({k*v,k*w});
		}
		if(s>0)
		{
			goods.push_back({s*v,s*w});
		}
	}
	for(auto good: goods)
	{
		for(int j=m;j>=good.v;j--)
			f[j]=max(f[j],f[j-good.v]+good.w);
	}
	
	printf("%d\n",f[m]);
	
	
	
	return 0;
}

例题三: 多重背包问题 III
需要利用多重背包的单调队列优化方法

这个优化最开始是从这个代码中来的:(也就是多重背包的第一份代码里的)

for(int i=1;i<=n;i++)
	{
		scanf("%d%d%d",&v[i],&w[i],&mx[i]);
		for(int j=m;j>=v[i];j--)
		{
			for(int k=1;k<=mx[i]&&k*v[i]<=j;k++)
			{
				f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
			}
		}
	}

优化的部分就是内部的两层for循环。此时可以把物品种类数n,背包容量m,某件物品的大小v[i],价值w[i],数量mx[i]都看成已知。
以m=20 ,v[i]=4,s=5,为例(s取5是为了好叙述,若s<5的话,后面单调队列中得考虑窗口的大小)
优化根据递推公式f[j]=max(f[j],f[j-kv[i]]+kw[i]),发现每次f[j]更新是与f[j-k*v[i]]有关,故将m对于v[i]取余,余数相同的划分到同一个集合,共有v[i]个集合。这样f[j]的更新只在与j同余的数的集合中进行。

当遍历到j时,以j=20,和j=19为例,f[20]与f[19]的更新所关联的f,同时f[20]=max(f[20]+0w,f[16]+1w,f[12]+2*w, ···)。写个表

f[ ]=201612840
+k*w0*w1*w2*w3*w4*w5*w
f[ ]=19151173
+k*w0*w1*w2*w3*w4*w

当遍历到j=16,和j15时,f[16]和f[15]的更新所关联的f为:

f[ ]=1612840
+k*w0*w1*w2*w3*w4*w
f[ ]=151173
+k*w0*w1*w2*w3*w

可以发现,当遍历到j=20时,只会更新f[20], 而f[16],f[12]等都会被更新,也就是说当遍历到j=16时,f[20]的更新不会影响到f[16]。
同理,f[12],f[8],f[4],f[0]的更新,都是相互独立的
不同的地方在,(这里将已经更新过的f[0],f[4],···,都记为F[0],F[4],···,
而原始的用小f表示)
F [ 0 ] = m a x ( f [ 0 ] + 0 ∗ w ) F [ 4 ] = m a x ( f [ 0 ] + 1 ∗ w ,      f [ 4 ] + 0 ∗ w ) F [ 8 ] =    m a x ( f [ 0 ] + 2 ∗ w ,      f [ 4 ] + 1 ∗ w ,      f [ 8 ] + 0 ∗ w ) F [ 12 ] = m a x ( f [ 0 ] + 3 ∗ w ,      f [ 4 ] + 2 ∗ w ,      f [ 8 ] + 1 ∗ w ,      f [ 12 ] + 0 ∗ w ) ⋮ F[0]=max(f[0]+0*w )\\ F[4]=max(f[0]+1*w,\;\;f[4]+0*w)\\ F[8]=\;max(f[0]+2*w,\;\;f[4]+1*w,\;\;f[8]+0*w)\\ F[12]=max(f[0]+3*w,\;\;f[4]+2*w,\;\;f[8]+1*w,\;\;f[12]+0*w)\\ \vdots F[0]=max(f[0]+0w)F[4]=max(f[0]+1w,f[4]+0w)F[8]=max(f[0]+2w,f[4]+1w,f[8]+0w)F[12]=max(f[0]+3w,f[4]+2w,f[8]+1w,f[12]+0w)

对于F[12],如何找出中最大的呢,
将 f [ 0 ] + 3 w , f [ 4 ] + 2 w , f [ 8 ] + 1 w , f [ 12 ] + 0 w , 都 减 去 3 w 得 到 f [ 0 ] − 0 w , f [ 4 ] − 1 w , f [ 8 ] − 2 w , f [ 12 ] − 3 w 可 以 发 现 F [ 8 ] = ( f [ 0 ] − 0 w , f [ 4 ] − 1 w , f [ 8 ] − 2 w ) + 2 w F [ 4 ] = ( f [ 0 ] − 0 w , f [ 4 ] − 1 w ) + w F [ 0 ] = ( f [ 0 ] − 0 w ) + w 因 此 我 们 只 要 求 出 ( f [ 0 ] − 0 w , f [ 4 ] − 1 w , f [ 8 ] − 2 w , f [ 12 ] − 3 w ) 的 [ 1 , 1 ] , [ 1 , 2 ] , [ 1 , 3 ] , [ 1 , 4 ] 的 最 大 值 , 就 可 以 求 出 F [ 0 ] , F [ 4 ] , F [ 8 ] , F [ 12 ] 了 将f[0]+3w,f[4]+2w,f[8]+1w,f[12]+0w,都减去3w得到\\ f[0]-0w,f[4]-1w,f[8]-2w,f[12]-3w\\ 可以发现\\ F[8]=(f[0]-0w,f[4]-1w,f[8]-2w)+2w\\ F[4]=(f[0]-0w,f[4]-1w)+w\\ F[0]=(f[0]-0w)+w\\ 因此我们只要求出(f[0]-0w,f[4]-1w,f[8]-2w,f[12]-3w)的[1,1],[1,2],[1,3],[1,4]的最大值,\\就可以求出F[0],F[4],F[8],F[12]了 f[0]+3w,f[4]+2w,f[8]+1w,f[12]+0w,3wf[0]0w,f[4]1w,f[8]2w,f[12]3wF[8]=(f[0]0w,f[4]1w,f[8]2w)+2wF[4]=(f[0]0w,f[4]1w)+wF[0]=(f[0]0w)+w(f[0]0w,f[4]1w,f[8]2w,f[12]3w)[1,1],[1,2],[1,3],[1,4]F[0],F[4],F[8],F[12]
这里需要利用单调队列,在求F[12]的过程中,并且把F[0],F[4],F[8]都求了

为此,还特意回去学了下单调队列
单调队列就是一个固定长度的窗口在数列上移动,也就3种操作:
遍历到a[i]时,
1.首先,要将超出窗口大小的元素弹去:若队列中的首末位置的元素跨度大于窗口的大小,则弹掉队首元素,直到小于等于窗口的大小
2.若队列为空,入队
3.队列非空,
 若 a[i]小于队尾元素,那么就弹掉队尾元素,一直弹到a[i]大于队尾元素
 若a[i]大于队尾元素,直接放入队尾
注:等于的情况可以根据题目而定

f [ 0 ] − 0 w , f [ 4 ] − 1 w , f [ 8 ] − 2 w , f [ 12 ] − 3 w f[0]-0w,f[4]-1w,f[8]-2w,f[12]-3w f[0]0w,f[4]1w,f[8]2w,f[12]3w这4个数,分别用编号0,4,8,12表示,
这样就可求出同时F[0],F[4],F[8],F[12]的值就是(0,),(0,4),(0,4,8),(0,4,8,12)的最大值加上对应的k*w

具体实现code:

单调队列中窗口大小是 物品的上限mx[i]*物品体积v[i]
hh,tt分别表示队列的首末位置
q[hh]存最大值

cin >> n >> m;
for(int i=0;i<n;i++)
{
	int c,w,s;
	cin>>c>>w>> s;
	memcpy(g,f,sizeof(f));
	for(int j=0;j<c;j++)
	{
		int hh=0,tt=-1;
		for(int k=j;k<=m;k+=c)
		{
			f[k]=g[k];
			if(hh<=tt&&k-s*c>q[hh]) hh++;
			if(hh<=tt) f[k]=max(f[k],g[q[hh]]+(k-q[hh])/c*w);
			while(hh<=tt&&g[q[tt]]-(q[tt]-j)/c*w<=g[k]-(k-j)/c*w) tt--;
			q[++tt]=k;
		}
	}
}
cout<<f[m]<<endl;

例题:多重背包问题 III
利用单调队列优化
代码:

#include <bits/stdc++.h>

using namespace std;
#define ll long long 
const int N=2e4+100;

int n,m;
int q[N],f[N],g[N];

int main()
{
	cin >> n >> m;
	for(int i=0;i<n;i++)
	{
		int c,w,s;
		cin>>c>>w>> s;
		memcpy(g,f,sizeof(f));
		
		for(int j=0;j<c;j++)
		{
			int hh=0,tt=-1;
			for(int k=j;k<=m;k+=c)
			{
				f[k]=g[k];
				if(hh<=tt&&k-q[hh]>s*c) hh++;
				if(hh<=tt) f[k]=max( f[k],g[q[hh]]+(k-q[hh])/c*w ) ;
				while(hh<=tt&&g[q[tt]]-(q[tt]-j)/c*w<=g[k]-(k-j)/c*w)  tt--; // 将比k大的都弹掉,hh->tt是递减的
				q[++tt]=k;
			}
		}
	}
	cout<<f[m]<<endl;
	
	
	return 0;
}

第四讲 混合背包问题

就是含01背包,完全背包,多重背包的混合型题目
例题:混合背包问题
code:

#include <bits/stdc++.h>

using namespace std;
#define ll long long
const int N=1005;

struct Thing
{
	int kind;
	int v,w;
};

vector<Thing> things;
int f[N];
int main()
{
	int n,m;
	int v,w,s;
	cin >> n >> m;
	for(int i=0;i<n;i++)
	{
		scanf("%d%d%d",&v,&w,&s);
		if(s<0) things.push_back({-1,v,w});
		else if(s==0) things.push_back({0,v,w});
		else
		{
			for(int k=1;k<=s;k<<=1)
			{
				s-=k;
				things.push_back({-1,k*v,k*w});
			}
			if(s>0)
				things.push_back({-1,s*v,s*w});
		}
		
	}
	for(auto thing: things)
	{
		if(thing.kind==0)
			for(int i=thing.v;i<=m;i++) f[i]=max(f[i],f[i-thing.v]+thing.w);
		else if(thing.kind==-1)
		{
			for(int i = m;i>= thing.v;i--) f[i]=max(f[i], f[i-thing.v]+thing.w);
		}
	}
	cout<<f[m]<<endl;

	return 0;
}

第五讲 二维费用背包问题

例题:二维费用的背包问题
在01背包的基础上加上一层循环
code :

#include <bits/stdc++.h>

using namespace std;
#define ll long long
const int N=105;


int f[N][N]; //体积为i,重量为j的最大价值
int N, V, M, v, m , w;
int main()
{
	cin>> N >> V >> M;
	for(int i=0;i<N;i++)
	{
		cin >> v >> m >> w;
		for(int j=V;j>=v;j--)
		{
			for(int k=M;k>=m;k--)
				f[j][k]=max(f[j][k],f[j-v][k-m]+w);
		}
	}
	cout<< f[V][M] << endl;
	
	

	return 0;
}

第六讲 分组背包问题

例题:分组背包问题
就是有n组物品,每组物品中最多只能拿一件物品,类似于01背包,在遍历j=m->0时,对于f[j]的更新,直接遍历每组物品中的s件物品,这样做时为了使f[j]的代表的方案中最多有一件该组的物品;
如果将s放外层循环,j=m->0放内层,f[j]所代表的方案有可能就包含该组2件及2件以上的物品。

code:

#include <bits/stdc++.h>

using namespace std;
const int N=110;

int n, m, s;
int f[N],v[N],w[N];

int main()
{
    cin >> n >> m;
    for(int i=0;i<n;i++)
    {
        cin >> s;
        for(int j=0;j<s;j++)
        {
            scanf("%d%d",&v[j],&w[j]);
        }
        for(int j=m;j>=0;j--)
        {
            for(int k=0;k<s;k++)
            {
                if(j>=v[k])
                    f[j]=max(f[j],f[j-v[k]]+w[k]);
            }
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

第七讲 有依赖的背包问题

例题:有依赖的背包问题
有依赖的背包,类似于选课的时候,有的课程有先修课,先修课就是这种依赖关系;
这类题目将这种依赖关系化成树的形状,若选结点i的某个儿子,就必须选结点i;
这道题就是树形dp中的某类题目
如果懂树形dp,对于这个的理解应该没什么问题;如果不太了解,理解这个问题也问题不大;
定义:
f[i][j]表示选结点i,体积为j,以i为根的子树的最大价值;
那么对于如何更新节点i(f[i][j])?
利用递归,由下往上更新
依次遍历i的儿子,每个儿子都有选与不选两种状态,而选这种状态包含了(只选这个儿子,选以这个儿子为根的子树),这个儿子的情况与结点i的更新完成后的情况是一样的

code:

#include <bits/stdc++.h>

using namespace std;
#define ll long long
const int N=105;

int h[N], e[N], ne[N], idx ;
int n,m;
int v[N],w[N],f[N][N], p;

void add(int u, int v)
{
	e[idx]=v, ne[idx]=h[u], h[u]=idx++;
}

void dfs(int u)
{
	for(int i=h[u];~i;i=ne[i])
	{
		int son=e[i];
		dfs(son);
		for(int j=m-v[u];j>=0;j--)
			for(int k=0;k<=j;k++) 
				// j放外层,f[u][j]的j由大到小,不会影响更新结果;
			//要使f[u][j-k]的值为原先的值,故j-k从大到小,即k从小到大
				f[u][j]=max(f[u][j],f[u][j-k]+f[son][k]); 
	}
	for(int i=m;i>=v[u];i--) f[u][i]=f[u][i-v[u]]+w[u];// 由于这里是必选结点u,就不需要去最大值了
	for(int i=0;i<v[u];i++) f[u][i]=0;
}


int main()
{
	memset(h,-1,sizeof h);
	idx=0;
	cin >> n >> m;
	int root;
	for(int i=1;i<=n;i++)
	{
		cin >> v[i] >> w[i] >> p;
		if(p==-1) root=i;
		else add(p,i);
	}
	dfs(root);
	cout << f[root][m] <<endl;
	return 0;
}

第八讲 背包问题求方案数

例题:背包问题求方案数

code:

#include <bits/stdc++.h>

using namespace std;
#define inf 0x3f3f3f3f
#define ll long long
const int mod = 1e9+7;
const int N=1005;


int n, m;
int v, w;
int f[N],g[N];// f[j]表示空间恰为j的最大值,g[j]表示f[j]对应的方案数

int main()
{
	cin >> n >> m;
	g[0]=1;//初始为1
	f[0]=0;
	for(int i=1;i<=m;i++) {
		f[i]=-inf;//保证恰好
		g[i]=0;
	}
	for(int i=0;i<n;i++)
	{
		cin>> v >> w;
		for(int j=m;j>=v;j--)
		{
			int t=max(f[j],f[j-v]+w);
			int s=0;// 暂存方案数
			if(t==f[j]) s+=g[j];
			if(t==f[j-v]+w) s+=g[j-v];
			s%=mod;
			f[j]=t;
			g[j]=s;
		}
	}
	int maxw=0;
	for(int i=0;i<=m;i++) maxw=max(maxw,f[i]);//枚举得到最优解
	int res=0;
	for(int i=0;i<=m;i++)
	{
		if(maxw==f[i])// 将最优解的方案累加
		{
			res=(res+g[i])%mod;
		}
	}
	cout<<res<<endl;
	
	return 0;
}

第九讲 背包问题求具体方案

例题:背包问题求具体方案
与逆着打印路径类似

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N=1010;

int n, m;
int v[N], w[N],f[N][N];

int main()
{
    cin >> n >> m;
    for(int i=1;i<=n;i++) cin>> v[i] >> w[i];
    
    for(int i=n;i>=1;i--)
        for(int j=0;j<=m;j++)
        {
            f[i][j]=f[i+1][j];
            if(j>=v[i]) f[i][j]=max(f[i][j],f[i+1][j-v[i]]+w[i]);
        }
        
    int vol=m;
    for(int i=1;i<=n;i++)
        if(vol>=v[i]&&f[i][vol]==f[i+1][vol-v[i]]+w[i])
        {
            cout<< i << ' ';
            vol-=v[i];
        }
    return 0;
}

论述下贪心与动态规划的区别:(略略略略略)
图片来源:https://wenku.baidu.com/view/cacb5d1b2b160b4e767fcfee.html?from=search
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值