Educational Codeforces Round 76 G Divisor Set

Divisor Set

结论+生成函数优化dp+NTT+启发式

个人感觉这道题非常好!非常精妙!

 

1.首先我们要知道一个结论:

我们把这个数的约数按拥有的质因子数(可重)划分,第i类约数表示有i个可以重复的质因子

然后我们从每一级的约数a向它的下一级约数b(a%b==0 且b只比a少一个质因子)连边,那么这个图显然是个DAG

显然如果我想找最大的可选的约数,那么从n级到1级的任意一条链上只能选一个值

然后经证明可得我选n/2级的时候所选的数集最大

证明:https://pure.tue.nl/ws/files/4373475/597494.pdf

 

2.那么现在问题就转变为我想求出每一级有多少个点(即约数)

这就是一个 生成函数优化dp 经典的问题了

对于每个不同得到质因子,假设是第i种质因子,并且个数有k个

我们设g(x)=1+x^1+x^2+,,,,,,+x^k 为这种因子的多项式

那么最后的答案多项式一定是形如f(x)=a[1]+a[2]*x^1+a[3]*x^2+......+a[n+1]*x^n这样的多项式

并且f(x)为所有g(i)的卷积

所有g(i)的卷积我们显然可以用NTT快速求出

 

3当然如果暴力NTT的话显然是n^2log的(因为你可能每次都取出最长的那个多项式和最短的多项式合并,这样每次都是nlog,最多做n次)

那么我们考虑用启发式的思想来优化它

我们可以很快想到可以每次找出长度最小的那2个子串来做NTT,这样每做一次NTT后,一个多项式的长度至少会增大一倍。

那么考虑最初的某个串,它最多增大log次就会到最初的长度,因为最后卷积出来的函数长度不会超过n(总长才n)

所以均摊下来是log的,然后算上NTT的一个log,总复杂度就优化到nlog^2

 

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <queue>
#include <utility>
#define fr first
#define sc second
#define mp make_pair
#define pb push_back
using namespace std;
const int P=998244353;
const int N=1e6+10;
const int maxn=1e6+10;
int fexp(int a,int n)
{
	int res=1;
	for (;n;n>>=1,a=1ll*a*a%P) if (n&1) res=1ll*res*a%P;
return res;
}
struct da
{
	int n,w[2][N];
	void init(int lim)
	{
		for (n=1;n<lim;n<<=1);
		w[0][0]=w[0][n]=1;
		int g=fexp(3,(P-1)/n);
		for (int i=1;i<n;i++) w[0][i]=1ll*w[0][i-1]*g%P;
		for (int i=0;i<=n;i++) w[1][i]=w[0][n-i];
	}
	void ntt(int *a,int v)
	{
		for (int i=0,j=0;i<n;i++)
		{
			if (i>j) swap(a[i],a[j]);
			for (int l=n>>1;(j^=l)<l;l>>=1);
		}
		for (int l=2;l<=n;l<<=1)
		{
			int m=l>>1;
			for (int i=0;i<n;i+=l)
			 for (int k=0;k<m;k++)
			 {
			 	int t=1ll*w[v][n/l*k]*a[i+k+m]%P;
			 	a[i+k+m]=(a[i+k]-t>=0)?(a[i+k]-t):(a[i+k]-t+P);
			 	a[i+k]=(a[i+k]+t<P)?(a[i+k]+t):(a[i+k]+t-P);
			 }
		}
		if (!v) return;
		int rv=fexp(n,P-2);
		for (int i=0;i<n;i++) a[i]=1ll*a[i]*rv%P;
	}
	void work(int *a,int *b,int *c,int m)
	{
		init(m); ntt(a,0); ntt(b,0);
		for (int i=0;i<n;i++) c[i]=1ll*a[i]*b[i]%P;
		ntt(c,1);
	}
}NTT;
int n,lc[10000010],cnt,v[maxn],a[maxn],b[maxn],c[maxn];
vector<int>d[maxn];
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >q;
int main()
{
	scanf("%d",&n);
	for (int i=1;i<=n;i++)
	{
		int xx=0; scanf("%d",&xx);
		if (!lc[xx]) lc[xx]=++cnt,d[cnt].pb(1);
		d[lc[xx]].pb(1);
	}
	for (int i=1;i<=cnt;i++) q.push(mp(d[i].size(),i));
	while (q.size()!=1)
	{
		int id1=q.top().sc,len1=d[id1].size(); q.pop();
		int id2=q.top().sc,len2=d[id2].size(); q.pop();
		for (int i=0;i<len1;i++) a[i]=d[id1][i];
		for (int i=0;i<len2;i++) b[i]=d[id2][i];
		NTT.work(a,b,c,len1+len2);
		for (int i=0;i<=2*(len1+len2)+100;i++) a[i]=0;
		for (int i=0;i<=2*(len1+len2)+100;i++) b[i]=0;
		d[id1].clear();
		for (int i=0;i<len1+len2-1;i++) d[id1].pb(c[i]);
		q.push(mp(len1+len2-1,id1));
		for (int i=0;i<=2*(len1+len2)+100;i++) c[i]=0;
	}
	int kk=q.top().sc;
	printf("%d\n",d[kk][n>>1]);
return 0;
}

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值