:root { --bg-color: #ffffff; --text-color: #333333; --header-bg: #f5f5f5; } .night-mode { --bg-color: #1a1a1a; --text-color: #e0e0e0; --header-bg: #2d2d2d; } body { background: var(--bg-color); color: var(--text-color); } .header { background: var(--header-bg); }

题解: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\),然后算贡献即可。得转换方程:

\[dp_{i,s}=\sum_{l \in L}dp_{i-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;
}

posted @ 2025-08-13 17:45  雨落潇湘夜  阅读(8)  评论(0)    收藏  举报
我的页脚图片