zoukankan      html  css  js  c++  java
  • 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)

    谢谢观看
    点个关注

    如初见 与初见
  • 相关阅读:
    Python之路【第四十五篇】:django日更
    Python之路【第四十四篇】:django日更
    C++ 调用动态链接库
    博客园美化
    postgresql中的UUID
    使用rustup安装rust环境
    MySQL 查询做排名
    Docker
    CentOS安装Docker和基础操作
    切换CentOS7的yum源为阿里源
  • 原文地址:https://www.cnblogs.com/HISKrrr/p/13199761.html
Copyright © 2011-2022 走看看