题解:P9131 [USACO23FEB] Problem Setting P

前言

本文同步自洛谷专栏,题目传送门

这是一篇时间复杂度 \(O(2^mm+nm)\) 的题解,侧重优化部分,针对暴力部分较为简略。

参考资料:题解 by luoguhandongheng 大佬题解 by IamZZ 大佬

希望能够讲明白 sosdp 的方法。

本文中,二进制位从 \(0\) 开始。

对于一个集合 \(S\)\(|S|\) 表示其中 \(1\) 的个数。

\(S\) 的最高位表示其第一个 \(1\) 的位置。

分析

初步实现

不赘述如何思考到状压 dp。

用一个集合 \(S\),来刻画一道题目的特征,第 \(i\) 位验题人认为困难则第 \(i-1\) 位为 \(1\);否则为 \(0\)

\(cnt_S\) 表示特征为 \(S\) 的题目道数。下文中 \(S\) 和题目将不作区分。

那么组题的顺序就要求前面的题为后面的题的子集。

\(f_S\) 表示以 \(S\) 结尾的组题方案数,则其可以从其所有子集转移过来,转移系数设为 \(val_S\)。特别地,要考虑从一道题也没有(即当前为第一道)转移过来的情况。

\(val_S\) 的求法较为简单,枚举挑选了几个 \(S\)(至少一个),再排列起来。

具体地,\(val_S=\sum_{1\le x\le cnt_S}\binom{cnt_S}{x}x! =cnt_S!\sum_{1\le x\le cnt_S}\frac{1}{(cnt_S-x)!}\)

至此,实现了复杂度 \(O(3^m)\) 做法。

优化

对于这个 \(O(3^m)\) 的 dp,我们发现复杂度瓶颈在枚举子集。

考虑如何优秀地统计子集贡献,暴力枚举 \(O(2^{|S|})\);简单地按最高位是 \(0,1\) 划分,虽然是 \(O(1)\),但无法维护。

考虑折中的方案,比如划分为 \(O(m)\) 个?

事实上,这样的划分方式是存在的,可能有很多种,这里提供一种。其实是只知道这一种。

定义对集合 \(S\)修改操作为:将 \(S\) 某一二进制位由 \(1\) 改为 \(0\)

那么 \(S\) 的所有子集可以划分为:相比 \(S\) 仅修改(有些位可不修改)前 \(i-1\) 位(指 \([0,i)\)修改了第 \(i-1\) 位。特别地\(i=0\) 时表示其本身。

事实上,修改了第 \(i-1\)其实就是修改的最高位为 \(i-1\),利用最高位的唯一性,可以证明这种划分方式能做到不重不漏。

这种划分方式下,各部分大小其实并不相等,依次为 \(1,1,2,4,8,\dots,2^{|S|-1}\)

由于两个限制有的关系,暴力维护存在困难,在此给出一种维护方式:

\(g_{S,i}\) 表示对集合 \(S\) 仅修改前 \(i-1\) 的子集的贡献和,处理第一个限制。

查询时,调用 \(g_{S\oplus 2^{i-1},i}\) 表示第二个限制。(注意 \(S\)\(i-1\) 位上要为 \(1\)

\(g\) 数组的维护如下:

  • 根据边界情况的定义,首先有 \(g_{S,0}=f_{S}\)

  • 如果 \(S\)\(i-1\) 位为 \(0\),则 \(g_{S,i}\leftarrow g_{S,i-1}\),表示没有统计到新的子集。

  • 如果 \(S\)\(i-1\) 位为 \(1\),则 \(g_{S,i}\leftarrow g_{S,i-1}+g_{S\oplus 2^{i-1},i-1}\),后者表示加入修改第 \(i-1\) 位得到的子集。

对于第三种情况,\(g_{S,i}\leftarrow g_{S,i-1}+g_{S\oplus 2^{i-1},i}\) 也是结果正确的写法,

因为根据第二种情况,\(g_{S\oplus 2^{i-1},i-1}=g_{S\oplus 2^{i-1},i}\) 必然成立,

但是从定义的角度出发,第二维写 \(i-1\) 才是正确的。

如果对于上述内容还有疑惑,可以结合如下代码思考。

$\red{\text{子集个数}}$
//一个简单的例子:求子集个数
//虽然可以直接表示,但是也可以用来理解子集划分的方式
#include<bits/stdc++.h>
using namespace std;
#define M 1050000

int n,up,f[M],g[M][21];

int main(){
    cin>>n,up=(1<<n)-1;
    for(int S=0;S<=up;S++){
        for(int i=1;i<=n;i++){
            if((1<<i-1)&S) f[S]+=g[S^(1<<i-1)][i];
        }
        f[S]++,g[S][0]=1;
        for(int i=1;i<=n;i++){
            g[S][i]=g[S][i-1];
            if(S&(1<<i-1)) g[S][i]+=g[S^(1<<i-1)][i-1];
        }
    } 
    for(int S=0;S<=up;S++) cout<<f[S]<<' ';
    return 0;
}

实现

时间复杂度 \(O(2^mm+nm)\),空间复杂度 \(O(2^mm)\)

$\red{\text{code}}$
#include<bits/stdc++.h>
using namespace std;
#define N 100005
#define M 1050000
#define mod 1000000007

int cnt[M],A[N],f[M],g[M][21];
int fac[N],inv[N],n,m,up,ans;//f[S] 表示以该情况中的结果结尾的答案(不能不放)
char s[N];//g[S][i] 表示仅能修改(指 1->0)前 i-1 位的情况下的子集 dp 和 

inline void inc(int &a,int b){a+=b,(a>=mod)&&(a-=mod);}

inline int ksm(int a,int b){
    int ret=1;
    for(;b;b>>=1,a=1ull*a*a%mod){
        if(b&1) ret=1ull*ret*a%mod;
    }
    return ret;
}

void input(){
    scanf("%d%d",&n,&m),up=(1<<m)-1,fac[0]=1;
    for(int i=0;i<m;i++){
        scanf(" %s",s+1);
        for(int j=1;j<=n;j++){
            if(s[j]=='H') A[j]|=1<<i;
        }
    }//一个 cnt 加入的条件是仅有所有子集全部加入了
    for(int i=1;i<=n;i++) cnt[A[i]]++;
}

void init(){
    for(int i=1;i<=n;i++) fac[i]=1ull*fac[i-1]*i%mod;
    inv[n]=ksm(fac[n],mod-2);
    for(int i=n;i>=1;i--) inv[i-1]=1ull*inv[i]*i%mod;
    for(int i=1;i<=n;i++) inc(inv[i],inv[i-1]);//前缀和
    for(int S=0;S<=up;S++) cnt[S]=1ull*fac[cnt[S]]*inv[cnt[S]]%mod;
    for(int S=0;S<=up;S++) inc(cnt[S],mod-1);//删去不放的情况
}

int main(){ 
    input(),init();
    for(int S=0;S<=up;S++){//填表法
        for(int i=1;i<=m;i++){//这里表示必须修改第 i-1 位
            if((1<<i-1)&S) inc(f[S],g[S^(1<<i-1)][i]);
        }//+1 相当于上一位差异为第 -1 位,表示从空集开始
        f[S]=1ull*(f[S]+1)*cnt[S]%mod;
        inc(ans,f[S]),g[S][0]=f[S];
        for(int i=1;i<=m;i++){
            g[S][i]=g[S][i-1];//转移表示修改了第 i-1 位
            if(S&(1<<i-1)) inc(g[S][i],g[S^(1<<i-1)][i-1]);
        }
    } 
    printf("%d",ans);
    return 0;
}

posted @ 2026-06-05 11:03  Wxb2010  阅读(8)  评论(0)    收藏  举报