【二项式反演&dp】洛谷P4859 已经没有什么好害怕的了 题解

文章讲述了如何利用二项式反演解决一个关于整数数组A和B配对的问题,其中A和B中的数字两两配对,满足A比B大x对,B比A大y对,且x-y=k。文章详细解释了如何通过动态规划和二项式反演来计算符合条件的配对方案数,并给出了相应的代码实现。
摘要由CSDN通过智能技术生成

令我灵光一现的一道二项式反演紫题!

题目

已经没有什么好害怕的了 - 洛谷

精简版题干:

有两组数据A[1..n]和B[1..n],均为1到2n的整数且互不相同。

现将A与B中的数字两两配对。设一种方案中A比B大的数对有x个,B比A大的数对有y个。

问x-y=k的方案数有多少,对10^9+9取模。

思路

题目要求了“一种方案中A比B大的数对有x个,B比A大的数对有y个”,看起来需要枚举x的个数,实则不然

注意到题目还要求了x和y的差值为k,又因为两个数组的整数互不相同,所以不会出现等于的情况,所以可以得到:

$\left\{\begin{matrix} x+y=n\\x-y=k\\x\in \mathbb{Z}\wedge y\in \mathbb{Z} \end{matrix}\right.$

解得:

$x=\frac{n+k}{2} \wedge x \in \mathbb{Z}$

那么x为定值,且当n+k不为偶数时无解

先考虑普通的dp:设$F(i,j)$表示前i个a数组值可以配对j个b数组值的方案数,$les(i)$表示小于a_i的b数组值有多少,那么易得:

$F(i,j)=F(i-1,j)+[les(i)-(j-1)]\times F(i-1,j-1)$

其中les(i)可以用双指针\Theta (n)处理出来:

ll ii=1;
for(int i=1;i<=n;i++)
{
	while(ii<=n&&b[ii]<a[i])ii++;
	les[i]=ii-1;
}

F数组也可以\Theta (n)算出来,初始状态F(0,0)=1

F[0][0]=1;
for(int i=1;i<=n;i++)
{
	F[i][0]=F[i-1][0];
	for(int j=1;j<=i;j++)
	F[i][j]=(F[i-1][j]+(les[i]-(j-1)+mod)%mod*F[i-1][j-1]%mod)%mod;
}

应用二项式反演:

f(i)表示配对a数组值大于b数组值的组数恰好为i,g(i)表示钦定(至少)配对a数组值大于b数组值的组数为i;发现g数组更好算,显然为:

$g(i)=F(n,i)\times (n-i)!$

朴素的理解:其中前i个“精确排”,剩下的n-i个“随便排”

那么就可以用二项式反演由g推出f(钦定与恰好的关系),再现二项式反演结论:

$g(n)=\sum_{i=0}^{n}\binom{n}{i}f(i)\Leftrightarrow f(n)=\sum_{i=0}^{n}(-1)^{n-i}\binom{n}{i}g(i)$

那么:

$f(i)=\sum_{j=i}^{n}\binom{j}{i}(-1)^{j-i}g(j)$

前文提到:$x=\frac{n+k}{2}$,设$I=\frac{n+k}2{}$,那么答案就是:

$f(I)=\sum_{j=I}^{n}\binom{j}{I}(-1)^{j-I}g(j)$

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2222,M=3333,mod=1e9+9;
ll C[M][M],fac[N];
ll n,k,a[N],b[N];
ll les[N],F[N][N],I,g[N],op,fI;
//les(i):小于a(i)的b的数量
//F(i,j):前i个a恰好可以配对j个b的方案数
//F(i,j)=F(i-1,j)+[les(i)-(j-1)]*F(i-1,j-1)
//f(i):配对a大于b的组数恰好为i
//g(i):配对a大于b的组数至少为i
//g(i)=F(n,i)*(n-i)!---前i个精确排,剩下n-i个随便排
//通过二项式反演可以g→f,答案就是f(I),I=(n+k)/2 
ll qpow(ll x,ll k)
{
	ll res=1;
	while(k)
	{
		if(k&1)res=res*x%mod;
		x=x*x%mod;
		k>>=1;
	}
	return res;
}
void get()
{
	for(int i=0;i<M;i++)
	C[i][0]=1;
	for(int i=1;i<M;i++)
	for(int j=1;j<=i;j++)
	C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
	fac[0]=fac[1]=1;
	for(int i=2;i<N;i++)
	fac[i]=fac[i-1]*i%mod; 
}
int main()
{
	get();
	scanf("%lld%lld",&n,&k);
	if((n+k)&1)
	{
		printf("0");
		return 0;
	}
	I=(n+k)/2;
	for(int i=1;i<=n;i++)
	scanf("%lld",&a[i]);
	for(int i=1;i<=n;i++)
	scanf("%lld",&b[i]);
	sort(a+1,a+n+1);
	sort(b+1,b+n+1);
	ll ii=1;
	for(int i=1;i<=n;i++)
	{
		while(ii<=n&&b[ii]<a[i])ii++;
		les[i]=ii-1;
	}
	F[0][0]=1;
	for(int i=1;i<=n;i++)
	{
		F[i][0]=F[i-1][0];
		for(int j=1;j<=i;j++)
		F[i][j]=(F[i-1][j]+(les[i]-(j-1)+mod)%mod*F[i-1][j-1]%mod)%mod;
	}
	for(int i=1;i<=n;i++)
	g[i]=F[n][i]*fac[n-i]%mod;
	for(int j=I;j<=n;j++)
	{
		if((j-I)&1)op=-1;
		else op=1;
		fI=((fI+op*C[j][I]%mod*g[j]%mod)%mod+mod)%mod;
	}
	printf("%lld",fI);
	return 0;
}

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值