[HNOI2012] 集合选数

发布时间 2023-07-18 09:29:35作者: Sonnety

[HNOI2012] 集合选数

题目描述

《集合论与图论》这门课程有一道作业题,要求同学们求出 \(\{ 1, 2, 3, 4, 5 \}\) 的所有满足以下条件的子集:若 \(x\) 在该子集中,则 \(2x\)\(3x\) 不能在该子集中。

同学们不喜欢这种具有枚举性质的题目,于是把它变成了以下问题:对于任意一个正整数 \(n \le 10^5\),如何求出 \(\{1,2,\ldots ,n\}\) 的满足上述约束条件的子集的个数(只需输出对 \(10^9+1\) 取模的结果),现在这个问题就交给你了。

输入格式

只有一行,其中有一个正整数 \(n\)\(30 \%\) 的数据满足 \(n \le 20\)

输出格式

仅包含一个正整数,表示 \(\{1,2,\ldots ,n\}\) 有多少个满足上述约束条件的子集。

样例 #1

样例输入 #1

4

样例输出 #1

8

提示

【样例解释】

\(8\) 个集合满足要求,分别是空集,\({1}\)\(\{1,4\}\)\(\{2\}\)\(\{2,3\}\)\(\{3\}\)\(\{3,4\}\)\(\{4\}\)

【数据范围】

对于 \(30 \%\) 的数据,\(n \le 20\)
对于 \(100 \%\) 的数据,\(1 \le n \le 10^5\)

题意概括

就是让你在\(1-n\)里面选数,要求选了\(x\)就不能选\(2x,3x\),求最大方案数。

思路历程:

私货:当代互联网天下第一!!!

1.设计状态

显然我们应该设计是否选择\(x\)为状态,而是否满足条件在转移前判断。

满足条件是不满足条件的对立事件,所以我们可以先把不能在一起的点做出来,这样我们就把问题转化为了不能选择矩阵中相邻的点:

\(1\)举例子,\(n=100\)

1  2  4  8   16  32  64
3  6  12 24  48  96
9  18 36 72
27 54
81

可见矩阵横着是乘\(2\),竖着是乘\(3\),均小于\(n=100\)

但是我们这个显然是不全的啊,要满足条件又不止满足\(1\)

我们可以发现,这个矩阵里面,\(2\)\(3\)这样的\(1*2\)的倍数或\(1*3\)的倍数的数已经选入,不用再管,但是像\(5\)这样的数,我们还需要继续再列一个矩阵。

因此我们用一个特判数组,只要矩阵里出现过这个点就是\(true\),否则再列一个矩阵。

matrix pre
inline void pre(int x){
	for(int i=1;i<=lg3n;++i){
		if(i==1)	a[i][1]=x;
		else	a[i][1]=a[i-1][1]*3;
		if(a[i][1]>n)	break;
		yend=i,line[i]=1,judge[a[i][1]]=true;
		for(int j=2;j<=lg3n;++j){
			a[i][j]=a[i][j-1]<<1;
			if(a[i][j]>n)	break;
			line[i]=j;
			judge[a[i][j]]=true;
		}
		len[i]=(1<<line[i])-1;
	}
}

for(int i=1;i<=n;++i){
	if(!judge[i]){
		pre(i);
	}
}

而对于状态,我们不能选择具有在矩阵中相邻的点的状态,可以进行初始化:

pre2
for(int i=0;i<=maxn-1;++i){
		if( (i<<1) & i)	g[i]=0;
		else	g[i]=1;
	}

2.设计转移

我们的状态是以一个数扩展而成的矩阵,状态也应该是这样的。

因此我们的状态建立在以某数\(x\)构造的矩阵上,有\(f_{i,j}\)表示矩阵内\(1-i\)行中第\(i\)行选择状态\(j\)有多少种方案,显然有转移:

\[\begin{aligned} & if(上一行的状态k合法 并且 上一行的状态k与改行枚举的状态j没有重合点)\\ & f_{i,j}=f_{i,j}+f_{i-1,k} \end{aligned} \]

最后,因为我们每个矩阵是不会存在交叉部分的,比如\(1\)的矩阵里可以选出\(1,2,3\)\(5\)的矩阵里可以选出\(5,10,15\),显然我们可以选出\(1,2,3,5\),或者\(1,2,3,5,10\)不同矩阵之间的方案数是遵循乘法原则的。

代码实现:

Miku's Code
#include<bits/stdc++.h>
using namespace std;

typedef long long intx;
const int maxn=1e5+50,mod=1e9+1;
const int lg2n=18,lg3n=13;

intx n,ans,num;
intx g[(1<<lg2n)+2];
intx a[lg2n+2][lg2n+2],line[lg2n+2],len[lg2n+2],yend;
bool judge[maxn];
intx f[lg2n+2][(1<<lg2n)+2];

inline void pre(intx x){							//矩阵初始化 
	for(intx i=1;i<=lg3n;++i){					//第一列初始化 
		if(i==1)	a[i][1]=x;
		else	a[i][1]=a[i-1][1]*3;
		if(a[i][1]>n)	break;
		yend=i,line[i]=1,judge[a[i][1]]=true;	//yend表示第一列的最后一行,line表示某一行的最后一列 
		for(intx j=2;j<=lg2n;++j){
			a[i][j]=a[i][j-1]*2;
			if(a[i][j]>n)	break;
			line[i]=j,judge[a[i][j]]=true;
		}
		len[i]=(1<<line[i])-1;					//len表示某一行最大状态 
	}
}

inline intx work(){
	num=0;
	for(intx i=0;i<=len[1];++i){
		f[1][i]=g[i];
	}
	for(intx i=2;i<=yend;++i){
		for(intx j=0;j<=len[i];++j){
			if(!g[j])	continue;
			f[i][j]=0;
			//一定要记得清空啊,或者说在pre()里面memset,但是用到在清省复杂度 
			for(intx k=0;k<=len[i-1];++k){
				if( g[k] && ((k & j)==0) ){
					f[i][j]+=f[i-1][k];
					f[i][j]%=mod;
				}
			}
		}
	}
	for(intx i=0;i<=len[yend];++i){
		num+=f[yend][i];
		num%=mod;
	}
	return num;
}

int main(){
	scanf("%lld",&n);
	ans=1;
	for(intx i=0;i<=(1<<lg2n)-1;++i){
		if( (i<<1) & i)	g[i]=0;
		else	g[i]=1;
	}
	for(intx i=1;i<=n;++i){
		if(!judge[i]){
			pre(i);
			int s=work();
			ans=ans*s%mod;
		}
	}
	printf("%lld",ans);
	return 0;
}