【训练题40:高维前缀和】Counting Good Teams | Gym - 101484K

这篇博客讨论了一种计数问题,涉及到n个人和m个课程,每个人擅长特定课程的集合。目标是找到两个人,他们各自至少有一门对方不擅长的课程。文章介绍了如何通过反面思考和高维前缀和优化算法,将时间复杂度降低到O(m×2^m),并给出了详细的代码实现。
摘要由CSDN通过智能技术生成

题意

  • Counting Good Teams | Gym - 101484K
    n n n 个人, m m m 个课。每个人有一个擅长科目的集合 S i ∈ [ 0 , 2 m ) S_i\in[0,2^m) Si[0,2m)
    你要选择一个两个人的队伍,使得这两个人每个人都至少有一门科目是他会的但是对方不会的。
    问选择的方案数。

范围

  • 2 ≤ n ≤ 1 0 5 2\le n\le 10^5 2n105
    2 ≤ m ≤ 21 2\le m\le21 2m21

思路

  • 正面枚举不好做,我们考虑反面情况,就是这两个人会的领域,其中一个人是另一个人的子集。
    d p [ S ] dp[S] dp[S] 表示会的领域为 S S S 的情况下,子集的数目。
    则根据定义, d p [ S ] = ∑ T ⊂ S d p [ T ] dp[S]=\sum_{T\sub S}dp[T] dp[S]=TSdp[T]
    但是我们暴力枚举子集的子集,时间复杂度为 O ( 3 n ) O(3^n) O(3n) (为什么,可以作为课后习题)
    但是我们有更优的策略来达到 O ( n × 2 n ) O(n\times2^n) O(n×2n)
  • 前缀和
    三维前缀和,我们可能会这么写
for i = 1 to n
	for j = 1 to n
		for k = 1 to n
			dp[i][j][k] += dp[i-1][j][k]
for i = 1 to n
	for j = 1 to n
		for k = 1 to n
			dp[i][j][k] += dp[i][j-1][k]
for i = 1 to n
	for j = 1 to n
		for k = 1 to n
			dp[i][j][k] += dp[i][j][k-1]
  • 高维前缀和,求子集 d p dp dp 则可以变成这样:
for i = 1 to n
	for j = 0 to (1<<n)-1
		if(j & (1<<i))
			dp[j] += dp[j ^ (1<<i)]
  • 高维前缀和,求超集 d p dp dp 则可以变成这样:
for i = 1 to n
	for j = 0 to (1<<n)-1
		if(!(j & (1<<i)))
			dp[j] += dp[j | (1<<i)]
  • 那么,我们对于某一个状态 S S S,直接就能得到有多少个状态 T T T,满足 T ⊂ S T\sub S TS
    考虑答案,就是 n ∗ ( n − 1 ) / 2 n * (n - 1) / 2 n(n1)/2 再减掉非法的方案。有哪些非法的方案?
    (1)集合 S S S 的个数有 c n t [ S ] cnt[S] cnt[S] 个,集合 S S S 的子集个数有 d p [ S ] dp[S] dp[S]
    那么集合 S S S 的真子集的个数有 d p [ S ] − c n t [ S ] dp[S]-cnt[S] dp[S]cnt[S] 个。
    那么减掉的数量就是 c n t [ S ] × ( d p [ S ] − c n t [ S ] ) cnt[S]\times(dp[S]-cnt[S]) cnt[S]×(dp[S]cnt[S])
    (2)集合 S S S 和集合 S S S 的人的配对也是非法的。
    那么减掉的数量就是 c n t [ S ] × ( c n t [ S ] − 1 ) / 2 cnt[S]\times(cnt[S]-1)/2 cnt[S]×(cnt[S]1)/2

代码

  • 时间复杂度: O ( m × 2 m ) O(m\times 2^m) O(m×2m)
ll cnt[MAX];
ll dp[MAX];

int main()
{
    int n,m;scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;++i){
        int t;
        scanf("%d",&t);
        cnt[t]++;
        dp[t]++;
    }
    for(int i = 0;i < m;++i)
    for(int j = 0;j < (1<<m);++j)
    if(j & (1<<i))
        dp[j] += dp[j^(1<<i)];
    ll ans = (ll)n * (n - 1) / 2;
    for(int i = 0;i < (1<<m);++i){
        ans -= cnt[i] * (dp[i] - cnt[i]);
        ans -= cnt[i] * (cnt[i] - 1) / 2;
    }
    printf("%lld",ans);
    return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值