【题解】P2150 [NOI2015] 寿司晚宴
题意
对于 \(2\) 到 \(n\) 共 \(n - 1\) 个自然数,考虑从中选取一些数并将其分成两部分,使得从两部分中各取任意一个数 \(x, y\) 都满足 \(x, y\) 互质。求选数并分数的方案总数,结果对给定常数 \(p\) 取模。
对于 \(30\%\) 的数据,\(2 \leq n \leq 30\)
对于所有数据,\(2 \leq n \leq 500, 0 < p \leq 10^9\)
思路
状压 dp
先考虑 \(30\%\) 的部分分。容易发现判断是否互质只与两部分中每个数含有的质因子有关,考虑到 \(30\) 以内的质数只有 \(10\) 个,故可以设 \(dp[S1][S2]\) 表示部分一中所有数的质因子并集为 \(S1\),\(S2\) 同理的方案总数。不妨预处理出 \([2, n]\) 中每个数的质因子的并集,记为 \(k\),容易得到状态转移方程(刷表法):
当 \(S1 \cap S2 = \empty\) 且 \(k \cap S2 = \empty\) 时,\(dp[S1 \mid k][S2] += dp[S1][S2]\)
当 \(S1 \cap S2 = \empty\) 且 \(k \cap S1 = \empty\) 时,\(dp[S1][S2 \mid k] += dp[S1][S2]\)
时间复杂度是 \(O(2^{10} n)\)
容易发现时间复杂度的瓶颈在于状压的质数个数,\(500\) 以内的质数共有 \(95\) 个,因此需要优化。
易得:对于自然数 \(n\),其大于 \(\sqrt{n}\) 的质因子至多只有 \(1\) 个。
因此可得对于 \(500\) 以内的自然数,其大于 \(\sqrt{500}\)(约为 \(22\))的质因子只有 \(1\) 个。
所以可以考虑把大于 \(22\) 的质因子单独提出来判断。对于 \([2, n]\) 内每一个可能被选中且大于 \(22\) 的质因子,讨论其对答案的贡献。因为大于 \(22\) 的质因子额外讨论,状态只需要记录 \(22\) 以内的质因子是否被选中,即至多有 \(2^8\) 种状态(\(22\) 以内的质数共 \(8\) 个)
考虑记录下 \([2, n]\) 中每一个数的质因子子集 \(k\) 和其大于 \(22\) 的唯一质因子 \(p\)。直接上手不好维护,考虑将所有数按照 \(p\) 分类。对于 \(p\) 相同的数,将其划为一段,分类讨论:
-
\(p\) 在部分一内
-
\(p\) 在部分二内
-
\(p\) 既不在部分一内,也不在部分二内
由于状态只压到 \(22\) 以内的质数,\(p\) 被分在不同部分的情况需要分类讨论。设 \(f1[S1][S2]\) 表示部分一中所有数 \(22\) 以内的质因子并集为 \(S1\),部分二的为 \(S2\) 且所有被 \(p\) 整除的数都不在部分二内的情况。\(f2[S1][S2]\) 同理。对于 \(p \mid x\),容易得到状态转移方程:
当 \(S1 \cap S2 = \empty\) 且 \(k \cap S2 = \empty\) 时,\(f1[S1 \mid k][S2] += f1[S1][S2]\)
当 \(S1 \cap S2 = \empty\) 且 \(k \cap S1 = \empty\) 时,\(f2[S1][S2 \mid k] += f1[S1][S2]\)
每一段开始时,将 \(dp\) 赋值到 \(f1, f2\) 作为初值。当每一段结束以后,考虑重新用 \(f1, f2\) 合并成 \(dp\)。容易发现 \(f1, f2\) 中重复的部分为两部分均没有 \(p\) 的情况,其并集为 部分一包含 \(p\) 并 部分二包含 \(p\) 并 均不包含 \(p\),所以得到:
\(dp[S1][S2] = f1[S1][S2] + f2[S1][S2] - dp[S1][S2]\)
最终的答案为所有合法状态的答案之和。
时间复杂度 \(O(2^{16} n)\)
代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 505;
const int maxs = (1 << 8);
struct item
{
int fac, sp;
bool operator < (const item& rhs) const { return (sp < rhs.sp); }
} num[maxn];
int n, p;
int prime[8] = {2, 3, 5, 7, 11, 13, 17, 19};
int dp[maxs][maxs], f1[maxs][maxs], f2[maxs][maxs];
void init()
{
for (int i = 2; i <= n; i++)
{
int cur = i;
num[i - 1].sp = -1;
for (int j = 0; j < 8; j++)
{
if (!(cur % prime[j]))
{
num[i - 1].fac |= (1 << j);
while (!(cur % prime[j])) cur /= prime[j];
}
}
if (cur > 1) num[i - 1].sp = cur;
}
sort(num + 1, num + n);
}
int main()
{
int ans = 0;
scanf("%d%d", &n, &p);
init();
dp[0][0] = 1;
for (int i = 1; i <= n - 1; i++)
{
if ((num[i].sp != num[i - 1].sp) || (num[i].sp == -1))
{
memcpy(f1, dp, sizeof(f1));
memcpy(f2, dp, sizeof(f2));
}
for (int j = maxs - 1; j >= 0; j--)
{
for (int k = maxs - 1; k >= 0; k--)
{
if (j & k) continue;
if (!(num[i].fac & k)) f1[j | num[i].fac][k] = (ll)(f1[j | num[i].fac][k] + f1[j][k]) % p;
if (!(num[i].fac & j)) f2[j][k | num[i].fac] = (ll)(f2[j][k | num[i].fac] + f2[j][k]) % p;
}
}
if ((num[i].sp != num[i + 1].sp) || (num[i].sp == -1))
{
for (int j = 0; j < maxs; j++)
for (int k = 0; k < maxs; k++)
if (!(j & k)) dp[j][k] = (ll)((f1[j][k] + f2[j][k]) % p - dp[j][k] % p + p) % p;
}
}
for (int i = 0; i < maxs; i++)
for (int j = 0; j < maxs; j++)
if (!(i & j)) ans = (ll)(ans + dp[i][j]) % p;
printf("%d\n", ans);
return 0;
}