@noi.ac - 508@ 01背包


@description@

有一天你学了一个能解决01背包问题的算法,你决定将这个算法应用到NOI比赛中。

你有一个大小为 V 的背包。

有 n 种物品,每一种物品均有 m 个。每一个物品都有一个体积,对于第 i 种物品中的第 j 个,它的体积为 vij。

你想把若干个物品按一定顺序放入背包,要求每一个物品只能使用一次且总体积不能超过 V,除此之外要求同种物品不能相邻。你想知道有多少种方案。

两种方案不同当且仅当二者选出的物品集合不同,或者物品集合相同但是这些物品的排列顺序不同。两个相同种类相同体积的物品算作不同物品。

你可以选择所有物品也可以一个物品都不选。

输出总方案数模 10^9+7 的值。

input
第一行三个整数 n,m,V,含义如题所述。

接下来 n 行,每行 m 个整数,其中第 i 行的第 j 个数为 vij 的值。

output
输出一行一个整数,表示总方案数模 10^9+7 的值。

sample input
2 2 3
1 2
2 2
sample output
9

explanation
我们把两个第一种物品用小写字母“a”和“b”表示,两个第二种物品用大写字母“A”和“B”表示。其中仅有a的体积为1,其他三个物品的体积均为2。

九种方案分别为:不选,a,b,A,B,aA,aB,Aa,和Ba。

注意AB不是一个合法的方案,因为它们的总体积为 2+2=4,大于3。

ab也不是一个合法的方案,因为虽然它们的总体积没有超过3,两个同种物品(i.e. a和b)在此方案中相邻。

对于100%的数据,1≤n,m≤50,1≤vij,V≤100。

@solution@

考虑假如已经给定了每种物品分别的数量,怎么求出使相同种类的物品不相邻的排列总数。

我们常见的使物品不相邻的组合计数方法有插空法,即先规划其他物品的位置再在其他物品的空隙中放入物品。
但因为要求每个种类的物品都互不相邻,故这种方法不能适用。因为有可能在当前相邻的物品,以后会加入某些物品将它们阻隔开。

考虑另一种方法:捆绑若干物品(即让这些物品必须在一起)然后容斥。
令 a[1...k] 表示一种捆绑方案,其中 a[i] 表示第 i 次捆绑将 a[i] 个物品捆绑成一组。可以通过搜索找到所有合法的 a。
通过手动推导可以得到如下的容斥式:

\[ans=\sum_k(k!*\prod_{i=1}^{k}((-1)^{a[i]-1}*(a[i]!))) \]

其中 \(a[i]!\) 表示每个组内部的方案数(即内部进行全排列),\((-1)^{a[i]-1}\) 是容斥的系数,\(k!\) 即捆绑后的全排列。

但显然这个算法是不合格的。
我们依然沿用容斥+捆绑的思路,考虑改变思路方向,即不去枚举每种物品分别的数量。
可以发现最后的答案只跟捆绑后的组数以及每一组捆绑的物品个数有关。于是我们可以将若干物品捆绑后得到的组当作一个新物品,同时给予这个新物品权值(即上式中的 \((-1)^{a[i]-1}*(a[i]!)\))。
然后再将所有物品放在一起进行有权值的背包 dp,最后再乘一个阶乘表示全排列(即上式中的 \(k!\))。

然而这个算法有一点瑕疵:假如我们把物品 1, 2 合为一个物品,物品 2, 3 合为一个物品,不难发现这两个新物品是不能共存的。
但由于我们只会把同种类的物品合起来,所以我们可以再通过一个 dp 回避这个问题。

定义 dp[i][j][V] 表示 i 个物品分成 j 个组,占背包容量 V 的权值和。
假如新加入一个物品,要么它单独成组;要么它加入前面的某一个组,这个时候以前加入的每个物品后面都有一个位置,并且每个组的最前端也有一个位置,所以一共有 i+j 个位置可供选择(类似于第一类斯特林数,但第一类斯特林数是圆排列而这里是普通排列)。同时加入到前面的组里去后,组的大小改变,容斥的系数要乘 -1。
所以得到转移式:

\[dp[i+1][j][V] = dp[i][j-1][V-v] - dp[i][j][V-v]*(i+j) \]

其中 v 是新加入物品的体积。

注意到题目中所有物品都是有体积的,故背包的容量就是背包可以装的物品数量的上界。
最后枚举背包被物品占据的体积 O(V),枚举背包内装有物品的数量 O(V),枚举物品种类 O(n),枚举这类物品占背包的容量 O(V),枚举这类物品有多少个组加入背包中 O(m)。
总时间复杂度 O(V^3nm)。可以调换枚举顺序先确定这类物品的信息,再判断这些信息方案数是否为 0,如果为 0 就跳过。
这个剪枝效果好到可以让你跑过这道题,在这么不科学的理论时间复杂度下。

@accepted code@

#include<cstdio>
const int MOD = int(1E9) + 7;
int f[50 + 5][50 + 5][100 + 5];
int dp[50 + 5][100 + 5][100 + 5], g[50 + 5][100 + 5];
int main() {
	int n, m, v, V; scanf("%d%d%d", &n, &m, &V);
	dp[0][0][0] = 1;
	for(int i=1;i<=n;i++) {
		for(int j=0;j<=m;j++)
			for(int k=0;k<=j;k++)
				for(int l=0;l<=V;l++)
					f[j][k][l] = 0;
		f[0][0][0] = 1;
		for(int j=1;j<=m;j++) {
			scanf("%d", &v);
			for(int p=j;p>=1;p--) {
				for(int q=p-1;q>=1;q--)
					for(int k=V;k>=v;k--)
						f[p][q][k] = (f[p][q][k] + (f[p-1][q-1][k-v] + 1LL*(MOD - 1)*(p + q - 1)%MOD*f[p-1][q][k-v]%MOD)%MOD)%MOD;
				for(int k=V;k>=v;k--)
					f[p][p][k] = (f[p][p][k] + f[p-1][p-1][k-v])%MOD;
			}
		}
		for(int j=0;j<=m;j++)
			for(int k=0;k<=V;k++) {
				g[j][k] = 0;
				for(int l=j;l<=m;l++)
					g[j][k] = (g[j][k] + f[l][j][k])%MOD;
			}
		for(int j=0;j<=m;j++)
			for(int k=0;k<=V;k++) {
				if( !g[j][k] ) continue;
				for(int p=j;p<=V;p++)
					for(int q=k;q<=V;q++)
						dp[i][p][q] = (dp[i][p][q] + 1LL*dp[i-1][p-j][q-k]*g[j][k])%MOD;
			}
	}
	int ans = 0;
	for(int i=0,f=1;i<=V;i++,f=1LL*f*i%MOD)
		for(int j=0;j<=V;j++)
			ans = (ans + 1LL*f*dp[n][i][j]%MOD)%MOD;
	printf("%d\n", ans);
}

@details@

算是这几天遇到的比较有意思的题。

再一次印证,容斥本身就是很难想的。。。
比如今年 THUWC 上的那道容斥题。。。

以及时间复杂度算出来明明需要跑 2.5*10^9 次却能在剪枝的情况下跑得飞快。。。

posted @ 2019-07-01 11:48  Tiw_Air_OAO  阅读(196)  评论(0编辑  收藏  举报