洛谷P2150 寿司晚宴

链接

  https://www.luogu.org/problem/show?pid=2150

题解

  考虑暴力,当 n30 时,素数有2 3 5 7 11 13 17 19 23 29,一共10个,状压dp可行,f[i][j]表示每个人选数的状态,然后暴力转移就好了,复杂度 O(22N) 。然后我就想不出来了……
  有一个常用到的性质就是,对于任何一个数字 x ,其大于x的约数只能有一个或者没有,利用这个性质,可以做这道题。 500=22 ,而小于等于22的最大的素数是19,从1到19一共只有8个素数。
  可以只压这8个素数, f[i][j] 表示第一个人的选数状态为i,第二个人为j的方案数。
  将 [2,500] 的所有数分解质因数,大于19的素数最多有一个,记下来,记作gp[i](giant prime),小素数们压进二进制数,记作tp[i](tiny primes)。
  接下来是转移。
  对于gp相等的数,我们划分到一组里去单独dp,因为这一组中只能让一个人选数,一个人选了另一个人就不能再这组里选了。
  对于一个组,其giant prime是相同的,令 g[i][j] 表示已经考虑了组中的所有数和前面做完的组的所有数,强制让第一个人选择这个组中的数(一个数不选的情况也纳入考虑),第二个人不能选,这样的方案数。
  初始状态 g[i][j]=f[i][j]
  那么我们每次加入组中的一个数 x ,令k=tp[x],转移就是 g[i bitor k][j]+=g[i][j] (当 i bitand k==0 ),注意循环顺序要倒序。
  做完一组之后,要将 g f数组合并, f[i][j]=g[i][j]+g[j][i]f[i][j] ,减去 f[i][j] 是因为两个人都啥也不选的情况重复了。
  这样有BUG,没有大素数的那一组不能这样转移,这样转移方案数会变少,因为这一组中只要两个人不选相同的数字就行,和前面不一样。
  这时直接 f[i bitor k]+=f[i][j] f[i][j bitor k]+=f[i][j]
  答案就是所有状态加起来。

代码

//状压DP 
#include <cstdio>
#include <algorithm>
#define maxn 1000
#define ll long long
using namespace std;
ll N, P, f[maxn][maxn], g[maxn][maxn], tp[maxn], hp[maxn], mark[maxn], prime[maxn],
    num[maxn], h[maxn][maxn];
bool cmp(ll a, ll b){return hp[a]<hp[b];}
void init()
{
    ll i, j;
    scanf("%lld%lld",&N,&P);
    for(i=2;i<=N;i++)
    {
        if(!mark[i])prime[++prime[0]]=i;
        for(j=1;j<=prime[0] and i*prime[j]<=N;j++)
        {
            mark[i*prime[j]]=1;
            if(i%prime[j]==0)break;
        }
    }
    for(i=2;i<=N;i++)
    {
        for(j=1;prime[j]*prime[j]<=N;j++)if(i%prime[j]==0)tp[i]|=(1<<j-1);
        for(;prime[j]<=i and j<=prime[0];j++)if(i%prime[j]==0)hp[i]=prime[j];
    }
    for(i=2;i<=N;i++)num[i]=i;
    sort(num+2,num+N+1,cmp);
}
void dp()
{
    ll l, r, p, i, j, k, ans=0;
    f[0][0]=1;
    for(l=2;l<=N;l=r+1)
    {
        for(r=l;hp[num[r]]==hp[num[l]] and r<=N;r++);r--;
        if(hp[num[l]]!=0)
        {
            for(i=0;i<256;i++)for(j=0;j<256;j++)g[i][j]=f[i][j];
            for(p=l;p<=r;p++)
            {
                k=tp[num[p]];
                for(i=255;i>=0;i--)
                    for(j=255;j>=0;j--)
                    {
                        if((i&j)or(j&k))continue;
                        g[i|k][j]+=g[i][j];
                    }
            }
            for(i=0;i<256;i++)for(j=0;j<256;j++)f[i][j]=(g[i][j]+g[j][i]-f[i][j])%P;
        }
        else
        {
            for(p=l;p<=r;p++)
            {
                k=tp[num[p]];
                for(i=255;i>=0;i--)
                    for(j=255;j>=0;j--)
                    {
                        if(i&j)continue;
                        f[i][j]%=P;
                        if((j&k)==0)f[i|k][j]+=f[i][j];
                        if((i&k)==0)f[i][j|k]+=f[i][j];
                    }
            }
        }
    }
    for(i=0;i<256;i++)for(j=0;j<256;j++)ans=(ans+f[i][j])%P;
    printf("%lld",((ans%P)+P)%P);
}
int main()
{
    init();
    dp();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值