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)

    谢谢观看
    点个关注

    如初见 与初见
  • 相关阅读:
    Android 编程下 Eclipse 恢复被删除的文件
    Android 编程下背景图片适配工具类
    Android 编程下 Managing Your App's Memory
    Android 编程下代码之(QQ消息列表滑动删除)
    Android 编程下 Canvas and Drawables
    Android 编程下 AlarmManager
    Android 编程下去除 ListView 上下边界蓝色或黄色阴影
    Java 编程下字符串的 16 位、32位 MD5 加密
    C#枚举类型和int类型相互转换
    MVC和普通三层架构的区别
  • 原文地址:https://www.cnblogs.com/HISKrrr/p/13199761.html
Copyright © 2011-2022 走看看