背包模型 笔记

4 篇文章 0 订阅

01背包

朴素版01背包

cin >> n >> m;
f[0][0] = 0;
for(int i = 1; i <= n; i ++)
{
	for(int j = 0; j <= m; j ++)
	{
		f[i][j] = f[i - 1][j];//第i个物品不选
		if(j - v[i] >= 0)
		{
			f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);//选第i个物品
		}
	}
}

cout << f[n][m];

一维

cin >> n >> m;
for(int i = 1; i <= n; i ++)cin >> v[i] >> w[i];
//初始化
//一维的要f[0~m]都是0
//如果要恰好背包容量是m的话,要f[0] = 0, f[1~m] = 负无穷
for(int i = 1; i <= n; i ++)
{
	for(int j = m; j >= v[i]; j --)
	{
		f[j] = max(f[j], f[j - v[i]] + w[i]);
	}
}

cout << f[m];

01背包方案数

f[0] = 1;
for(int i = 1; i <= n; i ++)
{
	cin >> v[i];
	for(int j = m; j >= v[i]; j --)
	{
		f[j] += f[j - v[i]];
	}
}
cout << f[m];

f[i][j]表示从前i中选,恰好等于j的方案数。

试着分析了一下“从前i中选,恰好等于j” 和“从前i中选,小于等于j”,集合划分居然一样,思来想去如果有哪里不同,那应该是初始化吧,前者初始化f[0][0]=1,f[0][1~m]=0,后者初始化f[0][0~m]=1,试了一下输出f[n][m]-f[n][m-1]过了,应该是这样的没错。

完全背包

朴素版

f[i][j]=max(f[i-1][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w,f[i-1][j-3v]+3w)+...

f[i][j-v]=max(f[i-1][j-v],f[i-1][j-2v]+v,f[i-1][j-3v]+2w)+...

f[i][j-v]+w正好就是后面那一部分

cin >> n >> m;
for(int i = 1; i <= n; i ++)cin >> v[i] >> w[i];
for(int i = 1; i <= n; i ++)
{
	for(int j = 1; j <= m; j ++)
	{
		f[i][j] = f[i - 1][j];
		if(j >= v[i])f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
	}
}

cout << f[n][m];

一维

cin >> n >> m;
for(int i = 1; i <= n; i ++)cin >> v[i] >> w[i];
for(int i = 1; i <= n; i ++)
{
	for(int j = v[i]; j <= m; j ++)
	{
		f[j] = max(f[j], f[j - v[i]] + w[i]);
	}
}

cout << f[m];

装满背包的方案数 

f[0] = 1;
for(int i = 1; i <= n; i ++)
{
    for(int j = v[i]; j <= m; j ++)
    {
        f[j] += f[j - v[i]];
    }
}

多重背包

二进制优化

每组有s个v和w相同的物品

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'

using namespace std;

typedef pair<int, int> PII;
typedef long long ll;
typedef long double ld;

const int N = 10010;

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

int main()
{
	IOS
	cin >> n >> m;
	for(int i = 0; i < n; i ++)
	{
		int a, b, s;
		cin >> a >> b >> s;
		for(int k = 1; k <= s; k <<= 1)
		{
			v[++ cnt] = a * k;
			w[cnt] = b * k;
			s -= k;
		}
		if(s)v[++ cnt] = a * s, w[cnt] = b * s;
	}
	
	for(int i = 1; i <= n; i ++)
		for(int j = m; j >= v[i]; j --)
			f[j] = max(f[j], f[j - v[i]] + w[i]);
			
	cout << f[m];
	
	return 0;
}

 单调队列优化(滑动窗口优化)

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'
 
using namespace std;
 
typedef pair<int, int> PII;
typedef long long ll;

const int N = 20010;

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

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

分组背包

每组选一个,每个v和w不同

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

二维费用背包

假设每个物品价值为1

for(int i = 1; i <= n; i ++)
{
	for(int j = m1; j >= v1[i]; j --)
	{
		for(int k = m2; k >= v2[i]; k --)
		{
			f[j][k] = max(f[j][k], f[j - v1[i]][k - v2[i]] + 1);
		}
	}
}

求容量至少为m1、m2时价值最小值:

f[i][j][k]表示从前i个物品中选,体积1>=j,体积2>=k的最小价值

不选第i个:f[i-1][j][k]

选第i个:f[i-1][j-v1][k-v2] + w

此时j-v1是可以小于0的,取max(0, j-v1)  k-v2同理

初始化:f[0][0][0] = 0, f[0][j][k] = 正无穷

cin >> m1 >> m2 >> n;
for(int i = 0; i <= m1; i ++)
{
	for(int j = 0; j <= m2; j ++)
	{
		f[i][j] = 2e9;
	}
}
f[0][0] = 0;

for(int i = 1; i <= n; i ++)
{
	int v1, v2, w;
	cin >> v1 >> v2 >> w;
	for(int j = m1; j >= 0; j --)
	{
		for(int k = m2; k >= 0; k --)
		{
			f[j][k] = min(f[j][k], f[max(0, j - v1)][max(0, k - v2)] + w);
		}
	}
}
cout << f[m1][m2];

f[i][j]总结三种情况:

体积最多为j的:一般背包问题

体积恰好为j的: f[0][0] = 0,其它的初始化为正无穷或负无穷

体积至少为j的::f[0][0] = 0,其它的初始化为正无穷或负无穷,与第二个不同的地方是j-v可以为负数

不合法状态初始化为正无穷或负无穷,初始化不合法状态是为了不使用这个值
 

有依赖的背包问题

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'
 
using namespace std;
 
typedef pair<int, int> PII;
typedef long long ll;

const int N = 110;

int n, m;
int v[N], w[N];
int h[N], e[N], ne[N], idx;
int f[N][N];//根节点为i的子树,背包容量最大为j
//朴素写法是f[u][i][j],根节点为u的子树,从前i个孩子中选,背包容量最大为j 

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

void dfs(int u)
{
	for(int i = h[u]; i != -1; i = ne[i])
		dfs(e[i]);
		
	for(int i = v[u]; i <= m; i ++)//先加上根节点的值
		f[u][i] = w[u];
		
	for(int i = h[u]; i != -1; i = ne[i])//这里不是正经意义上的从前i个中选,顺序不是1、2、3、4、5这种
	{
		int son = e[i];
		for(int j = m; j >= v[u]; j --)//分组背包倒着循环,用的上一层的
		{
			for(int k = 0; k <= j - v[u]; k ++)//分给孩子的容量不能占用跟根节点的
			{//j - v[u] + 1种方案,选一种
				f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
			}
		}
	}
}

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

贴一下另一种写法。其实把每棵树选子节点的过程看成一次独立的分组背包,最后再加上树的根节点会更好理解。 

#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'
#define fixed(s) fixed<<setprecision(12)<<s
//#define int long long

using namespace std;

typedef pair<int, int> PII;
typedef pair<double, double> PDD;
typedef long long ll;
typedef pair<ll, ll> PLL;

const int N = 110;

int n, m, root;
int v[N], w[N];
int f[N][N];
vector<int> g[N];

void dfs(int u)
{
	for(auto son : g[u])
	{
		dfs(son);
		
		for(int j = m - v[u]; j >= 0; j --)//从m开始枚举也行,但因为用不到,去掉v[u]这个大小会更快一点
		{
			for(int k = 0; k <= j; 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];
	for(int i = 0; i < v[u]; i ++)f[u][i] = 0;
}

int main()
{
	IOS
	cin >> n >> m;
	for(int i = 1; i <= n; i ++)
	{
		int p;
		cin >> v[i] >> w[i] >> p;
		if(p == -1)root = i;
		else g[p].push_back(i);
	}
	
	dfs(root);
	
	cout << f[root][m];
	
	return 0;
}

背包问题求方案数

01背包

朴素版

从前i个物品中选,背包容量最多为j的最大值方案数

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'
 
using namespace std;
 
typedef pair<int, int> PII;
typedef long long ll;

const int N = 1010, mod = 1e9 + 7;

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

int main()
{
	IOS
	cin >> n >> m;
	for(int i = 0; i < N; i ++)g[i][0] = g[0][i] = 1;
	//从前i个物品中选,背包容量为0 从前0个物品中选,背包容量为i 
	
	for(int i = 1; i <= n; i ++)
	{
		cin >> v[i] >> w[i];
		for(int j = 0; j <= m; j ++)
		{
			f[i][j] = f[i - 1][j];
			g[i][j] = g[i - 1][j];
			if(j >= v[i])
			{
				f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
				if(f[i - 1][j - v[i]] + w[i] > f[i - 1][j])g[i][j] = g[i - 1][j - v[i]];
				else if(f[i - 1][j - v[i]] + w[i] == f[i - 1][j])
				{
					g[i][j] = (g[i - 1][j] + g[i - 1][j - v[i]]) % mod;
				}
			}
		}
	}
	
	cout << g[n][m];
	
	return 0;
}

一维

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'
 
using namespace std;
 
typedef pair<int, int> PII;
typedef long long ll;

const int N = 1010, mod = 1e9 + 7;

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

int main()
{
	IOS
	cin >> n >> m;
	for(int i = 0; i < N; i ++)g[i] = 1;
	//从前i个物品中选,背包容量为0 从前0个物品中选,背包容量为i 
	
	for(int i = 1; i <= n; i ++)
	{
		cin >> v[i] >> w[i];
		for(int j = m; j >= v[i]; j --)
		{
			if(f[j - v[i]] + w[i] > f[j])g[j] = g[j - v[i]];
			else if(f[j - v[i]] + w[i] == f[j])
			{
				g[j] = (g[j] + g[j - v[i]]) % mod;
			}
			f[j] = max(f[j], f[j - v[i]] + w[i]);
		}
	}
	
	cout << g[m];
	
	return 0;
}

朴素版

从前i个物品中选,背包容量恰好为j的最大值方案数

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'
 
using namespace std;
 
typedef pair<int, int> PII;
typedef long long ll;

const int N = 1010, mod = 1e9 + 7;

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

int main()
{
	IOS
	cin >> n >> m;
	f[0][0] = 0;
	for(int i = 1; i < N; i ++)f[0][i] = -2e9;
	g[0][0] = 1;//背包容量恰好为j的写法 
	
	for(int i = 1; i <= n; i ++)
	{
		cin >> v[i] >> w[i];
		for(int j = 0; j <= m; j ++)
		{
			f[i][j] = f[i - 1][j];
			g[i][j] = g[i - 1][j];
			if(j >= v[i])
			{
				f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
				if(f[i - 1][j - v[i]] + w[i] > f[i - 1][j])g[i][j] = g[i - 1][j - v[i]];
				else if(f[i - 1][j - v[i]] + w[i] == f[i - 1][j])
				{
					g[i][j] = (g[i - 1][j] + g[i - 1][j - v[i]]) % mod;
				}
			}
		}
	}
	
	int maxn = 0;
	for(int i = 0; i <= m; i ++)maxn = max(maxn, f[n][i]);
	int ans = 0;
	for(int i = 0; i <= m; i ++)
	{
		if(f[n][i] == maxn)
		{
			ans = (ans + g[n][i]) % mod;
		}
	}
	cout << ans;
	
	return 0;
}

一维 

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'
 
using namespace std;
 
typedef pair<int, int> PII;
typedef long long ll;

const int N = 1010, mod = 1e9 + 7;

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

int main()
{
	IOS
	cin >> n >> m;
	for(int i = 1; i < N; i ++)f[i] = -2e9;
	g[0] = 1;//背包容量恰好为j的写法 
	
	for(int i = 1; i <= n; i ++)
	{
		cin >> v[i] >> w[i];
		for(int j = m; j >= v[i]; j --)
		{
			if(f[j - v[i]] + w[i] > f[j])g[j] = g[j - v[i]];
			else if(f[j - v[i]] + w[i] == f[j])
			{
				g[j] = (g[j] + g[j - v[i]]) % mod;
			}
			f[j] = max(f[j], f[j - v[i]] + w[i]);
		}
	}
	
	int maxn = 0;
	for(int i = 0; i <= m; i ++)maxn = max(maxn, f[i]);
	int ans = 0;
	for(int i = 0; i <= m; i ++)
	{
		if(f[i] == maxn)
		{
			ans = (ans + g[i]) % mod;
		}
	}
	cout << ans;
	
	return 0;
}

背包问题求具体方案

01背包求字典序最小的方案

正常求方案要倒着推,但不是字典序最小的,从后往前遍历物品即可

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'
 
using namespace std;
 
typedef pair<int, int> PII;
typedef long long ll;

const int N = 1010;

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

int main()
{
	IOS
	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 t = m;
	for(int i = 1; i <= n; i ++)
	{
		if(t >= v[i])
		{
			if(f[i][t] == f[i + 1][t - v[i]] + w[i])
			{
				cout << i << ' ';
				t -= v[i];
			}
		}
	}
	
	return 0;
}

------------------------------------此后为例题-----------------------------------------------

能量石 

岩石怪物杜达生活在魔法森林中,他在午餐时收集了 N块能量石准备开吃。

由于他的嘴很小,所以一次只能吃一块能量石。

能量石很硬,吃完需要花不少时间。

吃完第 i 块能量石需要花费的时间为 Si秒。

杜达靠吃能量石来获取能量。

不同的能量石包含的能量可能不同。

此外,能量石会随着时间流逝逐渐失去能量。

第 i块能量石最初包含 Ei单位的能量,并且每秒将失去 Li 单位的能量。

当杜达开始吃一块能量石时,他就会立即获得该能量石所含的全部能量(无论实际吃完该石头需要多少时间)。

能量石中包含的能量最多降低至 0。

请问杜达通过吃能量石可以获得的最大能量是多少?

输入格式

第一行包含整数 T,表示共有 T 组测试数据。

每组数据第一行包含整数 N,表示能量石的数量。

接下来 N 行,每行包含三个整数 Si,Ei,Li。

输出格式

每组数据输出一个结果,每个结果占一行。

结果表示为 Case #x: y,其中 x 是组别编号(从 11 开始),y 是可以获得的最大能量值。

数据范围

1≤T≤10,
1≤N≤100,
1≤Si≤100,
1≤Ei≤1e5,
0≤Li≤1e5

输入样例:
3
4
20 10 1
5 30 5
100 30 1
5 80 60
3
10 4 1000
10 3 1000
10 8 1000
2
12 300 50
5 200 0
输出样例:
Case #1: 105
Case #2: 8
Case #3: 500

 i,j两块石头

先取i获得的能量:E_{i}+E_j-s_i*L_j

先取j获得的能量:E_{i}+E_j-s_j*L_i

sort一下先后顺序就排好了,之后01背包即可

注意状态表示为“背包容量恰好为j”,转移时f[j-v]可能在j-v-1或者更以前时就等于f[j-v]了,并不是j-v是最优的,j-v越大损失的也越大。

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'
 
using namespace std;
 
typedef pair<int, int> PII;
typedef long long ll;

const int N = 110, M = 10010;

int n;
int f[M];
struct Node
{
	int s, e, l;
	bool operator<(const Node &t) const
	{
		return s * t.l < t.s * l;
	}
}a[N];

int main()
{
	IOS
	int _;
	cin >> _;
	for(int T = 1; T <= _; T ++)
	{
		cin >> n;
		int m = 0;
		for(int i = 1; i <= n; i ++)
		{
			int s, e, l;
			cin >> s >> e >> l;
			a[i] = {s, e, l};
			m += s;
		}
		sort(a + 1, a + 1 + n);
		
		memset(f, -0x3f, sizeof f);
		f[0] = 0;
		
		for(int i = 1; i <= n; i ++)
		{
			int s = a[i].s, e = a[i].e, l = a[i].l;
			for(int j = m; j >= s; j --)
			{
				f[j] = max(f[j], f[j - s] + max(0, e - (j - s) * l));
			}
		}
		
		int res = 0;
		for(int i = 1; i <= m; i ++)res = max(res, f[i]);
		
		cout << "Case #" << T << ": " << res << endl;
	}
	
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值