20250818 XYD 023 T2

题意

小 A 班上一共有 \(n\) 个同学(不包括小 A),小 A 作为班长,需要将所有同学(除自己以外)划分为若干个小组,以方便管理。

为了让大家尽量满意分组的结果,小 A 用独立程度来描述每一个同学,即其希望自己所在小组的人数 < 独立程度。

经过观察,小 A 得到了每个同学的独立程度,其中第 \(i\) 个同学为 \(a_i\) 小 A 很快分好了组,但他并不满足于此,他希望求出一共有多少种本质不同的分组方式。

这里两组方案本质不同当且仅当存在两个同学,其中一组方案中两人在同一组,而另一种方案中两人不在同一组。

由于答案可能非常大,只需要求出对 取模后的结果。

提示:每个同学需恰在某一组中,且每组均需至少包含一个人。

注意: 本题时限为0.5秒

对于所有测试数据,保证 \(1 <= n <= 2000, a_i <= n\)

思路

可将题目分组要求转化为每组人数小于等于每组最小值。

肯定是 DP,如果直接对于每一个人,去考虑把它分到那一个组则需将每个组的最小值和目前的大小作为状态,肯定不可行。如果对于每一个组,考虑把哪些人分到这个组,则需把每个人是否被选作为状态,也不可行。但是,我们可以发现一个性质,当一个组中的最小值确定后,这个组的最大长度便确定了,并且不需要再关注组中其他人的值(因为非最小值不会影响组的长度上限)。

依靠这样一个性质,我们可以考虑从小到大排序(因为排序之后,每组的第一个数就是最小值)然后,对于上面的性质,我们可以把它解释为以下:我们如果遇到一个人,就可以考虑从他开始新建一个组,而此时,整个组的人数上限就已经确定,后面的人我至多在乎选的是谁,而不在乎他的值,因为他的值不会影响组的人数上限。

现在,我们可以考虑之前被我们放弃了的 “考虑把哪些人分到这个组” 的DP方案。首先可以发现,我们不能将枚举组作为拓扑序,而应该将枚举人作为拓扑序,因为在现在的考虑下,每个组都是依靠最小值而建立的。然后我们来考虑转移,有两种,一种是形如上文的“新建一组”,一种是当前点已经被某个组给覆盖。所以可以得到状态 \(dp[i][j]\) 表示当前已经考虑了前 \(i\) 个人,而在后面还有 \(j\) 个没用。第一维非常合理,而第二位是因为我需要考虑在 \(i\) 后面选,所以后面还剩的没选的人的数量是会影响转移的。转移狮子大概就是对于第一种转移考虑这一组选 \(k\) 个人,然后组合数学在后面的 \(j\) 个人里选 \(k\) 个,第二种 \(j\) 减少。

综上可实现 \(O(N ^ 3)\)

我们又可以发现,其实我们在乎的不是每组的最小值是谁,而是每组的长度。所以可将枚举人改为枚举组的长度,因为我们是从小到大排序,所以“前 \(i\) 个人”状态变为考虑了长度小于 \(i\) 的组。然而这样我们却需要枚举长度为 \(i\) 的组的数量,但观察到长度为 \(i\) 的组的总人数会随着组的数量的增加而每次增加 \(i\),调和级数所以是 \(\log N\)。对于转移,考虑背包转移 \(i + 1\) 并在 \(j\) 中减少当前选出的 \(k\) 组人的总人数,但要乘上在 \(j\) 中选出这 \(k\) 组人的方案数,\(\frac{\binom{j}{i} \binom{j - i}{i} ... \binom{j - (k - 1)i}{i}}{k!}\)

综上可实现 \(O(N ^ 2 \log N)\)

Code

本代码为从大到小考虑,而题解为从小到大考虑。

#include<bits/stdc++.h>

using namespace std;
using ll = long long;

const int N = 2e3 + 5, MOD = 1e9 + 7;

int n;
ll a[N], f[N], inv[N], dp[N][N], d[N];

ll C(ll n, ll m) {//组合数
	if (n < m) {
		return 0;
	}
	return d[n] * inv[n - m] % MOD * inv[m] % MOD;
}

ll Pow(ll a, ll b) {//快速幂
	ll ans = 1;
	for (; b; b >>= 1, a = a * a % MOD) {
		if (b & 1) {
			ans = ans * a % MOD;
		} 
	}
	return ans;
}

int main() {
    freopen("group.in", "r", stdin);
    freopen("group.out", "w", stdout);
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		f[a[i]]++;//值为 a[i] 的人的数量
	}
	d[0] = 1, inv[0] = Pow(d[0], MOD - 2);
	for (ll i = 1; i <= n; i++) {
		d[i] = d[i - 1] * i % MOD;//阶乘
		inv[i] = Pow(d[i], MOD - 2);//阶乘的逆元预处理
	}
	dp[n + 1][0] = 1;
	for (int i = n, cnt = 0; i >= 1; i--) {
		cnt += f[i]; //值大于 i 的人数
		for (int j = 0; j <= cnt; j++) {
			int res = 1;
			dp[i][j] = dp[i + 1][j]; //长度为 i 的组选 0 个
			for (int k = 1; k * i <= j; k++) { //长度为 i 的组选 k 个
				res = res * C(cnt - j + k * i, i) % MOD; //组合数学
				dp[i][j] = (dp[i][j] + dp[i + 1][j - k * i] * res % MOD * inv[k] % MOD) % MOD;//考虑将 k 个长度为 i 的组解散
//				if (i == 1 && j == 4) {
//					cout << res << ' ';
//				}
			}
			//cout << dp[i][j] << ' ';
		}
		//cout << '\n';
	}
	cout << dp[1][n];
	return 0;
}

/*
4
1 3 2 4
*/
posted @ 2025-08-20 17:17  oymz  阅读(74)  评论(3)    收藏  举报