BZOJ 2734 [HNOI2012] 集合选数 ( 状压DP )

这题的做法还挺神的…


题意
  • 《集合论与图论》这门课程有一道作业题,要求同学们求出 {1,2,3,4,5}\{1, 2, 3, 4, 5\} 的所有满足以下条件的子集:若 xx 在该子集中,则 2x2x3x3x 不能在该子集中。同学们不喜欢这种具有枚举性 质的题目,于是把它变成了以下问题:对于任意一个正整数 N100000N≤100000,如何求出 {1,2,...,N}\{1, 2,..., N\} 的满足上述约束条件的子集的个数(只需输出对 1,000,000,0011,000,000,001 取模的结果),现在这个问题就交给你了。
分析
  • 我们发现对于某个数 xx,如果它选了就有两个数 2x,3x2x,3x不能选,于是我们巧妙的构造一个矩阵如下:
    x3x9x...2x6x18x...4x12x36x............... \begin{matrix} x&3x&9x&...\\ 2x&6x&18x&...\\ 4x&12x&36x&...\\ ...&...&...&...\\ \end{matrix}
  • 那么同一个矩阵中相邻的两个数就是不能选的,一行的选择只受上一行的制约。那么我们只要对于所有不是22的倍数且不是33的倍数的数 xx都构造一个这样的矩阵,那么不同矩阵里的数一定不重复的且互相不制约,根据乘法原理把每个矩阵的方案数乘起来就是答案了
  • 因为要保证数的范围在NN内,那么矩阵的行数和列数都是O(log n)O(log\ n)级别的,那么求方案就可以用简单的状压DP求了。f(i,j)f(i,j)表示当前到了第ii行状态为jj的可行方案数,jj的每个位置上11表示选,00表示不选。
    f(i,j)=[j]j&k=0f(i1,k)\large f(i,j)=[j状态合法]*\sum_{j\&k=0} f(i-1,k)
  • 如果状态中选了大于NN的数或者是状态中同时选了相邻的两个数就不合法。此处 j&k=0j\&k=0 表示这一行与上一行没有同时选中相同的一列。
  • 注意一下不要每次都用 memsetmemset 清零滚动数组,实测用 memsetmemset 慢多了。下面的是用的memsetmemset,上面的使用的forfor清零
    在这里插入图片描述
  • 注意模数
AC代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
const int mod = 1e9 + 1;
int N;


int arr[25][15], st[1<<15], tot, f[2][1<<15];
inline int solve(int s) {
	int n = 1, m = 1;
	memset(arr, -1, sizeof arr);
	arr[0][0] = s;
	while(arr[0][m-1] * 3 <= N) arr[0][m] = arr[0][m-1] * 3, ++m;
	while(arr[n-1][0] * 2 <= N) arr[n][0] = arr[n-1][0] * 2, ++n;
	for(int i = 1; i < n; ++i)
		for(int j = 1; j < m; ++j)
			if(~arr[i-1][j] && arr[i-1][j]*2 <= N) arr[i][j] = arr[i-1][j] * 2;
	tot = 0;
	for(int state = 0; state < (1<<m); ++state) { //预处理出合法状态
		bool flg = 1;
		for(int i = 1; i < m && flg; ++i)
			if((state>>i)&1 && (state>>(i-1))&1) flg = 0;
		if(flg) st[tot++] = state;
	}
	int now = 0;
	for(int j = 0; j < (1<<m); ++j) f[now][j] = 0;
	f[now][0] = 1;
	for(int i = 0; i < n; ++i) {
		now ^= 1;
		for(int j = 0; j < (1<<m); ++j) f[now][j] = 0; //for清零
		for(int pre = 0, state; pre < tot; ++pre) if(f[now^1][state=st[pre]])
			for(int nxt = 0, news; nxt < tot; ++nxt) if(!(state&(news=st[nxt]))) {
				bool flg = 1;
				for(int j = 0; j < m && flg; ++j)
					if(arr[i][j] == -1 && (news>>j)&1) flg = 0; //-1表示这个数大于N
				if(flg) f[now][news] = (f[now][news] + f[now^1][state]) % mod;
			}
	}
	int res = 0;
	for(int i = 0; i < tot; ++i)
		res = (res + f[now][st[i]]) % mod;
	return res;
}

int main () {
	scanf("%d", &N);
	int ans = 1;
	for(int i = 1; i <= N; ++i) if((i%2) && (i%3))
		ans = 1ll * ans * solve(i) % mod;
	printf("%d\n", ans);
}
posted @ 2019-12-14 14:51  _Ark  阅读(108)  评论(0编辑  收藏  举报