动态规划背包问题

关于背包问题的学习笔记

目录

01背包

 分析

错误写法

正确写法

滚动数组

一维做法

完全背包

分析

普通做法

一维做法

 延伸

多重背包

分析

二进制分组优化

分组背包

分析

一维做法

二维背包

分析

单调队列

分析

多重背包

分析


01背包

例题:

 分析:

考虑前i个物品,总体积为j时分为两种情况:
        第i个物品没取,问题变成了考虑了前 i−1个物品,总体积为j时的情况;
        第i个物品取了,问题变成了考虑了前i −1个物品,总体积为j-v[i]时的情况; 

状态:用f[i][j]表示考虑了前i个物品,总体积为j时的最大收益。
转移: f[i][j]=max(f[i−1][j],f[i−1][j−v[i]]+w[i]),分别对应第i个物品没取和取了两种情况。

错误写法:

根据状态转移方程写出了这样的代码:

int n, m;
int f[1010][1010];
int v[1010], w[1010];
signed main()
{
	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[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
		}
	}
	cout << f[n][m] << endl;
	return 0;
}

在这个写法中,当j<=v[ii]时,数组没有被更新,因此发生错误

正确写法:

int n, m;
int f[1010][1010];
int v[1010], w[1010];
signed main()
{
	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 = 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 - 1][j - v[i]] + w[i]);
		}
	}
	cout << f[n][m] << endl;
	return 0;
}

滚动数组:

但是当数据过大时,使用二维数组占用的空间太大,由于在计算i行时只用到了i-1行,所以可以用一个g数组存i-1行的数据,循环滚动f和g

int f[1010], g[1010];
int v[1010], w[1010];
int main() {
	int 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 = 0; j <= m; j++) {
			if (j >= v[i]) g[j] = max(f[j], f[j - v[i]] + w[i]);
			else g[j] = f[j];
		}
		memcpy(f, g, sizeof(g));
	}
	cout << f[m] << endl;
	return 0;
}

一维做法:

状态:用f[i]表示总体积为i时的最大收益。
转移:f[j] = max(f[j], f[j - v[i]] + w[i])表示取前i个物品总体积为j时的最大情况

注意在枚举体积时,要从大向小枚举,若小到大枚举,则在枚举过程中可能会多次放入同一物品

int f[1010];
int v[1010], w[1010];
int main() {
	int 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] << endl;
	return 0;
}

完全背包

完全背包的区别就是每个物品可以拿无数次

分析:

考虑前i个物品,总体积为j时分为两种情况:
        第i个物品没取,问题变成了考虑了前 i−1个物品,总体积为j时的情况;
        第i个物品取了,问题变成了考虑了前i 个物品,总体积为j-v[i]时的情况; 

状态:用f[i][j]表示考虑了前i个物品,总体积为j时的最大收益。
转移: f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i])分别对应第i个物品没取和取了两种情况。

普通做法:

int n, m;
int f[1010][1010];
int v[1010], w[1010];
signed main()
{
	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 = 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]);
		}
	}
	cout << f[n][m] << endl;
	return 0;
}

一维做法:

因为完全背包就是在01背包的基础上每个物品可以拿无数次,因此在枚举体积时正向枚举,即可在体积允许的情况下,将多个同一物品放入背包。

int f[1010];
int v[1010], w[1010];
int main() {
	int 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] << endl;
	return 0;
}

 延伸:

如果要让背包里的物品体积正好是m,应该怎么做呢?

只需要把f数组初始化为负无限大,然后让f[0]=0,这样只有在体积刚好凑成的时候f才能数组才能被更新。

int f[1010];
int v[1010], w[1010];
int main() {
	int n, m;
	cin >> n >> m;
	memset(f, 128, sizeof(f));
	f[0] = 0;
	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] << endl;
	return 0;
}

多重背包:

分析:

第i种物品可以使用 l[i] 次,我们可以把它拆成 l[i]个物品,每个物品只能用一次;
然后就是个01背包问题啦!

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

但是当数据过大时,这种做法会超时,可以采用二进制分组优化

二进制分组优化

从1,2,…,2m中选一些数字相加,可以得出任意[0,2m+1)内的值,每个数字只能用一次;

利用这条结论,我们可以用二进制表示体积m,例如:

5=1+4
6=2+4
7=1+2+4
8=1+1+2+4

因此对于第i种物品,可以使用l[i]次,那么可以把它拆成O(logn)个物品,每个物品只能用一次;

int f[2010];
int v[2010], w[2010], l[2010];
int main() {
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++) { 
		cin >> v[i] >> w[i] >> l[i];
	}
	for (int i = 1; i <= n; i++) {
		int res = l[i];
		for (int k = 1; k <= res; res -= k, k *= 2) {
			for (int j = m; j >= v[i] * k; j--) {
				f[j] = max(f[j], f[j - v[i]*k] + k*w[i]);
			}
		}
		for (int j = m; j >= v[i] * res; j--)
			f[j] = max(f[j], f[j - res * v[i]] + res * w[i]);
	}
	cout << f[m] << endl;
	return 0;
}

分组背包:

分析:

把考虑了前i组物品以后,总体积为0,1,...,m时的最大收益都记下来;
考虑了前i组物品,总体积为j时分为两种情况:
第i组物品一个没取,问题变成了考虑了前 i−1组物品,总体积为j时的情况;
第i组物品取了,我们枚举取了其中的物品k,问题变成了考虑了前 i−1组物品,总体积为 j−v[k]时的情况;
状态:用f[i][j]表示考虑了前i组物品,总体积为j时的最大收益。
转移: f[i][j]= max (f[i-1][j], f[i-1][j-v[k]]+w[k])),其中c[i]表示第i组中物品的编号集合。
 

int n, m;
int f[1010][1010];
int v[1010], w[1010], a[1010];
vector<int>c[1010];
int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i] >> v[i] >> w[i];
		c[a[i]].push_back(i);
	}
	for (int i = 1; i <= 1000; i++) {
		for (int j = 0; j <= m; j++) {
			f[i][j] = f[i - 1][j];
			for (auto k : c[i]) {
				if (v[k] <= j)
					f[i][j] = max(f[i][j], f[i - 1][j - v[k]] + w[k]);
			}
		}
	}
	cout << f[1010][m] << endl;
	return 0;
}

一维做法:

int n, m;
int f[1010];
int v[1010], w[1010], a[1010];
vector<int>c[1010];
int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i] >> v[i] >> w[i];
		c[a[i]].push_back(i);
	}
	for (int i = 1; i <= 1000; i++) {
		for (int j = m; j; j--) {
			for (auto k : c[i]) {
				if (v[k] <= j)
					f[j] = max(f[j], f[j - v[k]] + w[k]);
			}
		}
	}
	cout << f[m] << endl;
	return 0;
}

二维背包:

分析:

把考虑了前i个物品以后,总体积为0,1,...,m,总体力为0,1,...k时的最大收益都记下来;
考虑了前i个物品,总体积为j,总体力为x时分为两种情况:
        第i个物品没取,问题变成了考虑了前 i−1个物品,总体积为j,总体力为x时的情况;
        第i个物品取了,问题变成了考虑了前 i−1个物品,总体积为 j−v[i],总体力为x-t[i]时的情况; 
状态:用f[i][i][x]表示考虑了前i个物品,总体积为j,总体力为x时的最大收益。
转移: f[i][j][x]=max(f[i−1][j][x],f[i−1][j−ν[i]][x−t[i]]+wi)

这里就不贴普通做法的三维数组做法了,直接贴二维

int n, m, k;
int v[1001], w[1001], t[1001];
int f[1001][1001];
int main() {
	cin >> n >> m >> k;
	for (int i = 1; i <= n; i++) {
		cin >> v[i] >> w[i] >> t[i];
	}
	for (int i = 1; i <= n; i++) {
		for (int j = m; j >= v[i]; j--) {
			for (int x=k; x >= t[i]; x--) {
				f[j][x] = max(f[j][x], f[j - v[i]][x - t[i]] + w[i]);
			}
		}
	}
	cout << f[m][k] << endl;
	return 0;
}

单调队列:

分析:

假设现在考虑到了第i天,对于第 j(j<i)个生物,如果 b[j]<b[i],那么之后第j个生物都不用考虑了(在它存在期间,永远被第i个生物压制) ;
用一个队列,按编号从小到大的顺序,存放到目前为止,哪些生物是需要被考虑的(有可能在未来的某一天成为攻击力最高的生物) ;
队列中的生物的攻击力是单调递减的,每次攻击力最高的生物,都在队首;

具体实现:

加入一个生物,队列末尾比它攻击力低的生物都不用考虑了;
把队列末尾比它攻击力低的生物删除以后,加入队列末尾;
考虑完一个位置后,如果队首生物从此消失了,删除队首;

int n, a[100010], b[100010], c[100010][2];
int main() {
	int n;
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) {
		scanf("%d %d", &a[i], &b[i]);
	}
	int k = 0, l = 1;
	for (int i = 1; i <= n; i++) {
		for (; k >= l && b[i] >= c[k][0]; --k);//删掉队尾攻击力小的
		c[++k][0] = b[i]; c[k][1] = a[i];
		printf("%d\n", c[l][0]);
		for (; k >= l && c[l][1] == i; ++l);//删掉队头已经超过时限的元素
	}
    return 0;
}

多重背包:

分析:

把考虑了前i种物品以后,总体积为0,1,…,m时的最大收益都记下来;
考虑了前i种物品,总体积为j时分为两种情况:

        第i种物品没取,问题变成了考虑了前 i−1种物品,总体积为j时的情况;
        第i种物品取了,枚举它取了k次,问题变成了考虑前 i−1 种物品,总体积为 j−v[i]×k时的情况;
状态:用f[i][j]表示考虑了前i种物品,总体积为j时的最大收益。
转移: f[i][j]=max(f[i−1][j],max  f[i−1][j−v[i]×k]+w[i]×k)

把j按照 mod v[i]分类,只有同一类的状态间可以进行转移;
        例如:当 v[i]=4时,其中一类为1,5,9,13,...
在某一类中下标为k的位置,可以转移到下标在 [k+1,k+l[i]]中的位置;
每个位置取最大值,
下标k,x之间转移的时候, (x−k)×w[i]怎么处理?
要求max(f[k]+(x−k)×w[i]),f[k]表示某一类中下标为k时的最值;因为x*w[i]对于每个位置是定值;
所以把 f[k]−k×w[i]放进单调队列就行啦!

int n, m, v, w, l,t, f[10001], c[10001][2];
int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> v >> w >> t;
		for (int j = 0; j < v; j++) {
			int k = 0, l = 1;//k是尾指针,l是头指针
			for (int pos = j, x = 1; pos <= m; pos += v, ++x) {
				int e = f[pos] - x * w, r = x + t;
				for (; k >= l && c[k][0] <= e; --k);
				c[++k][0] = e; c[k][1] = r;
				f[pos] = c[l][0] + x * w;
				for (; k >= l && c[l][1] == x; ++l);
			}
		}
	}
	cout << f[m] << endl;
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值