洛谷 P4491 [HAOI2018]染色 ntt

题目描述
为了报答小 C 的苹果, 小 G 打算送给热爱美术的小 C 一块画布, 这块画布可 以抽象为一个长度为 N N N 的序列, 每个位置都可以被染成 M M M 种颜色中的某一种。

然而小 C 只关心序列的 N N N 个位置中出现次数恰好为 S S S 的颜色种数, 如果恰 好出现了 S S S 次的颜色有 K K K 种, 则小 C C C 会产生 W k W_k Wk 的愉悦度.

C C C 希望知道对于所有可能的染色方案, 他能获得的愉悦度的和对 1004535809 1004535809 1004535809 取模的结果是多少.

输入输出格式

输入格式:
从标准输入读入数据. 第一行三个整数 N , M , S N, M, S N,M,S

接下来一行 M + 1 M + 1 M+1 个整数, 第 i i i 个数表示 W i − 1 W_{i-1} Wi1​ .

输出格式:
输出到标准输出中. 输出一个整数表示答案.
输入输出样例

输入样例#1:
8 8 3
3999 8477 9694 8454 3308 8961 3018 2255 4910
输出样例#1:
524070430

说明

特殊性质: ∀ 1 ≤ i ≤ m , W i = 0 \forall 1 \le i \le m, W_i = 0 1im,Wi=0
对于 100 % 100\% 100% 的数据, 满足 0 ≤ W i &lt; 1004535809 0 \le W_i &lt; 1004535809 0Wi<1004535809

Data
在这里插入图片描述
分析:
f [ i ] f[i] f[i]表示至少有 i i i种颜色相同的方案数。
首先要在 m m m中颜色中选出 i i i中,方案为 ( m i ) \binom{m}{i} (im)。对于这 i i i种颜色,考虑每一种的放到画布上的方案,显然第一种是 ( n s ) \binom{n}{s} (sn),第二种是 ( n − s s ) \binom{n-s}{s} (sns),……,乘起来就是 n ! ( s ! ) i ∗ ( n − s ∗ i ) ! \frac{n!}{(s!)^i*(n-s*i)!} (s!)i(nsi)!n!,最后剩余的直接都是可以乱选的。
所以,
f [ i ] = ( m i ) ∗ n ! ( s ! ) i ∗ ( n − s ∗ i ) ! ∗ ( m − i ) ( n − s ∗ i ) f[i]=\binom{m}{i}*\frac{n!}{(s!)^i*(n-s*i)!}*(m-i)^{(n-s*i)} f[i]=(im)(s!)i(nsi)!n!(mi)(nsi)
设恰好为 i i i的方案为 g [ i ] g[i] g[i],直接容斥得到,
g [ i ] = ∑ j = i m ( − 1 ) j − i ∗ ( j i ) ∗ f [ j ] g[i]=\sum_{j=i}^{m}(-1)^{j-i}*\binom{j}{i}*f[j] g[i]=j=im(1)ji(ij)f[j]
把组合数拆开然后NTT即可。

代码:

// luogu-judger-enable-o2
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#define LL long long

const int N=1e7+7;
const int maxn=3e5+7;
const LL G=3;
const LL mod=1004535809;

using namespace std;

int n,m,S,len;
LL a[maxn],jc[N],inv[N],f[maxn],g[maxn],r[maxn],w[maxn];

LL ksm(LL x,LL y)
{
    if (y==0) return 1;
    LL c=ksm(x,y/2);
    c=(c*c)%mod;
    if (y&1) c=(c*x)%mod;
    return c;
}

void prework()
{
    jc[0]=1;
    for (int i=1;i<=max(n,m);i++) jc[i]=jc[i-1]*(LL)i%mod;
    inv[max(n,m)]=ksm(jc[max(n,m)],mod-2);
    for (int i=max(n,m);i>0;i--) inv[i-1]=inv[i]*(LL)i%mod;
}

void ntt(LL *a,int f)
{
    for (int i=0;i<len;i++)
    {
        if (i<r[i]) swap(a[i],a[r[i]]);
    }
    w[0]=1;
    for (int i=2;i<=len;i<<=1)
    {
        LL wn;
        if (f==1) wn=ksm(G,(mod-1)/i);
             else wn=ksm(G,(mod-1)-(mod-1)/i);
        for (int j=i/2-2;j>=0;j-=2) w[j]=w[j/2];
        for (int j=1;j<i/2;j+=2) w[j]=(w[j-1]*wn)%mod;
        for (int j=0;j<len;j+=i)
        {
            for (int k=0;k<i/2;k++)
            {
                LL u=a[j+k],v=a[j+k+i/2]*w[k]%mod;
                a[j+k]=(u+v)%mod;
                a[j+k+i/2]=(u+mod-v)%mod;
            }
        }
    }
    if (f==-1)
    {
        LL inv=ksm(len,mod-2);
        for (int i=0;i<len;i++) a[i]=a[i]*inv%mod;
    }
}

void NTT(LL *a,LL *b,LL *c,int n,int m)
{
    len=1;
    int k=0;
    while (len<=n+m) len<<=1,k++;
    for (int i=0;i<len;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(k-1));
    ntt(a,1),ntt(b,1);
    for (int i=0;i<len;i++) c[i]=a[i]*b[i]%mod;
    ntt(c,-1);
}

int main()
{
    scanf("%d%d%d",&n,&m,&S);
    for (int i=0;i<=m;i++) scanf("%lld",&a[i]);
    prework();	
    for (int i=0;i<=m;i++)
    {
        if (i*S>n) break;
        f[i]=jc[m]*inv[m-i]%mod*jc[n]%mod*ksm(inv[S],i)%mod*inv[n-S*i]%mod*ksm(m-i,n-S*i)%mod;
    }
    for (int i=0;i<=m;i++) 
    {
        if (i&1) g[i]=mod-inv[i];
            else g[i]=inv[i];
    }
    reverse(f,f+m+1);
    NTT(f,g,f,m+1,m+1);
    reverse(f,f+m+1);
    LL ans=0;
    for (int i=0;i<=m;i++) ans=(ans+f[i]*a[i]%mod*inv[i]%mod)%mod;
    printf("%lld\n",ans);
}
这道题目还可以使用树状数组或线段树来实现,时间复杂度也为 $\mathcal{O}(n\log n)$。这里给出使用树状数组的实现代码。 解题思路: 1. 读入数据; 2. 将原数列离散化,得到一个新的数列 b; 3. 从右往左依次将 b 数列中的元素插入到树状数组中,并计算逆序对数; 4. 输出逆序对数。 代码实现: ```c++ #include <cstdio> #include <cstdlib> #include <algorithm> const int MAXN = 500005; struct Node { int val, id; bool operator<(const Node& other) const { return val < other.val; } } nodes[MAXN]; int n, a[MAXN], b[MAXN], c[MAXN]; long long ans; inline int lowbit(int x) { return x & (-x); } void update(int x, int val) { for (int i = x; i <= n; i += lowbit(i)) { c[i] += val; } } int query(int x) { int res = 0; for (int i = x; i > 0; i -= lowbit(i)) { res += c[i]; } return res; } int main() { scanf("%d", &n); for (int i = 1; i <= n; ++i) { scanf("%d", &a[i]); nodes[i] = {a[i], i}; } std::sort(nodes + 1, nodes + n + 1); int cnt = 0; for (int i = 1; i <= n; ++i) { if (i == 1 || nodes[i].val != nodes[i - 1].val) { ++cnt; } b[nodes[i].id] = cnt; } for (int i = n; i >= 1; --i) { ans += query(b[i] - 1); update(b[i], 1); } printf("%lld\n", ans); return 0; } ``` 注意事项: - 在对原数列进行离散化时,需要记录每个元素在原数列中的位置,便于后面计算逆序对数; - 设树状数组的大小为 $n$,则树状数组中的下标从 $1$ 到 $n$,而不是从 $0$ 到 $n-1$; - 在计算逆序对数时,需要查询离散化后的数列中比当前元素小的元素个数,即查询 $b_i-1$ 位置上的值; - 在插入元素时,需要将离散化后的数列的元素从右往左依次插入树状数组中,而不是从左往右。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值