P3226 [HNOI2012]集合选数 状压dp(思维题)

题目

题目大意

《集合论与图论》这门课程有一道作业题,要求同学们求出{1, 2, 3, 4, 5}的所有满足以 下条件的子集:若 x 在该子集中,则 2x 和 3x 不能在该子集中。同学们不喜欢这种具有枚举性 质的题目,于是把它变成了以下问题:对于任意一个正整数 n≤100000,如何求出{1, 2,..., n} 的满足上述约束条件的子集的个数(只需输出对 1,000,000,001 取模的结果),现在这个问题就 交给你了。

输入格式

只有一行,其中有一个正整数 n,30%的数据满足 n≤20。

输出格式

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

算法分析

  • 这个题的算法很妙 这个题的要求是如果x在集合中那么2x 3x 不能在集合中
    乍一看似乎毫无思路只能打表 但是我们仔细分析题意之后 可以发现一个表格
1 3 9
2 6 18
4 12 36
8 24 72
看到这个表格能想到什么 暑假集训Day2 互不侵犯(状压dp)
如果选了当前位置的数 那么上下左右都不能再选 这不就是一个可以上下左右但是不能斜着攻击的国王吗
那就很容易了
先算一下数据大小 最多有18行 11列 所以我们可以用状压dp
  • 但是这个和国王的题还是有区别的 国王要求输出最多的方案数 而该题要求输出共有多少种选法,根据乘法原理(不知道是啥的去找数学老师跪搓衣板)
    所以就是将答案累乘就好了
    但是直接寻找这样一个表格然后累乘就对了吗?
    并不是 因为观看这样的表格 我们可以发现5并不在这个表格里面 所以我们还需要累乘多个这样的表格
  • 如何累乘表格? 自己算然后暴力 用一个数组记录就可以了
    用mark数组来标记这个表格是否出现过
  • 因此我们的算法雏形就出来了(状压过程可以参见暑假集训Day2 互不侵犯(状压dp)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e6+10,mod = 1000000001;
ll tot=1;
int bin[20],n,a[20][20],cnt[20],b[20],f[20][2048];
bool mark[maxn];

int cal(int x){
	memset(b,0,sizeof(b));
	a[1][1] = x;
	for(int i = 2;i <= 18;++i)
		if(a[i-1][1]*2 <= n)a[i][1] = a[i-1][1] * 2;
		else a[i][1] = n+1;
	for(int i = 1;i <= 18;++i)
		for(int j = 2;j <= 11;++j)
			if(a[i][j-1]*3 <= n)a[i][j] = a[i][j-1]*3;
			else a[i][j] = n+1;
	for(int i = 1;i <= 18;++i)
		for(int j = 1;j <= 11;++j)
			if(a[i][j] <= n){b[i] += bin[j-1];mark[a[i][j]] = 1;}
	for(int i = 0;i <= 18;++i)
		for(int j = 0;j <= b[i];++j)
			f[i][j] = 0;
	f[0][0] = 1;//记得初始化 不然都是0
	for(int i = 0;i < 18;++i)//枚举当前行数
		for(int j = 0;j <= b[i];++j)//枚举当前行的状态
			if(f[i][j])//如果当前行状态已经推过 小优化大约能省10ms
				for(int k = 0;k <= b[i+1];++k)//枚举下一行状态
					if((j&k)==0 && ((k&(k>>1)) == 0))f[i+1][k] = (f[i][j]+f[i+1][k])%mod;//记得取模 不会的去找数学老师跪搓衣板
	return f[18][0];//返回第18的值
}

int main(){
	//bin[0] = 1;for(int i = 1;i <= 20;++i)bin[i] = bin[i-1]<<1;
        for(int i = 0;i <= 20;++i)bin[i] = 1<<i;
	scanf("%d",&n);
	for(int i = 1;i <= n;++i)
		if(!mark[i])tot = (tot*cal(i))%mod;
	printf("%lld",tot);
	return 0;
}

下面注意一些细节问题:

时间复杂度(玄学) 但是我们还是可以通过一些优化来提高我们的时间效率的(不要吐槽为啥我自己代码没优化 问就是能AC的就是好代码)
比如:可以记录当前x的最大行数 和 每一行的最大列数 后面的枚举边界会变小 (这个优化不小大约能有30ms左右
然后就是一些比较小的优化 dp时候剪枝 初始化bin的时候不用bin[i-1]的位运算而是用1<<i(实测优化2~5ms)

谢谢观看
点个关注

posted @ 2020-06-27 20:45  HISKrrr  阅读(255)  评论(0编辑  收藏  举报