P3226 [HNOI2012] 集合选数 题解
想的时候尝试用 \(x\) 和 \(2x,3x\) 形成互斥关系并构造二分图求独立集。事后来看感觉挺唐的。
其实应该是把这些互斥关系构造成矩阵并在矩阵上 DP。好牛的题。
将每个数拆成 \(x=x'\times 2^a\times 3^b(2\nmid x',3\nmid x')\),那么显然对于不同的 \(x'\) 之间的贡献是独立的,不存在互斥关系。只考虑每一个 \(x'\) 类的贡献并将贡献相乘即可。
不妨以 \(x'=1\) 举例,构造出如下矩阵:
1 2 4 8 16 32...
3 6 12 24 48 96...
9 18 36 72 144 288...
如图,一个数既是其左边的数 \(\times 2\),也是其上面的数 \(\times 3\)。那么,我们实际上要求的是在矩阵中选出若干个数使得选出来的数互不相邻的方案数。
容易发现矩阵的行数和列数 \(R,C\) 都是极小的。有 \(R\leq \log_2 n\) 和 \(C\leq \log_3 n\)。那么可以直接状压 DP 转移。设 \(dp_{i,j}\) 表示矩阵第 \(i\) 行,选的状态为 \(j\) 的方案数。考虑转移:
- \(j\) 自身不合法,即 \(j\) 存在相邻两位都是 \(1\):直接 \(dp_{i,j}\) 为 \(0\) 判掉。
- 判定方式:如果
((j<<1)&j)!=0则不合法。
- 判定方式:如果
- 否则,找到上一行合法的 \(k\)。
- 如果
(j&k)!=0表示有上下相邻两位为 \(1\) 的情况,也判掉。 - 否则可以转移 \(dp_{i,j}\leftarrow dp_{i-1,k}\)。
- 如果
但是,当构造矩阵的时候,往往每一行的个数是不相等的,比如对于 \(n=15,x'=1\),有:
1 2 4 8
3 6 12
9
当转移第 \(2\) 行时,状态会对应到第 \(1\) 行的 2 4 8 而不是 1 2 4,那么转移是否就不对了呢?
其实可以换个思路,当作转移时矩阵进行了 reverse,将转移矩阵看作如下这样:
8 4 2 1
12 6 3
9
这样就可以对应上面的转移了。
最后的答案是 \(\sum dp_{R,j}\)。将所有 \(x'\) 的贡献乘起来即可。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5,M=25,K=(1<<21)+5,mod=1e9+7;
int n,ans=1,vis[N],a[M][M],len[N],fl[K],dp[M][K];
void add(int &x,int y){x+=y;if(x>=mod)x-=mod;}
signed main(){
cin>>n;
for(int T=1;T<=n;T++){
if(vis[T])continue;
int pos=1;
for(int i=1;i<=11;i++){
if(i==1)a[i][1]=T;
else a[i][1]=a[i-1][1]*3;
if(a[i][1]>n)break;
len[i]=1,pos=i;
for(int j=2;j<=17;j++){
a[i][j]=a[i][j-1]*2;
if(a[i][j]>n)break;len[i]=j;
}
for(int j=1;j<=len[i];j++)vis[a[i][j]]=1;
}
for(int i=0;i<(1<<len[1]);i++)fl[i]=(!((i<<1)&i));
for(int i=0;i<(1<<len[1]);i++)dp[1][i]=fl[i];
for(int i=2;i<=pos;i++){
for(int j=0;j<(1<<len[i]);j++){
if(!fl[j])continue;
for(int k=0;k<(1<<len[i-1]);k++){
if(!fl[k]||(j&k))continue;
add(dp[i][j],dp[i-1][k]);
}
}
}
int tmp=0;
for(int j=0;j<(1<<len[pos]);j++)
if(fl[j])add(tmp,dp[pos][j]);
ans=ans*tmp%mod;
for(int i=1;i<=pos;i++)
for(int j=0;j<(1<<len[i]);j++)
dp[i][j]=0;
}
cout<<ans;
return 0;
}

浙公网安备 33010602011771号