1 条题解

  • 0
    @ 2025-8-24 22:45:39

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar Little09
    「按照我们原来的期待 去证明我们的未来」

    搬运于2025-08-24 22:45:39,当前版本为作者最后更新于2023-03-08 11:04:18,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    首先我们把每道题 H 看做 11E 看做 00 的话,那么条件变成了选若干道题并排序,使得相邻两项满足前一项是后一项的子集。

    把状态相同的题目放在一起考虑,假设有 kk 道题,那么选择至少一题的方案数就是:

    i=1k(ki)i!\sum_{i=1}^k\binom kii!

    可以直接预处理。那么现在我们需要按顺序进行 DP 转移,有以下几种方法:

    直接暴力

    定义 dpSdp_{S} 表示最后一项是 SS 的选法的方案数,那么每次的转移我们需要枚举 SS 的所有子集进行转移,按 SS 大小依次转移。直接转移的总复杂度为 O(3m)O(3^m)

    分成前后两部分

    这套路某年 CSP 初赛考过。

    刚才的瓶颈在于枚举子集,那能不能改进一下。考虑 si,js_{i,j} 表示所有 dpxdp_{x} 的和,其中 xx 需要满足前 1010 位是 ii,后 1010 位是 kk,其中 kkjj 的子集。

    这样我们每次计算 dpdp 的时候只要枚举上一个的前 1010 位,转移 ss 的时候枚举后 1010 位,复杂度大致是 O(n2m2)O(n2^{\frac m2}),可以精细分析到更准确的下界 O(6m2)O(6^{\frac m2}),空间可以做到 O(2m)O(2^m)

    for (int i=0;i<=mx;i++)
    {
    	if (b[i]==0) continue;
    	int A=i>>10,B=i&1023;
    	ll res=1;
    	for (int j=0;j<1024;j++) if ((j&A)==j) (res+=dp[j][B])%=mod;
    	f[i]=val[i]*res%mod;
    	for (int j=0;j<1024;j++) if ((j&B)==B) (dp[A][j]+=f[i])%=mod;
    	(ans+=f[i])%=mod;
    }
    

    S|S| 顺序

    考虑按 S|S| 顺序进行 DP 转移,也就是按照 S|S| 分成 mm 层,每一层一定是从之前的层里转移过来。那么我们只需要做完每一层后做一次 FMT 累计入答案,每次计算 dpdp 的时候可以直接 O(1)O(1) 计算,这样时间可以做到 O(m22m)O(m^22^m),空间可以做到 O(2m)O(2^m)

    for (int i=0;i<=m;i++)
    {
    	for (int j=0;j<(1<<m);j++) dp[j]=0;
    	for (int j=0;j<(1<<m);j++)
    	{
    		if (__builtin_popcount(j)!=i) continue;
    		dp[j]=(1+s[j])*val[j]%mod;
    		(ans+=dp[j])%=mod;
    	}
    	FMT();
    }
    

    FMT 同时转移(官方做法)

    考虑我们执行 FMT 的过程,比方说在做完前 kk 位的时候我们得到的数组是什么:sis_i 是需要满足前 kk 位是 ii 的前 kk 位的子集,kk 位以后恰好就是 iikk 位以后,这样的位置的和。

    我们直接定义 sdpb,isdp_{b,i} 表示所有 dpxdp_{x} 的和,其中 xx 满足前 kk 位是 bb 的前 kk 位的子集,kk 位以后恰好就是 bbkk 位以后。如果我们计算出了 dpbdp_b,我们可以在 O(m)O(m) 的时间里计算出 sdpbsdp_b 的每个位,递推即可。而我们如果得到了所有 sdpxsdp_x 满足 x<bx<b,我们也可以 O(m)O(m) 计算出 dpbdp_b,枚举不同的第一个位置转移即可。

    这样可以做到时间 O(m2m)O(m2^m),空间 O(m2m)O(m2^m)

    for (int i=0;i<(1<<m);i++)
    {
    	dp[i]=1;
    	for (int j=0;j<m;j++) if (i&(1<<j)) (dp[i]+=s[j][i^(1<<j)])%=mod;
    	dp[i]=dp[i]*val[i]%mod;
    	s[0][i]=dp[i];
    	(ans+=dp[i])%=mod;
    	for (int j=1;j<m;j++) 
    	{
    		s[j][i]=s[j-1][i];
    		if (i&(1<<(j-1))) (s[j][i]+=s[j-1][i^(1<<(j-1))])%=mod;
    	}
    }
    

    分治

    考虑对最高位 0/10/1 分治,也就是在 trie 的结构上做类似 CDQ 分治的东西。那么就是先做左儿子,接下来考虑左儿子对右儿子的贡献,再做右儿子。

    考虑记以 SS 结束的答案为 dpSdp_S,我们还需要在 trie 上一边遍历一边维护 dpdp 的 FMT 数组 gg,假设当前做到的位是 dd,目前已有的数是 SS,我们需要 gig_idpjdp_j 的和,其中 jj 满足满足最高的 dd 位和 ii 相同,后面的位是 ii 子集。那么在每个节点做完左右儿子后,我们把当前子树内的 gg 从左儿子向右儿子累加即可。

    时间复杂度为 O(m2m)O(m2^m),空间复杂度为 O(2m)O(2^m)

    void solve(int i,int S) 
    {
    	if (i==-1) 
    	{
    		Add(g[S],dp[S]);
    		return;
    	}
    	solve(i-1,S);
    	for (int T=0;T<(1<<i);T++) Add(dp[S|T|(1<<i)],g[S|T]*val[S|T|(1<<i)]%mod);
    	solve(i-1,S|(1<<i));
    	for (int T=0;T<(1<<i);T++) Add(g[S|T|(1<<i)],g[S|T]);
    }
    
    • 1

    信息

    ID
    8462
    时间
    2000ms
    内存
    512MiB
    难度
    6
    标签
    递交数
    0
    已通过
    0
    上传者