zoukankan      html  css  js  c++  java
  • 状压DP之集合选数

    题目

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

    输入格式

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

    输出格式

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

    样例

    样例输入

    4
    

    样例输出

    8
    

    样例解释

    有8 个集合满足要求,分别是空集,{1},{1,4},{2},{2,3},{3},{3,4},{4}。

    上思路

    • 这道题真的很恶心,卡了我很长时间,在做题之前,我们需要有一个转变,x存在则2x,3x不能存在,我们建立一个二维矩阵,如下图所示
    x 3x 9x 27x
    2x 6x 18x 54x
    4x 12x 36x 108x

    由图中我们可以看出我们选出其中一个数,他的上下左右四个数都不能再选,这让我想到了炮兵营地这道题,但是还是有一定的不同,这里是让我们输出方案数,那道题是用来输出最优解的,不提炮兵营地,单讲这道题;

    • 我们可以通过构建不同的矩阵来求解最优,对于矩阵的处理,我们对已经处理过的矩阵不再重复处理,我们用矩阵的第一个数来识别矩阵是否出现过,维护(flag)数组,如果当前矩阵的第一个数已经出现在其他矩阵,则(flag)(1),不需要再进行多余处理,那么如何判断该数是否出现过呢?很简单,在构图的时候顺便将出现过的数字的(flag)记录为(1),为什么要这样做?我们完全可以直接构建第一位为(1)的矩阵啊,但是,仔细观察,不难发现我们前面的那个矩阵有些数字是不包括的,比如(5),所以这个判断是完全有用的;
    • 对于一个未出现过的数字,我们以它为第一个数构建二维矩阵,在构建时应记录最大边界,如果大于(n),则直接跳出,为什么说是最大边界呢?因为显然我们每一行的开始数字不同,跳出时自然不同,假设以当前数构建的矩阵行数为(m),最大列数为(o),那么我们就可以在一个较小的范围内寻找每一行的边界,我们维护(limit)数组来记录边界状态,边界内用(1)表示,边界外用(0)表示,则构成了一个二进制数计入数组;
    • 初始化,我们先预处理第一行在合法状态下的方案数即(f[1][0~(1<<0)-1]=1),即为第一行合法状态的方案数都为(1)(显然),合法状态必然满足((!(i&(i<<1)) && !(i&(i>>1)) && !(limit[1]&i))),左右不冲突,不能越过当前行(即第一行)边界;
    • (DP)时间到了,我们定义DP数组(f[i][j])表示第(i)(j)状态的的方案数,首先,我们枚举行数(因为第一行已经处理,故从第二行开始),然后枚举当前状态,判断当前状态合法与否,合法则继续枚举可以继续枚举上一状态,即(i-1)行状态,判断(i-1)行状态是否合法,判断(i-1)行与(i)行状态是否冲突,满足情况的话我们就可以转移了,(f[i][j]=(f[i][j]+f[i-1][k])%mod),将上一状态叠加到当前状态;
    • 最后,还需要将第m行中所有合法状态枚举一遍,叠加到(sum)中即可(记得取模);
    • 还有一点考虑到时间复杂度,不要直接(memset)把整个数组初始化,会被卡掉(痛的教训),我们只需要在需要的位置将相应的数据变为(0)即可(尤其是(f)数组和(limit)数组),否则可能会(TLE)

    接下来就是代码了

    #include<cstdio>
    #include<iostream>
    #include<cstring>
    #define ll long long 
    using namespace std;
    const int maxn=1e5+10;
    const int mod=1e9+1;
    ll f[25][1<<20],ans;
    bool flag[maxn];
    int n,map[25][25];
    inline ll get(int x){//处理
    	int limit[20];//记录边界
    	map[1][1]=x;//记录初始点
    	int m=1,o=1;
    	for(int i=1;i<=20;i++){
    		if(i>1){
    			if(map[i-1][1]*2>n)break;//超过n,跳出
    			map[i][1]=map[i-1][1]*2;
    			flag[map[i][1]]=1;//标记
    		}
    		m=i;
    		for(int j=2;j<=20;j++){
        		      if(map[i][j-1]*3>n) break;//超过n,跳出
           		      o=max(o,j);//求最大列数
           		      map[i][j]=map[i][j-1]*3;
            	      flag[map[i][j]]=1;//标记
        	      }
    	}
    	int yy=0;//记录该行个数
    	for(int i=1;i<=m;i++){
    		for(int j=1;j<=o;j++){
    			if(!map[i][j])break;
    			else map[i][j]=0;
    			yy=j;
    		}
    		limit[i]=0;
    		for(int j=yy+1;j<=o;j++)
    			limit[i]|=(1<<(o-j));//修改该行边界状态
    	}
    	for(int i=0;i<(1<<o);i++)//初始化
    		if(!(i&(i<<1)) && !(i&(i>>1)) && !(limit[1]&i))
    			f[1][i]=1;
    	for(int i=2;i<=m;i++)//枚举行数
    		for(int j=0;j<(1<<o);j++)//枚举当前行状态
    			if(!(j&(j<<1)) && !(j&(j>>1)) && !(limit[i]&j)){//判断合法与否
    				f[i][j]=0;//初始化,优化!!!!
    				for(int k=0;k<(1<<o);k++)//枚举上一状态
    					if(!(k&(k<<1)) && !(k&(k>>1)) && !(limit[i-1]&k) && !(j&k))//判断上一状态合法与否,上一状态和现状态是否冲突
    						f[i][j]=(f[i][j]+f[i-1][k])%mod;//叠加上一状态方案数
    			}
    	int sum=0;
    	for(int i=0;i<(1<<o);i++){//求第m行时,所有状态的最优解的和
    		if(!(i&(i<<1)) && !(i&(i>>1)) && !(i&limit[m])){
    			sum=(sum+f[m][i])%mod;
    		}
    	} 
    	return sum;
    } 
    int main(){
    	scanf("%d",&n);
    	ans=1;
    	for(int i=1;i<=n;i++)
    		if(!flag[i])//判断是否被标记
    			ans=(ans*get(i))%mod;
    	printf("%lld
    ",ans);
    }
    
    
  • 相关阅读:
    [LeetCode] 137. Single Number II
    [LeetCode] 136. Single Number
    [LeetCode] 678. Valid Parenthesis String
    [LeetCode] 605. Can Place Flowers
    [LeetCode] 419. Battleships in a Board
    [LeetCode] 1002. Find Common Characters
    [LeetCode] 912. Sort an Array
    [LeetCode] 350. Intersection of Two Arrays II
    [LeetCode] 349. Intersection of Two Arrays
    [LeetCode] 820. Short Encoding of Words
  • 原文地址:https://www.cnblogs.com/soda-ma/p/13197883.html
Copyright © 2011-2022 走看看