840C - On the Bench (容斥 + DP)

文章介绍了如何处理一个数组排列问题,要求任何两个相邻元素的乘积不是完全平方数。通过将完全平方数归为同一类,并使用并查集进行合并。接着,将问题转化为计算不同颜色球的同色不相邻排列数量,利用动态规划和容斥原理求解,最后给出了一段C++代码实现。
摘要由CSDN通过智能技术生成

840C - On the Bench

题意

给定序列 a [ 1... n ] a[1...n] a[1...n],求有多种 a a a 的排列,满足任意两个相邻的数乘积不是完全平方数,答案对 1 0 9 + 7 10^9+7 109+7 取模。

a i ≤ 1 0 9 ,   1 ≤ n ≤ 300 a_i\le 10^9,\ 1\le n \le 300 ai109, 1n300


转换

如果 x y = p 2 xy=p^2 xy=p2 y z = q 2 yz=q^2 yz=q2,那么 x z = ( p q x ) 2 xz=({\dfrac{pq}{x}})^2 xz=(xpq)2,也是一个完全平方数。所以我们可以把把 x , y , z x,y,z x,y,z 视为一个同一种颜色,以此类推……用并查集维护每一种颜色的数量。

于是问题转换为:

m m m 种颜色的球,第 i i i 种颜色的球有 s i s_i si 种,总共 n n n 个球,求同色不相邻的排列的数量。


DP + 容斥 O ( n 2 ) O(n^2) O(n2)

本方法参考 这篇博客

f ( k ) f(k) f(k) 表示把这些小球分成 k k k 个连续段的方案数,其中:

  • 每一段都是相同的颜色;
  • 段内小球考虑排列顺序;
  • 段与段之间无序,即所有的段构成一个无序集合。

下面用 DP 来求 f ( k ) f(k) f(k)

f ( i , k ) f(i,k) f(i,k) 表示考虑前 i i i 种颜色,分成了 k k k 组的方案数。容易推出 f f f 的转移方程
f ( i , k ) = ∑ j = 1 min ⁡ ( s i , k ) f ( i − 1 , k − j ) ⋅ s i ! ⋅ ( s i − 1 j − 1 ) j ! f(i,k) = \sum_{j=1}^{\min(s_i, k)}f(i-1,k-j)\cdot \dfrac{s_i!\cdot{s_i-1\choose j-1}}{j!} f(i,k)=j=1min(si,k)f(i1,kj)j!si!(j1si1)

简要介绍这个式子的含义。

枚举最后一种颜色(第 i i i 种颜色)分成了 j j j 段,所以由 f ( i − 1 , k − j ) f(i-1,k-j) f(i1,kj) 转移过来。

s i ! s_i! si! 是将 s i s_i si 个同色小球排列,然后 ( s i − 1 j − 1 ) {s_i-1\choose j-1} (j1si1) 是将这个排列分成 k k k 个段(原理是隔板法)。由于组与组是无序的,所以要除以 j ! j! j!

那么 f ( n , k ) f(n,k) f(n,k) 就是将所有的小球分为 k k k 组的方案数,简要记为 f ( k ) f(k) f(k)

特别注意一下这个 DP 式子的复杂度,它虽然是三重循环,但神奇的是它的复杂度其实是 O ( n 2 ) O(n^2) O(n2) 。因为第一层循环的是颜色,第三层循环的次数不超过该种颜色的数量,所以这两层循环的总次数其实是 ∑ i = m n s i \sum_{i=m}^{n}s_i i=mnsi,这两层和一块是 O ( n ) O(n) O(n) 级别的。


如果把所有小球分为 k k k 组,那么至少有 n − k n-k nk 对相邻的同色小球。

所以 f ( n ) ⋅ n ! f(n)\cdot n! f(n)n! 就是提前钦定 n − k n-k nk 对同色小球,其余随意组合的方案数之和。这样答案就是典型的容斥
a n s = f ( n ) ⋅ n ! − f ( n − 1 ) ⋅ ( n − 1 ) ! + … ans=f(n)\cdot n! - f(n-1)\cdot(n-1)! + \dots ans=f(n)n!f(n1)(n1)!+

#include <bits/stdc++.h>
using namespace std;

#define int long long
#define PII pair<int, int>


int Testnum = 1;

/**********************  Core code begins  **********************/

const int N = 1003, MOD = 1e9 + 7;
int n, a[N];
int fac[N], inv[N], invfac[N];
int m = 0, s[N], f[N][N];

struct DSU {
    int fa[N], siz[N];

    void init() {
        for (int i = 1; i < N; i++) {
            fa[i] = i;
            siz[i] = 1;
        }
    }

    int find(int x) {
        return fa[x] == x ? x : fa[x] = find(fa[x]);
    }

    void merge(int x, int y) {
        if (find(x) == find(y)) {
            return;
        }

        int fx = find(x), fy = find(y);
        fa[fx] = fy;
        siz[fy] += siz[fx];
    }
} dsu;


void init() {
    fac[0] = inv[0] = invfac[0] = 1;
    fac[1] = inv[1] = invfac[1] = 1;

    for (int i = 2; i < N; i++) {
        fac[i] = fac[i - 1] * i % MOD;
        inv[i] = ((MOD - MOD / i * inv[MOD % i]) % MOD + MOD) % MOD;
        invfac[i] = invfac[i - 1] * inv[i] % MOD;
    }
}


int C(int x, int y) {
    if (x < y || x < 0) {
        return 0;
    }

    return fac[x] * invfac[y] % MOD * invfac[x - y] % MOD;
}


void SolveTest() {
    cin >> n;

    dsu.init();
    init();

    for (int i = 1; i <= n; i++) {
        scanf("%lld", &a[i]);
    }

    for (int i = 1; i <= n; i++) {
        for (int j = i + 1; j <= n; j++) {
            int x = sqrt(a[i] * a[j]);

            if (x * x == a[i] * a[j]) {
                dsu.merge(i, j);
            }
        }
    }

    m = 0;

    for (int i = 1; i <= n; i++) {
        if (dsu.fa[i] == i) {
            s[++m] = dsu.siz[i];
        }
    }

    f[0][0] = 1;

    for (int i = 1; i <= m; i++) {

        for (int k = i; k <= n; k++) {
            for (int j = 1; j <= min(s[i], k); j++) {
                f[i][k] = (f[i][k] + f[i - 1][k - j] * fac[s[i]] % MOD 
                    * C(s[i] - 1, j - 1) % MOD * invfac[j] % MOD) % MOD;
            }

        }
    }

    int res = 0, sig = 1;

    for (int i = n; i >= 1; i--) {
        res = (res + sig * f[m][i] % MOD * fac[i] % MOD) % MOD;
        sig = MOD - sig;
    }

    cout << res;
}

/**********************  Core code ends  ***********************/


signed main() {

#ifdef LOCAL
    freopen("in.txt", "r", stdin);
#endif

    // cin >> Testnum;

    for (int i = 1; i <= Testnum; i++) {
        SolveTest();
    }

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值