2 条题解

  • 0
    @ 2026-5-5 23:59:34

    首先我们把每道题 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]);
    }
    
    • 0
      @ 2025-10-8 17:14:13

      sol_prob2_platinum_feb23.html
      sol_prob2_platinum_feb23.mhtml

      By Hdh 2024.11.23

      SOSDP P9131 [USACO23FEB] Problem Setting P

      人生第一道 Sum over Subsets DP(SOSDP)。
      SOSDP 顾名思义,解决的是求 f(T)=STg(S)f(T)=\sum\limits_{S \subset T}g(S) 的问题。 首先需要学会最简单的子集枚举。

      • 1

      信息

      ID
      7678
      时间
      2000ms
      内存
      512MiB
      难度
      8
      标签
      递交数
      16
      已通过
      5
      上传者