【LOJ6072】苹果树【折半搜索】【矩阵树定理】【二项式反演】

这篇博客探讨了一个组合计数问题,涉及树形结构的好坏点以及权值限制。文章指出常规的容斥方法在此问题中存在误差,并详细解释了如何修正定义以正确计算满足条件的点的方案数。通过矩阵树定理计算特定连边方案,并使用二项式反演进行求解。博主还提到了钦定有用点和钦定没用点的不同处理方式,并给出了相应公式。最后,提供了算法实现及复杂度分析。

题意:有好坏两种点共 nnn 个,每个好点有权值,把这 nnn 个点连成一棵树,一个好点为有用的当且仅当它至少与一个好点相邻,求所有有用的点的权值和不超过 limlimlim 的方案数。

n≤40n\leq 40n40

这题网上的容斥方法基本都是假的……

发现至少与一个好点相邻不好处理,但只能与坏点相邻比较方便,所以大概是个容斥。

设有 mmm 个好点, f(k)f(k)f(k) 表示钦定 kkk 个点,有用的点只能在这 kkk 个当中选,相当于钦定其他 m−km-kmk 个是没用的。(以下简称“可以有用”。)g(k)g(k)g(k) 表示恰好有 kkk 个有用的点。

那么有

f(k)=∑i=0k(ki)g(i)f(k)=\sum_{i=0}^k\binom{k}{i}g(i)f(k)=i=0k(ik)g(i)

钦定 kkk 个可以有用的点后,把所有好点和坏点连边,有用的点和坏点内部连边,就可以算出 fff

然后你会发现你假了。

第一,你算矩阵树只能钦定固定的 kkk 个可以有用,而 fff 的定义是任意钦定,钦定这个动作本身的方案是算在其中的。

第二,因为这 kkk 个是不固定的,你算出了 ggg 也没法算答案……

所以必须要改下定义。

定义 f(k)f(k)f(k)已经确定了 kkk 个点可以有用的连边方案。也就是具体是哪 kkk 个点已经帮你钦定好了,你只需要管连边的方案。

g(k)g(k)g(k)已经确定恰好有 kkk 个点有用的连边方案,具体含义同上。

先不管权值的事情,对于一个钦定可以有用的方案,建出图后考虑它的一棵生成树,把钦定的 kkk 个点中全部连的坏点的拿出来,假设有 iii 个,就对应了一种 g(i)g(i)g(i) 的方案。

所以式子是一样的

f(k)=∑i=0k(ki)g(i)f(k)=\sum_{i=0}^k\binom{k}{i}g(i)f(k)=i=0k(ik)g(i)

二项式反演

g(k)=∑i=0k(−1)k−i(ki)f(i)g(k)=\sum_{i=0}^k(-1)^{k-i}\binom{k}{i}f(i)g(k)=i=0k(1)ki(ik)f(i)

fff 因为是矩阵树算的,已经钦定好了。对于所有 g(k)g(k)g(k),乘上钦定本身的方案的和就是答案。钦定本身可以折半搜索+two pointer算出。

复杂度 O(2n/2+n4)O(2^{n/2}+n^4)O(2n/2+n4)

顺带一提,如果是钦定没用的点,式子是长这样的

f(k)=∑i=km(m−ki−k)g(i)f(k)=\sum_{i=k}^m\binom{m-k}{i-k}g(i)f(k)=i=km(ikmk)g(i)

而不是

f(k)=∑i=km(ik)g(i)f(k)=\sum_{i=k}^m\binom{i}{k}g(i)f(k)=i=km(ki)g(i)

原因同上

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cctype>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long ll;
const int MOD=1e9+7;
inline int add(const int& x,const int& y){return x+y>=MOD? x+y-MOD:x+y;}
inline int qpow(int a,int p)
{
	int ans=1;
	while (p)
	{
		if (p&1) ans=(ll)ans*a%MOD;
		a=(ll)a*a%MOD,p>>=1;
	}
	return ans;
}
int C[45][45],n,lim,a[45],cnt[45],f[45];
vector<int> L[45],R[45];
void dfs(vector<int>* v,int cur,int pos,int k,int sum)
{
	if (sum>lim) return;
	if (cur>pos) return v[k].push_back(sum);
	dfs(v,cur+1,pos,k,sum);
	dfs(v,cur+1,pos,k+1,sum+a[cur]);
}
inline int calc(const vector<int>& a,const vector<int>& b)
{
	int pos=b.size(),ans=0;
	for (int i=0;i<(int)a.size();i++)
	{
		while (pos&&a[i]+b[pos-1]>lim) --pos;
		ans=(ans+pos)%MOD;
	}
	return ans;
}
int g[45][45];
inline int det(int n)
{
	int ans=1;
	for (int i=1;i<n;i++) for (int j=1;j<n;j++) (g[i][j]<0)&&(g[i][j]+=MOD);
	for (int i=1;i<n;i++)
	{
		int pos=i;
		for (;!g[pos][i]&&pos<n;++pos);
		if (pos==n) return 0;
		if (pos>i) swap(g[i],g[pos]),ans=MOD-ans;
		ans=(ll)ans*g[i][i]%MOD;
		for (int j=i+1;j<n;j++)
		{
			int t=(ll)g[j][i]*qpow(g[i][i],MOD-2)%MOD;
			for (int k=i;k<n;k++) g[j][k]=(g[j][k]-(ll)t*g[i][k]%MOD+MOD)%MOD;
		}
	}
	return ans;
}
int main()
{
	scanf("%d%d",&n,&lim);
	C[0][0]=1;
	for (int i=1;i<=n;i++)
	{
		C[i][0]=1;
		for (int j=1;j<=i;j++) C[i][j]=add(C[i-1][j-1],C[i-1][j]);
	}
	for (int i=1;i<=n;i++) scanf("%d",&a[i]);
	sort(a+1,a+n+1);
	int bad=0;
	for (;a[bad+1]==-1;++bad);
	int mid=(bad+1+n)>>1;
	dfs(L,bad+1,mid,0,0),dfs(R,mid+1,n,0,0);
	int m=n-bad;
	for (int i=0;i<=m;i++) sort(L[i].begin(),L[i].end()),sort(R[i].begin(),R[i].end());
	for (int k=0;k<=m;k++)
	{
		for (int i=0;i<=k;i++) cnt[k]=add(cnt[k],calc(L[i],R[k-i]));
		memset(g,0,sizeof(g));
		for (int i=1;i<=bad;i++)
			for (int j=1;j<i;j++)
			{
				++g[i][i],++g[j][j];
				--g[i][j],--g[j][i];
			}
		for (int i=bad+1;i<=bad+k;i++)
			for (int j=1;j<i;j++)
			{
				++g[i][i],++g[j][j];
				--g[i][j],--g[j][i];
			} 
		for (int i=bad+k+1;i<=n;i++)
			for (int j=1;j<=bad;j++)
			{
				++g[i][i],++g[j][j];
				--g[i][j],--g[j][i];	
			}
		f[k]=det(n);
	}
	int sum=0;
	for (int k=0;k<=m;k++)
	{
		int ans=0;
		for (int i=0;i<=k;i++) ans=(ans+(((i-k)&1)? -1ll:1ll)*C[k][i]*f[i])%MOD;
//		cerr<<ans<<'\n';
		sum=(sum+(ll)cnt[k]*ans)%MOD;
	}
	printf("%d\n",(sum+MOD)%MOD);
	return 0;
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值