DP 复习

背包

0-1背包

简介

0-1背包是用于解决类似于“有若干个物品,物品有体积有价值,每个物品仅能取用一次,在体积和不超过(恰好为)给定值的情况下最大(最小)化价值之和”的问题。

思路

这是最基础的一类动规问题,我们一般设 \(f_{i,j}\) 表示前 \(i\) 个物品,选出若干个体积之和不超过(刚好为)\(j\) 的最大价值。
动规方程显然为 \(f_{i,j}=max\{f_{i-1,j},f_{i-1,j-w_i}+v_i\}\)(要么不选要么选,两种情况取最大值),但是当 \(j \lt w_i\) 时,我们应该直接继承 \(f_{i-1,j}\) 的值(不能选就直接不选)。
那么我们就可以写出核心代码(共 \(n\) 个物品,容量为 \(m\)\(w_i\) 为体积,\(v_i\) 为价值):

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

我们知道体积和不超过给定值与体积和恰好为给定值一般来说是不一样的。但是光看上述代码发现这份代码又好像求的既可以是不超过,也可以是求恰好为,整个思路是一样的。我们需要引入一个概念:DP 边界。简单来说就是初始化,而这两种问题的区别就在于初始化。

首先,我们看不超过,我们知道,\(f_{i,j}\) 表示表示前 \(i\) 个物品,选出若干个体积之和不超过\(j\) 的最大价值。由于该方程仅与 \(f_{i-1,\dots}\) 有关,所以我们可以考虑当 \(i\)\(0\) 时的情况。那么根据定义,\(0\) 个物品当然在任何 \(j\) 值的情况下价值和都应该是 \(0\),所以我们将 \(f_{0,\dots}\) 全部赋值为 \(0\)
而对于恰好的问题,我们同样看到 \(i=0\) 的情况,此时,按照方程的定义只有 \(f_{0,0}=0\),其他的状态都属于不可到达,这里我们将不可到达的状态的值设为 \(\pm\infty\)(求最大值设最小值,求最小值设最大值)以此来避免其转移到其他位置,以此来防止出现“\(0\) 个物品体积和恰好为 \(j(j\gt 0)\)”的状态转移到其他状态。

那么我们就可以得到真正的代码:

for(int i = 1; i <= m; i++) {
	f[0][i] = 0;//不大于
	f[0][i] = INF;//恰好为
}

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

小优化

观察方程,我们发现每一个状态 \(f_{i,j}\) 都只由 \(f_{i-1,\dots}\) 转移而来,并且第二维一定会小于等于 \(j\),所以我们就可以将 \(i\) 这一维直接去掉,变成 \(f_{j}=max\{f_{j},f_{j-w_i}+v_i\}\)
我们注意到,这里的 \(f_{j-w_i}\) 其实是 \(i-1\) 时转移来的值,所以我们需要在转移 \(f_{j-w_i}\) 之前转移 \(f_{j}\),也就是说需要从大到小枚举 \(j\)

for(int i = 1; i <= n; i++) {
	for(int j = m; j >= w[i]; j--) {//j<w[i]的情况不需要处理,自动继承
		f[j] = max(f[j], f[j - w[i]] + v[i]);
	}
}

完全背包

简介

完全背包是用于解决类似于“有若干种物品,物品有体积有价值,每种物品能取用任意次,在体积和不超过(恰好为)给定值的情况下最大(最小)化价值之和”的问题。
看起来和 0-1 背包很像。

思路

同样的,我们设 \(f_{i,j}\) 表示前 \(i\) 种物品,选出若干个体积之和不超过(刚好为)\(j\) 的最大价值。首先,如果不选当前这种物品,那么它的贡献当然就是 \(f_{i-1,j}\),再考虑选当前这种物品,由于可以重复选,所以在考虑选第 \(i\) 种物品的时候要的是前 \(i\) 种物品,体积和不超过(恰好为)\(j-w_i\) 的最大价值,为 \(f_{i,j-w_i}+v_i\)。那么就有 \(f_{i,j}=max\{f_{i-1,j},f_{i,j-w_i}+v_i\}\)。根据方程的转移顺序,我们必须按 \(j\) 从小到大更新状态(于是有了外层枚举 \(j\) 的非主流写法)。同样的,\(j\lt w_i\) 时直接继承。

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

不大于和恰好的区别同 0-1 背包,这里不再讲。

小优化

与 0-1 背包相同,完全背包也可以把第一维去掉,注意这里要从小到大枚举 \(j\)

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

多重背包

思路

多重背包就是在完全背包的基础上加上了每种物品只有 \(k_i\) 个的限制,其实可以直接拆开变成 0-1 背包。也可以直接在 0-1 背包里面加上一层循环来枚举这种物品取多少个。思路很简单。

for(int i = 1; i <= n; i++) {
	for(int j = 1; j <= m; j++) {
		for(int cnt = 0; cnt <= k[i] && cnt * w[i] <= j; cnt++) {
			f[j] = max(f[j], f[j - cnt * w[i]] + cnt * v[i]);
		}
	}
}

优化

原版的多重背包时间复杂度为 \(O(m\sum k_i)\),有些题目会卡掉,所以我们需要对其进行优化。
原本的做法直接枚举了每一种物品的数量,这么做会导致有很多状态被重复访问(比如 \(f_{i,j+2w_i}\) 会被 \(f_{i,j}\)\(f_{i,j+w_i}\) 两个状态转移到),但是如果我们用另一种更优的方法来枚举物品的数量,并且保证每个可能的数量都可以被我们美枚举到,我们便可以将 \(\sum k\) 优化掉。
考虑怎么才能用较少的数字来表示出 \([1,k_i]\) 中的所有数字。做倍增题做得多的巨佬可能一下就能想到二进制拆分(我反正想不到),所以我们不再枚举 \([1,k_i]\) 中的所有数字,而是将 \(k_i\) 个物品分为 \(1,2,4\dots 2^a,b\) 个。其中,\(b\) 是拆分后剩下的,那么我们就可以通过这些东西凑出 \([1,k_i]\) 中的所有数字。
首先,我们可以通过 \(1,2\dots 2^a\) 凑出小于 \(2^{a+1}-1\) 的所有数字,然而 \(b\) 一定会小于 \(2^{a+1}-1\) 否则会拆分出 \(2^{a+1}\)。然后我们只要将 \([1,2^{a+1}-1]\) 中最大的 \(b\) 个数(显然是有大于 \(b\) 个数的)全部加上 \(b\) 就可以凑出 \([2^{a+1},k_i]\) 中的所有数。
拆完之后跑 0-1 背包就行。
以下是拆分的代码

for(int i = 1; i <= n; i++) {
	int vv, ww, k;
	read(vv), read(ww), read(k);
	
	for(int j = 1; j <= k; j <<= 1) {
		v[++cnt] = j * vv;
		w[cnt] = j * ww;
		k -= j;
	}
	
	if(k) {
		v[++cnt] = k * vv;
		w[cnt] = k * ww;
	}
}

也可以把拆分放进转移过程。

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

以上可以将复杂度优化至 \(O(\sum\log(k_i)m)\),已经可以通过绝大部分题目。
还有一种单调队列优化,复杂度更加优秀,可以将其优化至 \(O(nm)\)。具体来说,就是发现在方程 \(f_{i,j}=max\{f_{i-1,j},f_{i-1,j-cnt\times w_i}\}\) 中,按照第二维膜 \(w_i\) 分组后发现只有同组的状态能够转移到对方,那么我们直接枚举当前到了第 \(i\) 种物品,然后枚举是哪一组,对于每一组的状态,我们用单调队列来优化,这样可以保证每一种体积只会便利到一遍,那么它的时间复杂度为 \(O(nm)\)

for(int i = 1; i <= n; i++) {
	int lim = min(k[i], m / w[i]);
	for(int j = 0; j < w[i]; j++) {//枚举组数
		int l = 0, r = -1;
		
		for(int cnt = 0; cnt <= (m - j) / w[i]; cnt++) {
			int a = cnt, b = f[cnt * w[i] + j] - cnt * v[i];
			while(l <= r && q[l].first + lim < cnt) l++;
			while(l <= r && q[r].second <= b) r--;
			q[++r] = makr_pair(a, b);
			
			f[cnt * w[i] + j] = q[l].second + cnt * v[i];
		}
	}
}

计数

posted @ 2023-10-03 11:26  JR_ytxy  阅读(11)  评论(0)    收藏  举报