[BZOJ3771]Triple(FFT+生成函数+容斥)

题目:

我是超链接

题意:

给出n个互不相同的数,问从中选出1/2/3个数,每一个可以组合出的和有多少种方案。

题解:

这破题不给n的范围?!好吧n把斧头价值不同那么最多是40000把。
要求组合的方案数,考虑生成函数。生成函数相乘的时候指数记录了所有的方案,那么首先构造只有一个的生成函数:如果有一个价值为i的东西就把x^i这一项设为1,设这个生成函数为 G(x) G ( x ) ,那么 G3(x) G 3 ( x ) 就求出了在所有东西里选3次的方案数,设这个数字为 F1 F 1

但这样会存在有一个东西选了两次或者三次的方案数,要把他们减去才能求出恰好选了三个不一样的东西的方案数。要求有一个东西选了至少两次的方案数,就让ta在生成函数里强制选两次,也就是价值为i的东西就把 x2i x 2 i 的系数设为1,这个生成函数为 G2(x) G 2 ( x ) ,求出 G2(x)×G(x) G 2 ( x ) × G ( x ) 就是一个东西至少选两次的方案数,设为 F2 F 2

用组合数可以得到 F2 F 2 中的每一种方案在 F1 F 1 中都被计算了3次,所以要容斥。并且这样处理完之后一个东西选了3次的方案还被多减了两次所以要加回来,那么总的柿子应该是

G3(x)F2(x)3+G3(x)23! G 3 ( x ) − F 2 ( x ) ∗ 3 + G 3 ( x ) ∗ 2 3 !

这里除以3!是因为 G3(x) G 3 ( x ) 本来就存在选择一样方案数,也就是说是【排列】,我们要转化为【组合】。这样计算完了就得到了恰好选三个不一样的方案数

那恰好选两个不一样的方案数呢?

G2(x)G2(x)2 G 2 ( x ) − G 2 ( x ) 2

这个数据范围要nlogn,那就FFT优化生成函数吧

题解学习自优秀的学姐

代码:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int N=300000;
const double pi=acos(-1.0);
int num[N],r[N],x[N],a[N],b[N];
struct complex
{
    double x,y;
    complex(double X=0,double Y=0){x=X;y=Y;}
}g2[N],g[N];
complex operator +(complex a,complex b){return complex(a.x+b.x,a.y+b.y);}
complex operator -(complex a,complex b){return complex(a.x-b.x,a.y-b.y);}
complex operator *(complex a,complex b){return complex(a.x*b.x-a.y*b.y,a.y*b.x+a.x*b.y);}
void FFT(complex *a,int id,int n)
{
    for (int i=0;i<n;i++)
      if (i<r[i]) swap(a[i],a[r[i]]);
    for (int k=1;k<n;k<<=1)
    {
        complex wn=complex(cos(pi/k),id*sin(pi/k));
        for (int i=0;i<n;i+=(k<<1))
        {
            complex w=complex(1,0);
            for (int j=0;j<k;j++,w=w*wn)
            {
                complex x=a[i+j],y=w*a[i+j+k];
                a[i+j]=x+y; a[i+j+k]=x-y;
            }
        }
    }
}
int main()
{
    int n;scanf("%d",&n);int maxx=0;
    for (int i=1;i<=n;i++) scanf("%d",&x[i]),num[x[i]]++,maxx=max(maxx,x[i]),g[x[i]].x=1,g2[x[i]*2].x=1;
    //一个单独算,而且作为double,应该赋值为1而不是++ 
    int m=maxx*3,L=0,nn;
    for (nn=1;nn<=m;nn<<=1) L++;
    for (int i=0;i<=nn;i++) r[i]=(r[i>>1]>>1)|((i&1)<<L-1);

    FFT(g,1,nn); FFT(g2,1,nn);
    for (int i=0;i<=nn;i++) g2[i]=g2[i]*g[i];
    for (int i=0;i<=nn;i++) g[i]=g[i]*g[i]*g[i];
    FFT(g,-1,nn); FFT(g2,-1,nn);
    for (int i=0;i<=nn;i++) a[i]=(int)(g[i].x/nn+0.5),b[i]=(int)(g2[i].x/nn+0.5);
    for (int i=0;i<=nn;i++) a[i]-=b[i]*3;
    for (int i=1;i<=n;i++) a[x[i]*3]+=2;
    for (int i=0;i<=nn;i++) a[i]/=6,num[i]+=a[i];//三个不一样 

    memset(g,0,sizeof(g));
    for (int i=1;i<=n;i++) g[x[i]].x=1;
    FFT(g,1,nn);
    for (int i=0;i<=nn;i++) g[i]=g[i]*g[i];
    FFT(g,-1,nn); 
    for (int i=0;i<=nn;i++) a[i]=(int)(g[i].x/nn+0.5);
    for (int i=1;i<=n;i++) a[x[i]*2]--;
    for (int i=0;i<=nn;i++) a[i]/=2,num[i]+=a[i];//两个不一样 

    for (int i=1;i<=nn;i++)
      if (num[i]) printf("%d %d\n",i,num[i]);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值