DP专题-算法盲点扫荡:状压 DP

1. 前言

本文是作者写的第 3 篇状压 DP 的博文,专门用来总结、复习状压 DP 这一动态规划的相关内容。

2. 题单

题单:

P2396 yyy loves Maths VII

看到这个 n ≤ 24 n \leq 24 n24 就想到应该是个状压 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=fij

其中保证 j = 2 k , k ∈ [ 0 , n − 1 ] j=2^k,k \in [0,n-1] j=2k,k[0,n1],且 f i ⊕ j f_{i \oplus j} fij 合法即 d i s i ⊕ j dis_{i \oplus j} disij 不是厄运数字。

如果在枚举 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 n30 的时候质数只有 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=(fi1,jsi,k+fi1,j,ksi)

其中 j & k = 0 j \& k=0 j&k=0,如果要统计 f i − 1 , j ∣ s i , k f_{i-1,j|s_i,k} fi1,jsi,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} fi1,j,ksi 要有 j & s i = 0 j \& s_i=0 j&si=0

发现这玩意只和 f i − 1 f_{i-1} fi1 有关,可以滚动数组滚掉第一维。

最后答案就是所有状态之和。

至此,30 pts 到手。


100 pts:

现在 n ≤ 500 n \leq 500 n500,质数变多了,我们要怎么办呢?

既然做到这道题,各位应该都知道一个定理:

  • 对于任意一个正整数 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,s2fs1,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 灵活多变,有的时候还需要结合别的算法才能够发现到底要状压哪个地方,很考验思维能力。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值