DP专题-算法盲点扫荡:状压 DP
1. 前言
本文是作者写的第 3 篇状压 DP 的博文,专门用来总结、复习状压 DP 这一动态规划的相关内容。
- DP专题-学习笔记:状压 DP(2021/3/2)
- DP专题-专项训练:状压 DP(2021/3/6)
- DP专题-算法盲点扫荡:状压 DP(2021/6/4)
2. 题单
题单:
P2396 yyy loves Maths VII
看到这个 n ≤ 24 n \leq 24 n≤24 就想到应该是个状压 DP。
设 f i f_i fi 表示当状态为 i i i 时有多少种方案使得能够打出状态 i i i 表示的牌组,其中从右到左二进制下 i i i 的第 k k k 位表示第 k k k 张卡牌是否使用,使用为 1,不使用为 0。
又设 d i s i dis_i disi 表示在状态 i i i 下 yyy 能够到达的距离。
有转移方程:
f i = ∑ f i ⊕ j f_i=\sum f_{i \oplus j} fi=∑fi⊕j
其中保证 j = 2 k , k ∈ [ 0 , n − 1 ] j=2^k,k \in [0,n-1] j=2k,k∈[0,n−1],且 f i ⊕ j f_{i \oplus j} fi⊕j 合法即 d i s i ⊕ j dis_{i \oplus j} disi⊕j 不是厄运数字。
如果在枚举 j j j 的时候 [ 1 , n ] [1,n] [1,n] 全部枚举,会发现这样复杂度是 O ( 2 n n ) O(2^nn) O(2nn) 的,会 TLE。
因此我们需要使用一点技巧来快速求出 i i i 每一位二进制位为 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 选的数字中两两之间不能互质也就是 gcd ( x , y ) = 1 \gcd(x,y)=1 gcd(x,y)=1。
而要确认任意两数的 gcd \gcd gcd 是否等于 1 我们可以只处理质数。
首先有一个关键点:
小 G 所选的数字与小 W 所选的数字不能有公共质因子。
知道了这个点,30 pts 的做法就好做了。
30 pts:
因为当 n ≤ 30 n \leq 30 n≤30 的时候质数只有 10 个,因此我们可以状压。
设 f i , j , k f_{i,j,k} fi,j,k 表示目前已经处理完前 i i i 张卡牌,小 G 选的质数集合为 j j j,小 W 选的质数集合为 k k k 的方案数,其中 j & k = 0 j \& k = 0 j&k=0。
- 如果小 G 选的质数集合为 j j j,那么其能够选择的数就是 j j j 中所有数互相组合的数。
设 s i s_i si 表示 i i i 的质因数构成的质数集合状压后的结果。
那么有转移方程:
f i , j , k = ∑ ( f i − 1 , j ∣ s i , k + f i − 1 , j , k ∣ s i ) f_{i,j,k}=\sum (f_{i-1,j|s_i,k}+f_{i-1,j,k|s_i}) fi,j,k=∑(fi−1,j∣si,k+fi−1,j,k∣si)
其中 j & k = 0 j \& k=0 j&k=0,如果要统计 f i − 1 , j ∣ s i , k f_{i-1,j|s_i,k} fi−1,j∣si,k 要有 s i & k = 0 s_i \& k=0 si&k=0,如果要统计 f i − 1 , j , k ∣ s i f_{i-1,j,k|s_i} fi−1,j,k∣si 要有 j & s i = 0 j \& s_i=0 j&si=0。
发现这玩意只和 f i − 1 f_{i-1} fi−1 有关,可以滚动数组滚掉第一维。
最后答案就是所有状态之和。
至此,30 pts 到手。
100 pts:
现在 n ≤ 500 n \leq 500 n≤500,质数变多了,我们要怎么办呢?
既然做到这道题,各位应该都知道一个定理:
- 对于任意一个正整数 n n n,其仅有一个大于 n \sqrt{n} n 的质因数。
这里, n \sqrt{n} n 约为 22。
因此我们可以考虑类似于根号分治的方法分个类,称所有大于 22 的质数为大质数,小于等于 22 的质数为小质数。
显然,小质数只有 8 个,因此我们可以考虑状压小质数。
对于大质数而言,我们可以按照大质数从大到小排个序,大质数相同的排在一起。
对于所有大质数相同的数:
我们需要记录 3 个值 f , f 1 , f 2 f,f1,f2 f,f1,f2。
f f f 定义同 30 pts 的定义, f 1 f1 f1 要求是这些数不能是小 W 选的, f 2 f2 f2 要求是这些数不能是小 G 选的。
转移方程?上面改一下就好了啊qwq
需要注意的是因为这里的数归属有了限定(必须去小 G 那里或者是必须去小 W 那里),因此转移的时候不能搞错数组。
在这一类大质数处理完之后,我们可以得到最后的 f f f 数组是:
f s 1 , s 2 = f 1 s 1 , s 2 + f 2 s 1 , s 2 − f s 1 , s 2 f_{s1,s2}=f1_{s1,s2}+f2_{s1,s2}-f_{s1,s2} fs1,s2=f1s1,s2+f2s1,s2−fs1,s2
为什么要减去 f s 1 , s 2 f_{s1,s2} fs1,s2 呢?因为两者都不选的情况被重复统计了两次。
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 灵活多变,有的时候还需要结合别的算法才能够发现到底要状压哪个地方,很考验思维能力。