DP专题-盲点扫荡:状压 DP
1. 前言
本文是作者写的第 3 篇状压 DP 的博文,专门用来总结、复习状压 DP 这一动态规划的相关内容。
2. 题单
题单:
P2396 yyy loves Maths VII
看到这个 就想到应该是个状压 DP。
设 表示当状态为 时有多少种方案使得能够打出状态 表示的牌组,其中从右到左二进制下 的第 位表示第 张卡牌是否使用,使用为 1,不使用为 0。
又设 表示在状态 下 yyy 能够到达的距离。
有转移方程:
其中保证 ,且 合法即 不是厄运数字。
如果在枚举 的时候 全部枚举,会发现这样复杂度是 的,会 TLE。
因此我们需要使用一点技巧来快速求出 每一位二进制位为 1 的位数来转移。
这个可以采用 bitset 中的 .count() 函数,当然更简单的应用就是采用 lowbit 函数。
什么你没学过 lowbit 函数?建议右转树状数组。
Code:
/*
========= Plozia =========
Author:Plozia
Problem:P2396 yyy loves Maths VII
Date:2021/6/1
========= Plozia =========
*/
#include <bits/stdc++.h>
typedef long long LL;
const int MAX_State = (1 << 24) + 10, MAXN = 24 + 10, P = 1e9 + 7;
int n, m, b[3], f[MAX_State], dis[MAX_State];
int Read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return sum * fh;
}
int Max(int fir, int sec) { return (fir > sec) ? fir : sec; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }
int main()
{
n = Read();
for (int i = 1; i <= n; ++i) dis[1 << (i - 1)] = Read();
m = Read(); b[1] = b[2] = -0x7f7f7f7f;
for (int i = 1; i <= m; ++i) b[i] = Read();
f[0] = 1;
for (int i = 1; i <= (1 << n) - 1; ++i)
{
int l = i & (-i);
dis[i] = dis[i - l] + dis[l];
if (dis[i] == b[1] || dis[i] == b[2]) continue ;
for (int j = i & (-i), k = i; k; k -= j, j = k & (-k))
{ f[i] = (f[i] + f[i - j]) % P; }
}
printf("%lld\n", f[(1 << n) - 1]); return 0;
}
P2150 [NOI2015] 寿司晚宴
好题!
题目上说要求小 G 选的数字和小 W 选的数字中两两之间不能互质也就是 。
而要确认任意两数的 是否等于 1 我们可以只处理质数。
首先有一个关键点:
小 G 所选的数字与小 W 所选的数字不能有公共质因子。
知道了这个点,30 pts 的做法就好做了。
30 pts:
因为当 的时候质数只有 10 个,因此我们可以状压。
设 表示目前已经处理完前 张卡牌,小 G 选的质数集合为 ,小 W 选的质数集合为 的方案数,其中 。
- 如果小 G 选的质数集合为 ,那么其能够选择的数就是 中所有数互相组合的数。
设 表示 的质因数构成的质数集合状压后的结果。
那么有转移方程:
其中 ,如果要统计 要有 ,如果要统计 要有 。
发现这玩意只和 有关,可以滚动数组滚掉第一维。
最后答案就是所有状态之和。
至此,30 pts 到手。
100 pts:
现在 ,质数变多了,我们要怎么办呢?
既然做到这道题,各位应该都知道一个定理:
- 对于任意一个正整数 ,其仅有一个大于 的质因数。
这里, 约为 22。
因此我们可以考虑类似于根号分治的方法分个类,称所有大于 22 的质数为大质数,小于等于 22 的质数为小质数。
显然,小质数只有 8 个,因此我们可以考虑状压小质数。
对于大质数而言,我们可以按照大质数从大到小排个序,大质数相同的排在一起。
对于所有大质数相同的数:
我们需要记录 3 个值 。
定义同 30 pts 的定义, 要求是这些数不能是小 W 选的, 要求是这些数不能是小 G 选的。
转移方程?上面改一下就好了啊qwq
需要注意的是因为这里的数归属有了限定(必须去小 G 那里或者是必须去小 W 那里),因此转移的时候不能搞错数组。
在这一类大质数处理完之后,我们可以得到最后的 数组是:
为什么要减去 呢?因为两者都不选的情况被重复统计了两次。
Code:
/*
========= Plozia =========
Author:Plozia
Problem:P2150 [NOI2015] 寿司晚宴
Date:2021/6/4
========= Plozia =========
*/
#include <bits/stdc++.h>
typedef long long LL;
const int MAXN = 500 + 10;
int n, P, f[MAXN][MAXN], f1[MAXN][MAXN], f2[MAXN][MAXN], ans;
int Prime_Num[20] = {0, 2, 3, 5, 7, 11, 13, 17, 19};
struct node { int val, Prime, State; } a[MAXN];
int Read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return sum * fh;
}
int Max(int fir, int sec) { return (fir > sec) ? fir : sec; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }
bool cmp(const node &fir, const node &sec) { return fir.Prime > sec.Prime; }
int main()
{
n = Read(), P = Read();
for (int i = 2; i <= n; ++i)
{
a[i].val = i;
for (int j = 1; j <= 8; ++j)
{
if (a[i].val % Prime_Num[j] == 0)
{
while (a[i].val % Prime_Num[j] == 0) a[i].val /= Prime_Num[j];
a[i].State |= 1 << (j - 1);
}
}
if (a[i].val != 1) a[i].Prime = a[i].val;
else a[i].Prime = -1;
a[i].val = i;
}
std::sort(a + 2, a + n + 1, cmp);
f[0][0] = 1;
for (int i = 2; i <= n; ++i)
{
if (i == 1 || a[i].Prime != a[i - 1].Prime || a[i].Prime == -1)
{
memcpy(f1, f, sizeof(f1)); memcpy(f2, f, sizeof(f2));
}
for (int j = 255; j >= 0; --j)
for (int k = 255; k >= 0; --k)
{
if ((j & k) != 0) continue ;
if ((a[i].State & j) == 0)
{
f1[j][a[i].State | k] += f1[j][k];
if (f1[j][a[i].State | k] > P) f1[j][a[i].State | k] -= P;
}
if ((a[i].State & k) == 0)
{
f2[j | a[i].State][k] += f2[j][k];
if (f2[j | a[i].State][k] > P) f2[j | a[i].State][k] -= P;
}
}
if (i == n || a[i].Prime != a[i + 1].Prime || a[i].Prime == -1)
{
for (int j = 255; j >= 0; --j)
for (int k = 255; k >= 0; --k)
{
if ((j & k) != 0) continue ;
f[j][k] = ((f1[j][k] + f2[j][k]) % P + P - f[j][k]) % P;
}
}
}
for (int j = 255; j >= 0; --j)
for (int k = 255; k >= 0; --k)
{
if ((j & k) != 0) continue ;
ans += f[j][k]; if (ans > P) ans -= P;
}
printf("%d\n", ans); return 0;
}
3. 总结
状压 DP 灵活多变,有的时候还需要结合别的算法才能够发现到底要状压哪个地方,很考验思维能力。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具