题解:SP707 TFSETS - Triple-Free Sets
建议先写此题之前先完成本题的弱化版 P3226。
题意
给定一个集合 \(\{1,2,3,\cdots,n\}\),求该集合内的合法子集数量对 \(10^9+1\) 取模的值。一个集合 \(S\) 合法,当且仅当不存在 \(x,y \in S\) 使得 \(x=2y\) 或者 \(x=3y\)。
题意转化
可以构造这样一个网格图,对于每个结点 \(x\) 左边相连结点 \(y\) 以及上方相连的结点 \(z\) 满足 \(x=2y=3z\) 且 \(x\le n\)。
举个例子,以 \(1\) 为左上角,\(n=12\) 形成的网格图长这样:
1 2 4 8
3 6 12
9
但可以发现,这个网格图中没有包含 \(5,7,10,11\),所以实际上存在多个网格图,每个网格图内的结点互不相同。所以 \(n=12\) 的网格图实际是这样的:
1 2 4 8
3 6 12
9
5 10
7
11
注意每个网格图的左上角不一定是质数,其实是没 \(2\) 和 \(3\) 作为因子的数,比如说 \(35\) 也可以。
那么题目就可以转化为:
给定若干个网格图,结点 \(u\) 可以染色仅当其上下左右四个结点都没有染色,求染色方案数。
初步思路
注意到每个网格图的大小都是 \(O(\log_2n \times \log_3n)\) 的,也就是不超过 \(17 \times 11\),单个网格图的大小可能连一半都没到。行和列的长度也很小,因此可以考虑状压 DP。
为了更好理解,我们对每一行状压,状态总数 \(S=2^{17}\)。用每个二进制位表示该行的染色情况,第 \(i\) 位为 \(1\) 表示已染色,\(0\) 表示未染色。可以设计状态 \(dp_{i,s}\) 表示第 \(i\) 行染色情况为 \(s\) 的染色方案数。
然后考虑转移,首先需保证当前状态合法,即没有相邻的结点被染色或是 \(s\) 的二进制位中不存在相邻的 \(1\)。然后就是枚举上一行的所有合法状态 \(L\),然后算贡献即可。得转换方程:
网格图之间互不影响,每个网格图跑一遍状压然后根据乘法原理相乘即可。
可以写出以下代码:
int solve(int x) {
int u = 0;
for(int i = x ; i <= n ; i *= 3) { // 对于网格图,只需记录其大小,内容实际不重要
a[++ u] = 0;
for(int j = i ; j <= n ; j <<= 1) a[u] ++;
}
for(int s = 0 ; s < (1 << a[1]) ; s ++) dp[1][s] = !(s & (s >> 1)); // 初始化第一行
for(int i = 2 ; i <= u ; i ++) {
int S = (1 << a[i]) - 1 , L = (1 << a[i - 1]) - 1; // 计算当前这行和上一行的状态总数
for(int s = 0 ; s <= S ; s ++) {
if(s & (s >> 1)) continue; // 筛去有相邻 1 的状态
dp[i][s] = 0;
for(int l = 0 ; l <= L ; l ++) {
if((l & (l >> 1)) || (l & s)) continue; //筛去有相邻 1 的状态以及上下有相邻 1 的状态
dp[i][s] += dp[i - 1][l]; // 转移
dp[i][s] %= mod;
}
}
}
int h = 0 , m = (1 << a[u]);
for(int s = 0 ; s < m ; s ++) { // 答案为最后一行的所有合法状态所记录的和
if(!(s & (s >> 1))) h += dp[u][s] , h %= mod;
}
return h;
}
void into() {
for(int i = 1 ; i <= n ; i ++) {
if((i % 2 != 0) && (i % 3 != 0)) { // 枚举每个网格图
ans *= solve(i); // 乘法原理计算答案
ans %= mod;
}
}
}
可以发现,每一个结点数大于 \(1\) 的网格图大小都不超过上一个网格图的一半,因此单组数据的时间复杂度为 \(O(S^2\log_3n)\),可以通过单测的 P3226。
但总复杂度为 \(O(TS^2\log_3n)\),而 \(T\) 最大可达 \(500\),所以需要进行优化。
优化思路
最理想的优化就是提前预处理,只跑一轮状压 DP,然后通过预处理的信息应付每组数据。
不难发现,每个网格图只与其形状与大小有关,和其实际内容不重要。我们不妨设 \(g_{i,j,k}\) 表示以 \(k\) 为左上角的网格图中第 \(i\) 行第 \(j\) 列的结点值,可以发现 \(g_{i,j,k}=k\times 2^{j-1}\times 3^{i-1}\),那么就可以得出 \(g_{i,j,1}=\frac{g_{i,j,k}}{k}\)。而不超过 \(n\) 的最大 \(g_{i,j,k}\) 就可以看作网格的右下角,那么其在以 \(1\) 为左上角的网格图中,其右下角就为 \(\frac{g_{i,j,k}}{k}\) 即 \(\lfloor\frac{n}{i}\rfloor\)。
综上可以得出结论:对于一个以 \(i\) 为左上角的网格图,其等价与在 \(n=\lfloor\frac{n}{i}\rfloor\) 时以 \(1\) 为左上角的网格图。
举个例子,当 \(n=20\) 时,以 \(5\) 为左上角的网格图是这样的:
5 10 20
15
它实际等价与在 \(n=\lfloor\frac{20}{5}\rfloor=4\) 时以 \(1\) 为左上角的网格图:
1 2 4
3
那么我们就用 \(f_i\) 记录以 \(i\) 为右下角的染色方案数,\(b_i\) 表示网格图中的结点值按升序排序后的结果。我们依次往网格图中加点,每加一次点就跑一次状压 DP 计算答案。假设加入的点 \(b_i\) 在第 \(x\) 行,那么就只需要从第 \(x\) 行重新跑一遍状压 DP,减少不必要的计算。计算答案后更新 \(f_{b_i}\) 一直到 \(f_{b_{i+1}-1}\),涵盖每种情况。
然后根据预处理的信息,直接调用每组数据的 \(n\) 遍历每一个网格图,根据乘法原理计算答案即可,时间复杂度为 \(O(S^2\log_3n+Tn)\)。
全代码
#include<bits/stdc++.h>
#define int long long
#define I_love_Foccarus return
#define cin_fast ios::sync_with_stdio(false) , cin.tie(0) , cout.tie(0)
#define endl '\n'
//#define getchar getc
#define pii pair<int,int>
#define mk(a,b) make_pair(a,b)
#define fi first
#define se second
#define pd(a) push_back(a)
#define in(a) a = read_int()
using namespace std;
const int Size = 1 << 14;
const int N = 1e5 + 5;
const int inf = 0x3f3f3f3f , mod = 1e9 + 1;
const long long INF = 0x3f3f3f3f3f3f3f3f;
inline char getc(){
static char syn[Size] , *begin = syn , *end = syn;
if(begin == end) begin = syn , end = syn + fread(syn , 1 , Size , stdin);
I_love_Foccarus *begin ++;
}
inline int read_int() {
int x = 0;
char ch = getchar();
bool f = 0;
while('9' < ch || ch < '0') f |= ch == '-' , ch = getchar();
while('0' <= ch && ch <= '9') x = (x << 3) + (x << 1) + ch - '0' , ch = getchar();
I_love_Foccarus f ? -x : x;
}
int a[15] , f[N] , tot , cnt;
int dp[15][1 << 18];
struct node{
int x , v;
} b[N] ;
bool cmp(node x , node y) {
return x.v < y.v;
}
int solve(int x) {
a[x] ++; // 该行长度增加
if(x > cnt) cnt ++; // 拓展网格图行数
if(x == 1) { // 第一行单独处理
for(int s = 0 ; s < (1 << a[1]) ; s ++) dp[1][s] = !(s & (s >> 1));
x ++;
}
for(int i = x ; i <= cnt ; i ++) { // 从第 x 行开始重新转移
int m = (1 << a[i]) - 1 , lm = (1 << a[i - 1]) - 1; // 计算当前这行和上一行的状态总数
for(int s = 0 ; s <= m ; s ++) {
if(s & (s >> 1)) continue; // 筛去有相邻 1 的状态
dp[i][s] = 0;
for(int t = 0 ; t <= lm ; t ++) {
if((t & (t >> 1)) || (s & t)) continue; //筛去有相邻 1 的状态以及上下有相邻 1 的状态
dp[i][s] += dp[i - 1][t]; // 转移
dp[i][s] %= mod;
}
}
}
int h = 0 , m = (1 << a[cnt]);
for(int s = 0 ; s < m ; s ++) { // 答案为最后一行的所有合法状态所记录的和
if(!(s & (s >> 1))) {
h += dp[cnt][s];
h %= mod;
}
}
return h;
}
void into() {
for(int i = 1 , k = 1 ; i <= 1e5 ; i *= 3 , k ++) // 记录以 1 为左上角的网格图的每一个数所在的行
for(int j = i ; j <= 1e5 ; j <<= 1)
b[++ tot].x = k , b[tot].v = j;
sort(b + 1 , b + tot + 1 , cmp); // 按大小升序排序
int l = 1;
int now;
for(int i = 1 ; i <= tot ; i ++) {
now = solve(b[i].x); // 计算加点后的值
while(l < b[i + 1].v) f[l ++] = now; // 更新等效状态
}
while(l <= 1e5) f[l ++] = now; // 补充剩余状态
}
signed main() {
//cin_fast;
int t;
in(t);
into();
while(t --) {
int n , ans = 1;
in(n);
for(int i = 1 ; i <= n ; i ++) {
if(i % 2 != 0 && i % 3 != 0) {
ans *= f[n / i] , ans %= mod; // 乘法原理计算答案
}
}
printf("%lld \n" , ans);
}
I_love_Foccarus 0;
}

浙公网安备 33010602011771号