AIGC标识 数论题解

计算系数

二项式定理:

\[(ax+by)^k=\sum_{p=0}^kC_k^p(ax)^p(by)^{k-p}=\sum_{p=0}^k(C_k^pa^pb^{k-p})x^py^{k-p} \]

int ans=((quick(a,n)%mod*quick(b,m)%mod)%mod*(C(k,n)%mod))%mod;
cout<<ans;

combination

直接算\(C_n^m\)

while(T--)
{
    int n,m;
    cin>>n>>m;
    cout<<C(n,m)<<"\n";
}

abc433F - 1122 Subsequence 2

范德蒙卷积

设:

  • \(n\) 为分界点前面包括分界点的某元素出现次数,
  • \(m\) 为分界点后面对应元素出现次数。

分界点确定后,方案数就是对于每个 \(k\) 前面这个元素中选择 \(k−1\) 个与后面对应元素中选择 \(k\) 个的方案数之积的和。

式子:

\[\sum_k C_{n-1}^{k-1}C_m^k=C_{n+m-1}^n \]

前缀和求,预处理组合数。

int n=s.size();
s=" "+s;
for(int i=1;i<=n;i++) for(int j=0;j<10;j++) qzh[i][j]=qzh[i-1][j]+(j==s[i]-'0');
for(int i=n;i>=1;i--) for(int j=0;j<10;j++) hzh[i][j]=hzh[i+1][j]+(j==s[i]-'0');
int ans=0;
for(int i=1;i<=n;i++)
{
    if(s[i]=='9') continue;
    int x=qzh[i][s[i]-'0'];
    int y=hzh[i][s[i]-'0'+1];
    if(x==0||y==0) continue;
    ans+=C(x+y-1,x);
    ans%=mod;
}
cout<<ans;

abc154F - Many Many Paths

推式子

\[\begin{split} \sum_{i=r1}^{r2}\sum_{l=c1}^{c2}C_{l+i}^l &= \sum_{i=r1}^{c1}(\sum_{l=0}^{c2}C_{l+i}^l-\sum_{l=0}^{c1-1}C_{l+i}^l)\\ &= \sum_{i=r1}^{c1}(C_{c2+i+1}^{c2}-C_{c1+i}^{c1-1}) \end{split} \]

预处理即可

for(int i=r1;i<=r2;i++) f[i]=(C(c2+i+1,c2)-C(c1+i,c1-1)+mod)%mod;
int ans1=0;
for(int i=r1;i<=r2;i++) ans1=(ans1+f[i])%mod;
cout<<ans1%mod;

abc240G - Teleporting Takahashi

把平面旋转 \(45°\)

那么可以先枚举第一维,就转化成二维平面中是否能从 \((0,0)\) 走到 \((y,z)\),旋转一下,就变化成了 \((y−z,y+z)\)

走了 \(a\) 步 \(+1\),\(b\) 步 \(−1\),总共走了 \(k\) 步,目的地是 \(x\),可以得到一个方程组:

\[\begin{cases} a-b=x\\ a+b=k \end{cases} \]

\(a=\frac{x+k}{2},b=\frac{x-k}{2}\),而最终方案就是 \(C_k^a\)或\(C_k^b\)。

拓展到这题上面,最终方案就是到 \(y−z\) 的方案数乘上到 \(y+z\) 的方案数,也就是:

\[C_k^{\frac{y+k-z}{2}}*C_k^{\frac{y+k+z}{2}} \]

对于第一维的判断;设走了\(a\) 步 \(+1\),\(b\) 步 \(−1\),那么最终方案数就是\(C_n^a*C_{n-a}^b(a+b≤n)\)。

inline int solve(int k,int kx,int ky)
{
    int a=abs(kx-ky),b=abs(kx+ky);
    if(k+a&1||k+b&1||k<a||k<b) return 0;
    return C(k,k+a>>1)*C(k,k+b>>1)%mod;
}
x=abs(x),y=abs(y),z=abs(z);
JN();
for(int i=0,j=x;i+j<=n;i++,j++) ans=(ans+C(n,i)*C(n-i,j)%mod*solve(n-i-j,y,z)%mod)%mod;
cout<<ans;

[CodePlus 2018 4 月赛]组合数问题 2

求\(K\)个不同组合数的最大和。

容易发现\(C_n^m\)都比要\(C_{n-1}^m\)都大。

于是一开始可以将所有的\(C_n^i\)都丢到一个优先队列中,每次取出最大值\(C_x^y\),然后将\(C_x^{y-1}再丢\)到优先队列中。重复\(k\)次即可。

发现组合数太大,优先队列无法比较。

于是考虑对组合数取对数,然后比较,就可以了。

\(log(C_x^y)=log({x!\over y!(x-y)!})=log(x!)-log(y!)-log((x-y)!)\)

我们用\(log(x!)=log((x-1)!*x)=log((x-1)!)+log(x)\)预处理阶乘的log即可

priority_queue<pair<double,pair<int,int>>> q;
for(int i=0;i<=n;i++) q.push({lg[n]-lg[n-i]-lg[i],{i,n}});
for(int i=1;i<=k;i++)
{
    int x=q.top().se.fi;
    int y=q.top().se.se;
    q.pop();
    ans=(ans+((jie[y]*ni[y-x])%mod*ni[x])%mod+mod)%mod;
    y--;
    if(y>=x) q.push({lg[y]-lg[y-x]-lg[x],{x,y}});
}
cout<<ans;

[SHOI2015]超能粒子炮・改

方式1

设\(f(n,k)=\sum_{i=0}^kC_n^i\),使用卢卡斯定理

\[\begin{split} f(n,k) &= \sum_{i=0}^kC_n^i\\ &= \sum_{i=0}^kC_{n/p}^{i/p}*C_{n\%p}^{i\%p}\\ &= \sum_{x=0}^{p-1}C_{n\%p}^x*\sum_{i=0}^k[i\%p=x]C_{n/p}^{i/p}\\ &= \sum_{x=0}^{p-1}C_{n\%p}^x*\sum_{i=0}^{(k-x)/p}C_{n/p}^i\\ &= \sum_{x=0}^{p-1}*f(n/p,(k-x)/p) \end{split} \]

前面那个东西可以提前预处理好前缀和

方式2

设\(f(n,k)=\sum_{i=0}^kC_n^i\),使用卢卡斯定理修改之

\(=\sum_{i=0}^kC_{n\%p}^{i\%p}C_{n/p}^{i/p}\)

对着\(i/p\)相同的若干段分别计算

\(C_{n/p}^0*\sum_{i=0}^{p-1}C_{n\%p}^{i\%p}\)

\(+C_{n/p}^1*\sum_{i=p}^{2p-1}C_{n\%p}^{i\%p}\)

\(+C_{n/p}^2*\sum_{i=2p}^{3p-1}C_{n\%p}^{i\%p}\)

...

\(=\sum_{j=0}^{k/p-1}C_{n/p}^j*\sum_{i=0}^{p-1}C_{n\%p}^i\)

右边本来是在左边的\(\sum\)内部的,但是可以发现循环的是\(i\),和外面的\(j\)无关,直接拿到外面即可

因此\(i/p\)的前面一大堆可以替换为\(f(n/p,k/p-1)*f(n\%p,p-1)\)

对于最后一段不足\(p\)的一段:

\(\sum_{i=k/p*p}^kC_{n\%p}^{i\%p}C_{n/p}^{i/p}\)

=\(C_{n/p}^{k/p}\sum_{i=k/p*p}^kC_{n\%p}^{i\%p}\)

\(=C_{n/p}^{k/p}\sum_{i=0}^{k\%p}C_{n\%p}^i\)

\(=C_{n/p}^{k/p}f(n\%p,k\%p)\)

结论是如果\(k\%p\)非\(0\),\(f(n,k)=f(n/p,k/p-1)*f(n\%p,p-1)+C_{n/p}^{k/p}f(n\%p,k\%p)\)

如果\(\%p=0\)不需要后面那块

如果\(f(n,k)\)落在了\(p\)内部,可以提前预处理之,快速拿到答案。

\(C_{n/p}^{k/p}\)这块可以卢卡斯定理


const int N=3010;
const int mod=2333;
int c[N][N];
inline int solve(int n,int k)
{
    if(n<mod) return c[n][min(n,k)];
    if(!k) return 1;
    int x=k%mod,y=n%mod;
    int ret=c[y][min(y,x)]*solve(n/mod,k/mod);
    if(k-x!=0) ret=(ret+(c[y][y]-c[y][min(y,x)]+mod)*solve(n/mod,(k-x-1)/mod))%mod;
    return ret;
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    for(int i=0;i<N;++i) c[i][0]=1;
    for(int i=1;i<N;++i) for(int j=1;j<=i;++j) c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
    for(int i=0;i<N;++i) for(int j=1;j<=i;++j) c[i][j]=(c[i][j]+c[i][j-1])%mod;
    int T,n,k;
    cin>>T;
    while(T--)
    {
        cin>>n>>k;
        cout<<solve(n,k)<<"\n";
    }
    return 0;
}

[USACO 2025 US Open, Gold] Problem 1. Moo Decomposition

只看一段循环节就行,每一段循环节之间不会互相影响,如果不同段的 M、O 匹配,说明有一段必然有剩余的 O,总体的 M、O 数量一定不匹配,必然无解,与题目描述矛盾。

设遇到字符 O 的数量为 O,局部的方案数为 ans。

从后往前枚举。

  • 如果遇到 O,令 O 增加一;
  • 如果遇到 M,从 O 个中选 K 个匹配,令 O 减少 K,令 ans 增加从 O 个中选 K 个的方案数,即\(C_O^K\)

答案为\(ans^L\)

int k,n,l;
string s;
cin>>k>>n>>l>>s;
s=" "+s;
int cnt=0,O=0,ans=1;
for(int i=1;i<=n;i++) if(s[i]=='0') cnt++;
for(int i=n;i>=1;i--)
{
    if(s[i]=='O') O++;
    else
    {
        ans=ans*C(O,k)%mod;
        O-=k;
    }
}
cout<<quick(ans,l);

F - Maximum Diameter

  1. 合法序列判定条件

    长度为 \(N\) 的正整数序列 \(X\) 能构成好树的充要条件是序列所有元素之和等于 \(2(N-1)\)。因为 \(N\) 个节点的树共有 \(N-1\) 条边,每条边贡献 \(2\) 个度数,总度数和为 \(2(N-1)\)。

  2. 最大直径计算逻辑

    对于任意合法序列,所有好树的最大直径等于序列中度数\(≥2\) 的节点数量\(+1\)。构造方式为将所有度数\(≥2\) 的节点连成一条链,两端各接一个叶子节点,剩余叶子节点挂载到链上的节点即可,此时树的直径为链的长度\(+1\),也就是度数\(≥2\) 的节点数\(+1\)。

  3. 求和公式推导

    我们需要计算所有合法序列的\(f(X)\)之和,\(f(X)=\)度数\(≥2\)的节点数\(+1\),因此总和可以拆分为:
    总和 \(=\) 所有合法序列的总数 \(+\) 所有合法序列中度数\(≥2\)的节点总数

    • 合法序列总数:通过插板法可得合法序列的数量为组合数\(C(2N-3,N-1)\)。
    • 所有合法序列中度数\(≥2\) 的节点总数:每个位置上 \(X_i≥2\) 的合法序列数量为 \(C(2N-4, N-2)\),总共有 \(N\) 个位置,因此这部分总和为 \(N*C(2N-4, N-2)\)。
    • 将两部分合并化简后得到最终公式:总和 \(= (N-3)*C(2N-4, N-2)+2*C(2N-3, N-1)\)
int T;
cin>>T;
while(T--)
{
    int n;
    cin>>n;
    cout<<((n-3)*C(2*n-4,n-2)%mod+2*C(2*n-3,n-1)%mod+mod)%mod<<"\n";
}

[清华集训2016] 组合数问题

k是100以内的质数,nm很大,考虑卢卡斯定理

众所周知,卢卡斯定理告诉我们,\(C_a^b≡C_{a\%k}^{b\%k}C_{a/k}^{b/k}\)

对着\(C_{a/k}^{b/k}\)接着用卢卡斯定理,此时是不是变成了\(C_{a\%k}^{b\%k}C_{a/k\%k}^{b/k\%k}C_{a/k/k}^{b/k/k}\)

也就是说,他是\(a,b\)在\(k\)进制下的每一位一一对应的组合数乘起来:设\(a\)的\(k\)进制下是\(a_0,a_1,a_2...\),\(b\)的\(k\)进制下是\(b_0,b_1,b_2...\),则\(C_a^b≡C_{a_0}^{b_0}*C_{a_1}^{b_1}*C_{a_2}^{b_2}...\)

那么\(C_a^b\%k≡0\)等价于\(k\)进制下存在某一位\(C_{a_i}^{b_i}≡0\),

我们的\(a_i,b_i\)是\([0,k-1]\)范围内的,根据组合数的定义式\(C_a^b={a!\over b! (a-b)!}\),分子和分母涉及的阶乘都小于\(k!\),都没有\(k\)出现,因此是不存在\(a_i>=b_i\)的情况下\(C_{a_i}^{b_i}≡0\)的

只可能是\(a_i<b_i\)的情况\(C_{a_i}^{b_i}=0\)

我们问题转变成了,k进制下,对于\(0\leq i \leq n,0\leq j \leq min(i,m)\),有多少个\(ij\)满足\(k\)进制下存在某一位\(a_i<b_i\),数位dp之

\(f[i][flag1][flag2][flag3]\)表示到了\(k\)进制下第\(i\)位,有没有顶着n的上界,有没有顶着m的上界,之前是不是已经满足了\(a_i<b_i\)

const int N=2e5+5;
const int M=110;
const int mod=1e9+7;
const int inf=1e18;
int t,k,a[M],b[M],cnta,cntb,vis[M][2][2][2][2];
inline int dfs(int s,int bl,int fl,int l1,int l2)
{
    if(s==0) return bl;
    if(vis[s][bl][fl][l1][l2]!=-1) return vis[s][bl][fl][l1][l2];
    int ls1=k-1,ls2=k-1;
    if(l1) ls1=a[s];
    if(l2) ls2=b[s];
    int ans=0;
    for(int i=0;i<=ls1;i++)
    {
        for(int j=0;j<=ls2;j++)
        {
            if(fl==0)
            {
                if(i!=j)
                {
                    if(i>j) ans=(ans+dfs(s-1,bl|(i<j),1,l1&&i==ls1,l2&&j==ls2))%mod;
                    else continue;
                }
                else ans=(ans+dfs(s-1,bl|(i<j),0,l1&&i==ls1,l2&&j==ls2))%mod;
            }
            else ans=(ans+dfs(s-1,bl|(i<j),fl,l1&&i==ls1,l2&&j==ls2))%mod;
            ans=(ans+mod)%mod;
        }
    }
    ans%=mod;
    vis[s][bl][fl][l1][l2]=ans;
    return ans;
}

int n,m;
cin>>n>>m;
cnta=0,cntb=0;
memset(vis,-1,sizeof(vis));
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
while(n)
{
    a[++cnta]=n%k;
    n/=k;
}
while(m)
{
    b[++cntb]=m%k;
    m/=k;
}
cout<<dfs(max(cnta,cntb),0,0,1,1)<<'\n';

代码逐行详解

const int N=2e5+5;
const int M=110; // 最多k为100,1e18转k进制最多60位,足够存储
const int mod=1e9+7;
const int inf=1e18;
int t,k,a[M],b[M],cnta,cntb,vis[M][2][2][2][2];
// t:测试数据组数,k:给定的质数
// a[]:存储n的k进制各位(低位到高位),cnta: a的有效长度
// b[]:存储m的k进制各位(低位到高位),cntb: b的有效长度
// vis:数位DP记忆化数组,维度分别是:当前处理的位、是否已经找到满足条件的对、是否已经出现i>j、n是否还在原范围、m是否还在原范围

inline int dfs(int s,int bl,int fl,int l1,int l2)
{
    // s:当前处理到第s位,bl:是否已经找到符合条件的(i,j)对(1表示已经找到)
    // fl:是否已经出现i>j的情况(1表示已经出现,后续j可以随便选)
    // l1:当前i的选择是否还受n的限制(1表示受限制,最多只能选到a[s])
    // l2:当前j的选择是否还受m的限制(1表示受限制,最多只能选到b[s])
    if(s==0) return bl; // 所有位处理完成,返回是否找到符合条件的对
    if(vis[s][bl][fl][l1][l2]!=-1) return vis[s][bl][fl][l1][l2]; // 记忆化返回
    int ls1=k-1,ls2=k-1; // 当前位i、j的最大可选值
    if(l1) ls1=a[s]; // 如果还受n限制,最大只能选a[s]
    if(l2) ls2=b[s]; // 如果还受m限制,最大只能选b[s]
    int ans=0;
    for(int i=0;i<=ls1;i++) // 枚举当前位i的取值
    {
        for(int j=0;j<=ls2;j++) // 枚举当前位j的取值
        {
            if(fl==0) // 还没出现i>j的情况,需要保证j<=i
            {
                if(i!=j) // 当前位i不等于j
                {
                    if(i>j) ans=(ans+dfs(s-1,bl|(i<j),1,l1&&i==ls1,l2&&j==ls2))%mod; // i>j,标记fl=1,后续j可以随便选
                    else continue; // i<j不符合要求,跳过
                }
                else ans=(ans+dfs(s-1,bl|(i<j),0,l1&&i==ls1,l2&&j==ls2))%mod; // i==j,fl保持0
            }
            else ans=(ans+dfs(s-1,bl|(i<j),fl,l1&&i==ls1,l2&&j==ls2))%mod; // 已经出现i>j,j可以随便选
            ans=(ans+mod)%mod;
        }
    }
    ans%=mod;
    vis[s][bl][fl][l1][l2]=ans;
    return ans;
}
int n,m;
cin>>n>>m;
cnta=0,cntb=0;
memset(vis,-1,sizeof(vis)); // 初始化记忆化数组
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
while(n) // 将n转换为k进制,存入a数组
{
    a[++cnta]=n%k;
    n/=k;
}
while(m) // 将m转换为k进制,存入b数组
{
    b[++cntb]=m%k;
    m/=k;
}
cout<<dfs(max(cnta,cntb),0,0,1,1)<<'\n'; // 从最高位开始DFS

agc043B - 123 Triangle

1. 第一步先做差

令 \(b_i=|a_i-a_{i+1}|\),\(i=1,\dots,N-1\)。此时 \(b_i\in\{0,1,2\}\)。

之后每一步都是对相邻数做绝对差,而绝对差在模 2 意义下就等于减法(因为 \(|x-y|\equiv x-y \pmod 2\))。所以最终答案的奇偶性可以直接用组合数算:

\[x_{N,1}\equiv \sum_{i=1}^{N-1}\binom{N-2}{i-1}b_i \pmod 2 \]

用 Lucas 定理:\(\binom{n}{k}\) 为奇数当且仅当 (n & k) == k,也就是代码里的 check(n-1, i-1)(这里 n 已经被减过 1,所以传的是 n-1 = N-2)。

2. 但答案不只是奇偶性,可能是 0/1/2

关键结论:

  • 如果所有 \(b_i\) 中存在 1,那么最终答案只能是 0 或 1,此时只需算奇偶性即可(模 2 的结果就是答案)。
  • 如果所有 \(b_i\) 都不是 1(即每个 \(b_i\in\{0,2\}\)),那么把每个 \(b_i\) 除以 2,答案也除以 2,变成 0 或 1 的问题,最后再乘回 2。

这正是代码中 flag 的作用:

for(int i=1;i<=n;i++) if(a[i]==1)
{
    flag=1;
    break;
}
if(!flag) for(int i=1;i<=n;i++) a[i]>>=1;   // 全为 0/2 时整体除以 2
...
if(!flag) ans<<=1;                          // 最后答案乘回 2

逐段解释代码

int n,ans=0,flag=0;
cin>>n;          // 原始 N
n--;             // n = N-1,即差值序列长度
string s; cin>>s;
s=' '+s;         // 1-indexed

for(int i=1;i<=n;i++) a[i]=abs(s[i]-s[i+1]);   // 第一步差值 b_i

注意 s[i] 是字符 '1'~'3',s[i]-s[i+1] 是字符 ASCII 差,正好等于数字差(因为 '1','2','3' 连续),再取绝对值得到 \(b_i\)。

for(int i=1;i<=n;i++) if(a[i]==1){flag=1;break;}
if(!flag) for(int i=1;i<=n;i++) a[i]>>=1;for(int i=1;i<=n;i++) if(a[i]==1)
{
    flag=1;
    break;
}
if(!flag) for(int i=1;i<=n;i++) a[i]>>=1;

判断是否存在 1;不存在则整体除 2(此时 \(a_i\in\{0,2\}\to\{0,1\}\))。

for(int i=1;i<=n;i++) if(check(n-1,i-1)) ans^=(a[i]&1);

这里 n 已经是 \(N-1\),所以 n-1 = N-2,对应 \(\binom{N-2}{i-1}\)。用 Lucas 判断奇偶,只把系数为奇数的位置的 \(b_i\) 的奇偶性异或起来——这就是模 2 的求和。

if(!flag) ans<<=1;
cout<<ans;

如果一开始没有 1,说明答案被缩过一半,乘回 2。

[NOIP2025]清仓甩卖

设所有糖果按原价从大到小排序(相同原价按编号)。记为 \(a_1\ge a_2\ge\cdots\ge a_n\)(代码里就是这么排的)。

结论:一个定价方案"不能达到最大原价和",当且仅当存在某个位置,贪心买了一个"性价比更高但原价较小"的糖,占用了钱,导致后面一个"原价更大"的糖买不起。

更精确地,代码中的判定方式是:

对每个 \(i\)(作为分界点),考虑 \(a_i\) 这颗糖。若存在 \(j\) 满足 \(a_j\cdot 2 > a_i\)(即 \(a_j > a_i/2\),原价接近),并且贪心时因为买了这些"小原价、高性价比"的糖而挤掉了 \(a_i\),则这种方案是"坏"的。


为什么答案是 \(2^n - \text{坏方案数}\)?

因为"好方案"(能达到最大值)可能很难直接数,但"坏方案"有一个清晰的结构:

  • 存在一个"临界"的糖 \(a_i\)(按原价降序的第 \(i\) 个),
  • 它本可以被买到(或者它代表最大原价和的一部分),
  • 但因为排序后前面某些糖的定价 \(w=1\)(性价比高),导致钱被花掉,\(a_i\) 买不到。

代码:枚举"坏方案"的临界点

for(int i=1;i<=n;i++)
{
    int pos=i;
    while(pos<=n&&a[pos]==a[i]) pos++;
    pos--;
    int k=n+1;
    for(int j=pos+1;j<=n&&a[j]*2>a[i];j++)
    {
        while(a[j]+a[k-1]<a[i]) k--;
        ans+=C(j-2,m-i-1)*pow2[n-k+1];
        ans%=mod;
    }
}
cout<<(pow2[n]-ans+mod)%mod<<'\n';

这一段是整个算法的核心,我们逐个变量解释:

变量含义

  • \(i\):当前考虑的"原价降序第 \(i\) 个"糖,作为被挤掉的目标。

  • \(pos\):与 \(a_i\) 原价相同的最后一个位置(相同原价的糖,编号小的先考虑,所以它们可以一起处理)。

  • \(j\):从 \(pos+1\) 开始,考虑那些原价比 \(a_i\) 小但大于 \(a_i/2\) 的糖(条件 a[j]*2 > a[i])。

    • 为什么是 \(a_j > a_i/2\)? 因为若 \(a_j\le a_i/2\),则两颗这样的糖原价和 \(\le a_i\),用它们替换 \(a_i\) 不会更优,不构成"坏方案"的关键。只有 \(a_j > a_i/2\) 时,"买两颗小糖 vs 买一颗大糖"才可能出现"买了小的反而总和更大/更小"的微妙关系(性价比排序的干扰)。
  • \(k\):满足 \(a_j + a_{k-1} \ge a_i\) 的最小位置,即"能与 \(a_j\) 配对、原价和 \(\ge a_i\)"的糖的位置下界。while(a[j]+a[k-1]<a[i]) k--; 是从上往下收缩。

组合数部分

ans+=C(j-2, m-i-1)*pow2[n-k+1];

这一项计数的是:

  • 前 \(j\) 个糖中(原价 \(\ge a_j\) 的部分),除去 \(a_i\) 所在的"目标段",要选出恰好让钱花到 \(m-i-1\) 元的方案数:C(j-2, m-i-1)。
  • \(k\) 之后的糖(原价较小的部分)可以任意定价:pow2[n-k+1]。
  • 中间段的定价被唯一确定(为了让 \(a_i\) 被挤掉,需要特定的 \(w\) 配置)。

输出

cout << (pow2[n]-ans+mod)%mod<<'\n';

总方案数 \(2^n\) 减去坏方案数 ans,就是"能达到最大原价和"的方案数。

信封问题

全错排模板
\(D(n)=(n-1)*(D(n-1)+D(n-2))\)
如果n号元素放在了i位置,并且n位置放了i号元素

先选元素i,然后发现方案数为\(D(n-2)\)

结论是\((n-1)*D(n-2)\)

如果n号元素放在了i位置,并且n位置没有放i号元素

先选i,然后发现方案数为\(D(n-1)\)

结论是\((n-1)*D(n-1)\)

d[1]=0;
d[2]=1;
d[3]=2;
for(int i=4;i<=n;i++) d[i]=(i-1)*(d[i-1]+d[i-2]);
cout<<d[n];

[JSOI2011] 分特产

一、核心工具:隔板法

1. 允许空盒

把 \(a_i\) 个相同物品放入 \(n\) 个不同盒子,允许空,方案数:

\[\binom{a_i+n-1}{n-1} \]

2. 不允许空盒

每个盒子至少 1 个:先给每个盒子放 1 个,剩 \(a_i-n\) 个随便放,方案 \(\binom{a_i-1}{n-1}\)(要求 \(a_i\ge n\))。

但本题不同特产可以互补(某个同学可以在某种特产上为 0,只要在另一种上非 0 就行),所以不能对每种特产单独要求"不空",必须用容斥。


二、无限制总数

若不要求每人至少一个,各特产独立:

\[T_0=\prod_{i=1}^{m}\binom{a_i+n-1}{n-1} \]

这就是"所有分配方案"(可能有人空手)。


三、容斥:强制每人至少一个

设 \(A_k\) = "第 \(k\) 个同学没有拿到任何特产"。

我们要求 \(|\overline{A_1}\cap\overline{A_2}\cap\cdots\cap\overline{A_n}|\)。

由容斥原理:

\[\text{ans}=\sum_{S\subseteq\{1,\dots,n\}}(-1)^{|S|}\left|\bigcap_{k\in S}A_k\right| \]

按 \(|S|=k\) 归类,\(\binom{n}{k}\) 种选法。固定 \(S\)(\(k\) 个同学强制空手),剩下 \(n-k\) 个同学分所有特产(允许空),方案:

\[\prod_{i=1}^{m}\binom{a_i+(n-k)-1}{(n-k)-1} \]

所以:

\[\boxed{\text{ans}=\sum_{k=0}^{n}(-1)^k\binom{n}{k}\prod_{i=1}^{m}\binom{a_i+n-k-1}{n-k-1}} \]

边界 \(k=n\)

剩下 0 个同学。若所有 \(a_i=0\),方案为 1(什么都不分,且所有人都"空手",但本题要求每人至少一个,实际这是被容斥的极端项);本题 \(a_i\ge1\),剩下 0 人无法分任何特产,\(\bigcap\) 为空,贡献 0。代码中 rem==0 直接 continue。


四、代码:容斥主循环

int ans=0;
for(int k=0;k<=n;k++)
{
    int rem=n-k;
    if(rem==0) continue;
    int ways=C(n,k);
    for(int i=1;i<=m;i++)
    {
        ways=ways*C(a[i]+rem-1,rem-1)%mod;
    }
    if(k&1) ans=(ans-ways+mod)%mod;
    else ans=(ans+ways)%mod;
}
cout<<ans%mod<<"\n";
return 0;

逐行:

  • for(k=0..n):枚举强制空手的同学数 \(k\)。

  • rem=n-k:剩下参与分配的同学数。

  • rem==0:\(k=n\),无方案,跳过(本题 \(a_i\ge1\) 保证贡献为 0)。

  • ways=C(n,k):选哪 \(k\) 个同学空手,\(\binom{n}{k}\)。

  • 内层 for(i=1..m):对每种特产,剩下 rem 人分 \(a_i\) 个(允许空),乘上 \(\binom{a_i+rem-1}{rem-1}\)。

    • 注意:\(a_i+rem-1=a_i+n-k-1\),rem-1=n-k-1,与公式一致。
  • if(k&1) ans-=ways,否则 ans+=ways:容斥符号 \((-1)^k\)。减法加 mod 防负。

  • 输出 ans%mod。

公式总结

\[\text{ans}=\sum_{k=0}^{n}(-1)^k\binom{n}{k}\prod_{i=1}^{m}\binom{a_i+n-k-1}{n-k-1}\pmod{10^9+7} \]

  • 隔板法:\(a\) 个相同物品分 \(r\) 人允许空 \(=\binom{a+r-1}{r-1}\)。
  • 容斥:强制 \(k\) 人空手,剩余 \(n-k\) 人任意分。
  • 各特产独立,乘积;容斥符号交替。

BZOJ1042 硬币购物

一、题目重述

有 4 种硬币,面值分别为 \(c_1,c_2,c_3,c_4\)。

有 \(n\) 次询问,每次给出:

  • \(d_1,d_2,d_3,d_4\):第 \(i\) 种硬币最多只能用 \(d_i\) 枚;
  • \(s\):要凑出的总价值。

求每次有多少种付款方案,即求非负整数解 \((x_1,x_2,x_3,x_4)\) 的个数:

\[\begin{cases} x_1c_1+x_2c_2+x_3c_3+x_4c_4=s\\ 0\le x_i\le d_i\quad(i=1,2,3,4) \end{cases} \]

数据范围:\(c_i,d_i,s\le 10^5\),\(n\le 1000\)。


二、核心思路:完全背包 + 容斥原理

1. 如果没有数量上限

先忽略 \(x_i\le d_i\) 的限制,只求:

\[x_1c_1+x_2c_2+x_3c_3+x_4c_4=s,\qquad x_i\ge 0 \]

这就是一个完全背包计数问题。设 dp[j] 表示用四种硬币(每种无限枚)凑出价值 \(j\) 的方案数,则:

dp[0]=1;
for(int i=1;i<=4;i++) for(int j=c[i];j<=100000;j++) dp[j]+=dp[j-c[i]];

dp[j] 就是上面方程的非负整数解个数。这个预处理只需做一次,之后所有询问都能用。

2. 加上数量上限——容斥原理

有了上限后,直接 DP 不行(每次 \(d_i\) 不同)。用容斥原理把"上限"这个麻烦转成"无上限"。

定义集合 \(A_i\) 表示"第 \(i\) 种硬币用了超过 \(d_i\) 枚"的方案集合。我们要求的是:

\[\text{答案}=|\text{全集}|-|A_1\cup A_2\cup A_3\cup A_4| \]

由容斥原理:

\[|A_1\cup A_2\cup A_3\cup A_4| =\sum|A_i|-\sum|A_i\cap A_j|+\sum|A_i\cap A_j\cap A_k|-|A_1\cap A_2\cap A_3\cap A_4| \]

所以:

\[\text{答案}=\sum_{S\subseteq\{1,2,3,4\}}(-1)^{|S|}\cdot|\text{满足 }S\text{ 中每种硬币超限的方案数}| \]

3. 如何计算"强制某些硬币超限"的方案数

假设强制集合 \(S\) 中的硬币 \(j\) 至少用 \(d_j+1\) 枚。令:

\[x_j'=x_j-(d_j+1)\ge 0 \]

那么原方程变成:

\[\sum_{j\in S}c_j(d_j+1)+\sum_{i=1}^4 x_i'c_i=s \]

即剩余价值:

\[t=s-\sum_{j\in S}c_j(d_j+1) \]

若 \(t<0\),说明强制超限后已经超过 \(s\),方案数为 0;否则方案数就是 dp[t](因为剩下的部分没有上限,正好是预处理好的完全背包)。

4. 容斥的实现

用二进制枚举 \(S\),i 从 0 到 15,第 \(j-1\) 位为 1 表示第 \(j\) 种硬币在 \(S\) 中:

for(int i=0;i<=15;i++)
{
    int t=sum,cnt=0;
    for(int j=1;j<=4;j++) if((i>>(j-1))&1)
    {
        t-=c[j]*(d[j]+1);
        cnt^=1;          // 记录 |S| 的奇偶
    }
    if(t<0) continue;
    if(!cnt) res+=dp[t];      // |S| 偶数,加
    else res-=dp[t];         // |S| 奇数,减
}
  • cnt 用异或来统计选中个数的奇偶:偶数次异或为 0,奇数次为 1。
  • |S| 偶数(包括空集)符号为 \(+\),奇数符号为 \(-\),正好符合容斥公式。

三、正确性说明

  1. 完全背包正确性:标准完全背包计数,dp[j] 枚举每种硬币无限使用,得到所有非负解。
  2. 容斥正确性:容斥原理是集合论恒等式,对任意集合族成立。这里"全集"是无上限解,"\(A_i\)"是第 \(i\) 种超限的解,答案正是"所有 \(x_i\le d_i\)"的解,即补集大小。
  3. 强制超限的转化:令 \(x_j'=x_j-(d_j+1)\) 是一一对应的双射,所以"\(x_j\ge d_j+1\) 的解"与"\(x_j'\ge0\) 且总价值扣掉 \(c_j(d_j+1)\) 的解"一一对应,数量为 dp[t]。

一句话总结:先用完全背包预处理"无上限"方案数,再用容斥原理把"每种硬币有上限"转化为 16 种"强制某些硬币超限"的子问题,每个子问题扣掉强制部分后直接查 dp 表,奇减偶加即得答案。

水仙花

一、条件化简

相邻两数 \((a_i,a_{i+1})\) 不合法(两个条件都不满足):

  • \(a_i>a_{i+1}\)(不满足条件 A)
  • \(a_{i+1}\mid a_i\)(不满足条件 B)

所以不合法 = 后一项是前一项的真因子(更小且能整除)。

合法序列 = 不存在"相邻且后项是前项真因子"的序列。


二、关键观察

"后项是前项真因子"意味着数值严格下降且至少减半,所以一条连续的坏关系链长度最多 \(\log_2 m\le 17\)。

思路:用容斥。把序列切成若干段,每段内部全是"坏边"(下降整除链),段间是合法边。

设:

  • sum[j] = 长度为 \(j\) 的下降整除链数量(\(a_1>a_2>\dots>a_j\),每步整除)
  • dp[i] = 长度为 \(i\) 的合法序列数,dp[0]=1

递推:

\[dp[i]=\sum_{j=1}^{20}(-1)^{j+1}\,dp[i-j]\cdot sum[j] \]

即 \(j\) 奇加、\(j\) 偶减。因为链长最多约 17,枚举到 20 够用。


三、怎么算 sum[j]

pre[i][j] = 以值 \(j\) 开头、长度 \(i\) 的下降整除链数。

  • 边界:pre[1][j]=1(长度 1,任何值都行),sum[1]=m
  • 转移:长度 \(i\)、开头 \(j\) 的链 = 接上以 \(j\) 的真倍数 \(k\) 开头的长度 \(i-1\) 链:
pre[i][j] = sum_{k=2j,3j,...<=m} pre[i-1][k]
for(int i=2;i<=20;i++)
    for(int j=1;j<=m;j++)
        for(int k=j*2;k<=m;k+=j)
            pre[i][j]+=pre[i-1][k];

sum[i] = 所有 pre[i][j] 之和。

复杂度:倍数枚举 \(\sum m/j=O(m\log m)\),共 20 层,约 \(2.4\times10^7\),可过。


四、DP

dp[0]=1;
for(int i=1;i<=n;i++)
    for(int j=1;j<=20;j++){
        if(i<j) continue;
        if(j%2) dp[i]+=dp[i-j]*sum[j]%mod;
        else dp[i]-=dp[i-j]*sum[j]%mod;
        dp[i]=(dp[i]%mod+mod)%mod;
    }

答案 dp[n]。

符号解释:长度为 \(j\) 的坏链段有 \(j-1\) 条坏边,容斥符号 \((-1)^{j-1}\),即 \(j\) 奇加、\(j\) 偶减。

一句话总结

不合法 = 后项是前项真因子 → 坏链长度只有 \(\log m\) → 预处理坏链数,容斥 DP 拼接,奇加偶减。

代码框架不变,只需注意:pre/sum 预处理一次,DP 用 dp[i-j]*sum[j],符号 j%2 交替,减法取模防负。

BZOJ2839 集合计数

一、题目意思

有一个 \(N\) 元集合,它有 \(2^N\) 个子集。从中选出至少一个子集,使得这些子集的交集元素个数恰好为 \(K\)。求方案数模 \(10^9+7\)。

样例 \(N=3,K=2\):交集恰有 2 个元素,答案 6。


二、核心思路:先"钦定"交集,再容斥

第 1 步:钦定 \(K\) 个元素必须在交集中

从 \(N\) 个元素里选出这 \(K\) 个,方案数 \(\binom{N}{K}\)。

设钦定的集合为 \(S\)(\(|S|=K\))。下面数:所有选中子集都包含 \(S\) 的方案数。

第 2 步:剩余 \(N-K\) 个元素怎么处理

每个子集必须包含 \(S\) 的全部元素,对剩下 \(N-K\) 个元素可以任意取舍。所以一个子集有 \(2^{N-K}\) 种形态。

现在要从这 \(2^{N-K}\) 种形态里选出至少一个子集(非空选取),方案数是:

\[2^{2^{N-K}}-1 \]

(每个"形态"可选/不选,共 \(2^{2^{N-K}}\) 种选法,减去全不选的一种。)

第 3 步:容斥,让交集恰好是 \(K\) 个

上面的数法允许交集多于 \(K\) 个(含 \(S\) 只是至少含 \(K\) 个)。用容斥:

\[\text{ans}=\binom{N}{K}\sum_{i=K}^{N}(-1)^{i-K}\binom{N-K}{i-K}\left(2^{2^{N-i}}-1\right) \]

解释:

  • \(\binom{N-K}{i-K}\):在剩余元素里再选 \(i-K\) 个,凑成"交集至少含 \(i\) 个"。
  • \(2^{2^{N-i}}-1\):固定这 \(i\) 个元素后,剩余 \(N-i\) 个元素任意,子集形态 \(2^{N-i}\) 种,非空选取方案。
  • \((-1)^{i-K}\):容斥符号,\(i-K\) 奇减偶加。

三、代码逐段对应

for(int i=k;i<=n;i++)
{
    ans=(ans+(i%2==k%2?1:mod-1)*C(n-k,i-k)%mod
        *((quick(2,quick(2,n-i,mod-1))+mod-1)%mod)%mod)%mod;
}
cout<<ans*C(n,k)%mod;

对照公式:

公式 代码
\((-1)^{i-K}\) (i%2==k%2?1:mod-1):同奇偶为 \(+1\),否则 \(-1\)
\(\binom{N-K}{i-K}\) C(n-k,i-k)
\(2^{2^{N-i}}-1\) (quick(2,quick(2,n-i,mod-1))+mod-1)%mod
外层 \(\binom{N}{K}\) 最后 *C(n,k)

四、关键细节:指数取模用 \(mod-1\)

算 \(2^{2^{N-i}}\) 时,指数是 \(2^{N-i}\),可能巨大。由费马小定理,模 \(10^9+7\)(质数)下:

\[2^{E}\equiv 2^{E\bmod (mod-1)}\pmod{mod} \]

所以先算 quick(2,n-i,mod-1) 得到 \(E\bmod(mod-1)\),再 quick(2, 那个值) 得到 \(2^E\)。

这就是代码里 quick(2,quick(2,n-i,mod-1)) 的由来。

注意:\(mod-1=10^9+6\) 不是质数,但费马小定理只要求底数 2 与模 \(mod\) 互质,指数模 \(mod-1\) 即可,不需要 \(mod-1\) 是质数。


五、一句话总结

先钦定 \(K\) 个元素在交集中(\(\binom{N}{K}\)),对剩下元素用容斥保证交集恰好 \(K\) 个,每项是 \(2^{2^{N-i}}-1\)(非空选取子集),指数用费马小定理模 \(mod-1\)。

公式:

\[\boxed{\binom{N}{K}\sum_{i=K}^{N}(-1)^{i-K}\binom{N-K}{i-K}\left(2^{2^{N-i}}-1\right)} \]

BZOJ3622 已经没有什么好害怕的了

一、题目转化

\(n\) 个糖果、\(n\) 个药片两两配对。设:

  • \(X\) = "糖果能量 > 药片能量"的组数
  • \(Y\) = "药片能量 > 糖果能量"的组数

由题意 \(X+Y=n\),要求 \(X-Y=k\),即:

\[X=\frac{n+k}{2},\qquad Y=\frac{n-k}{2} \]

先判断:若 \(n+k\) 为奇数,或 \(\frac{n+k}{2}>n\),或 \(k<0\),无解,输出 0。否则令 \(m=\frac{n+k}{2}\),问题变成:恰好有 \(m\) 组"糖果 > 药片"的配对方案数。


二、核心技巧:先算"至少"再容斥

直接求"恰好 \(m\) 组"很难。改用经典套路:

第 1 步:排序 + 预处理

把 \(a\)(糖果)和 \(b\)(药片)都升序排序。

设 cnt[i] = 有多少个药片 \(b_j\) 满足 \(b_j<a_i\)(即第 \(i\) 个糖果能"赢"的药片数)。由于排好序,双指针 \(O(n)\) 求出。

第 2 步:DP 求"钦定 \(i\) 组糖果赢"的方案数

设 f[i][j] = 考虑前 \(i\) 个糖果,钦定其中 \(j\) 个糖果配对给"它能赢的药片"(这些药片互不相同)的方案数。

转移:

  • 第 \(i\) 个糖果不钦定赢:f[i][j] += f[i-1][j]
  • 第 \(i\) 个糖果钦定赢:它能赢 cnt[i] 个药片,但前面已用掉 \(j-1\) 个(都是它能赢的药片中的),所以有 cnt[i]-(j-1) 种选法:

\[f[i][j]=f[i-1][j]+f[i-1][j-1]\cdot(cnt[i]-j+1) \]

边界 f[0][0]=1。

含义:f[n][j] = 钦定 \(j\) 组"糖果赢"的方案数(其余 \(n-j\) 组随便配,但只钦定了 \(j\) 组,剩余未指定)。

第 3 步:把"钦定 \(j\) 组"转成"恰好 \(j\) 组"

f[n][j] 中,一个"恰好有 \(t\) 组赢"的完整配对会被计算 \(\binom{t}{j}\) 次(从 \(t\) 组赢里选 \(j\) 组钦定)。所以:

\[f[n][j]=\sum_{t\ge j}\binom{t}{j}\cdot g[t] \]

其中 \(g[t]\) = 恰好 \(t\) 组赢的方案数。由二项式反演:

\[g[m]=\sum_{j=m}^{n}(-1)^{j-m}\binom{j}{m}f[n][j] \]

但注意 f[n][j] 只钦定了 \(j\) 组,剩下 \(n-j\) 组任意配对的方案数还没乘!完整写:

设 h[j] = 钦定 \(j\) 组赢、其余 \(n-j\) 组任意配的方案数 = \(f[n][j]\cdot(n-j)!\)。

则:

\[h[j]=\sum_{t\ge j}\binom{t}{j}g[t] \]

反演:

\[g[m]=\sum_{j=m}^{n}(-1)^{j-m}\binom{j}{m}f[n][j]\cdot(n-j)! \]


三、代码实现要点

// 排序
sort(a+1,a+n+1); sort(b+1,b+n+1);
// cnt[i]: 双指针
for(int i=1,p=0;i<=n;i++){
    while(p<n&&b[p+1]<a[i])p++;
    cnt[i]=p;
}
// DP
f[0][0]=1;
for(int i=1;i<=n;i++)
    for(int j=0;j<=i;j++){
        f[i][j]=f[i-1][j];
        if(j>0)f[i][j]=(f[i][j]+f[i-1][j-1]*(cnt[i]-j+1))%mod;
    }
// 反演
for(int j=m;j<=n;j++){
    int term=C(j,m)*f[n][j]%mod*fac[n-j]%mod;
    if((j-m)%2)ans=(ans-term+mod)%mod;
    else ans=(ans+term)%mod;
}

四、一句话总结

排序后 DP 求"钦定 \(j\) 组糖果赢"的方案数 \(f[n][j]\),乘以 \((n-j)!\) 得"至少 \(j\) 组"的计数,再用二项式反演得到"恰好 \(m=\frac{n+k}{2}\) 组"的答案。

\[\boxed{g[m]=\sum_{j=m}^{n}(-1)^{j-m}\binom{j}{m}f[n][j]\,(n-j)!} \]

[NOI Online #2 提高组] 游戏

一、把游戏翻译成"匹配"

\(n=2m\) 个点,A、B 各 \(m\) 个。游戏 \(m\) 回合,每回合 A 选一个、B 选一个,配成一对。

一个完整的游戏过程 = 一个 A 到 B 的双射(完美匹配)。因为 A 的每个点恰好被选一次、B 的每个点也恰好被选一次,A 的每个点对应 B 的一个点。

一回合"非平局"⟺ 这对点有祖先-后代关系:

  • A 的点在 B 点的子树内 → A 赢(非平局)
  • B 的点在 A 点的子树内 → B 赢(非平局)

两者不可能同时发生(两个不同点,一个在另一个子树内,反过来不成立)。所以:

非平局对数 = 匹配中有祖先-后代关系的对数。

题目要求:对每个 \(k=0..m\),输出恰好有 \(k\) 对祖先-后代关系的匹配数。


二、套路:先"钦定",再"反演"

直接数"恰好 \(k\) 对"很难。标准套路分两步:

  1. 钦定:先数"钦定 \(j\) 对是祖先-后代关系"的方案数 f[1][j](其余点不管)。
  2. 反演:用二项式反演把"钦定 \(j\) 对"转成"恰好 \(k\) 对"。

三、第 1 步:树形背包求 f[1][j]

定义

f[u][j] = 在 \(u\) 的子树内,钦定 \(j\) 对祖先-后代关系(每对一个 A、一个 B,且一个是另一个的祖先)的方案数。其余点先不配对。

合并孩子(树上背包)

把每个孩子 \(v\) 的 DP 合并进 \(u\):

for(int j=sz[u]/2;j>=0;j--)
    for(int k=1;k<=sz[v]/2;k++)
        f[u][j+k]=(f[u][j+k]+f[u][j]*f[v][k])%mod;

sz[u] 是子树点数,最多 \(\lfloor sz[u]/2\rfloor\) 对。

在 \(u\) 处新增一对

合并完孩子后,考虑让 \(u\) 参与一对。若 \(u\) 属 A,它和子树内某个还没被用的 B 点配对——\(u\) 是祖先,B 点在子树内,合法!

子树内 B 点总数是 cntB[u]。已经钦定了 \(j-1\) 对(每对用掉一个 B),当前这对还要再占一个 B,所以可用 B 点数是:

\[\text{cntB}[u]-(j-1) \]

递推:

\[f[u][j]\;+=\;f[u][j-1]\cdot(\text{cntB}[u]-j+1) \]

\(u\) 属 B 时用 cntA[u] 同理。

边界

f[u][0]=1(一对都不钦定,方案数 1)。

这样 f[1][j] = 整棵树钦定 \(j\) 对的方案数。


四、第 2 步:从"钦定"到"恰好"——最关键的一步

f[1][j] 到底数了什么?

f[1][j] 只钦定了 \(j\) 对。剩下没配对的点完全没管。

此时用了 \(j\) 个 A、\(j\) 个 B,剩下:

  • A 剩 \(m-j\) 个
  • B 剩 \(m-j\) 个

要把这些剩下的 A 和 B 一一配对(双射),方案数是:

\[\boxed{(m-j)!} \]

为什么不是 \((n-2j)!=(2m-2j)!\)?

\((2m-2j)!\) 是把剩下 \(2m-2j\) 个点全部排列的方案数。但这里 A 点和 B 点是分开的两类,配对必须是"A 配 B"。把 A 点排列有 \((m-j)!\) 种,B 点排列有 \((m-j)!\) 种,但配对方案只取决于"哪个 A 对哪个 B",固定 A 的顺序后 B 的排列决定配对,所以是 \((m-j)!\) 种。

简单例子:剩 2 个 A(A1,A2)、2 个 B(B1,B2),配对方案是 2! = 2 种:(A1-B1,A2-B2) 和 (A1-B2,A2-B1)。而全排列 \(4!=24\) 显然太多。

所以定义:

\[h[j]=f[1][j]\cdot(m-j)! \]

\(h[j]\) = "钦定 \(j\) 对祖先关系,其余任意配对"的方案数。

二项式反演

设 \(g[t]\) = 恰好 \(t\) 对祖先关系的完美匹配数。

一个恰好 \(t\) 对的匹配,从中选 \(j\) 对钦定,有 \(\binom{t}{j}\) 种选法。所以:

\[h[j]=\sum_{t\ge j}\binom{t}{j}g[t] \]

二项式反演:

\[g[k]=\sum_{j=k}^{m}(-1)^{j-k}\binom{j}{k}h[j] \]

对每个 \(k=0..m\) 输出 \(g[k]\)。


五、完整正确代码

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=5005;
const int mod=998244353;
const int inf=1e18;
int n,m;
char s[N];
vector<int>g[N];
int sz[N],cntA[N],cntB[N],f[N][N],tmp[N];
int fac[N],inv[N];
int quick(int x,int y)
{
    int res=1;
    while(y)
    {
        if(y&1)res=res*x%mod;
        x=x*x%mod;
        y>>=1;
    }
    return res;
}
int C(int n,int m)
{
    if(n<m||n<0||m<0)return 0;
    return fac[n]*inv[m]%mod*inv[n-m]%mod;
}
void dfs(int u,int fa)
{
    sz[u]=1;
    if(s[u]=='0')cntA[u]=1;
    else cntB[u]=1;
    f[u][0]=1;
    for(int i=0;i<(int)g[u].size();i++)
    {
        int v=g[u][i];
        if(v==fa)continue;
        dfs(v,u);
        for(int j=0;j<=sz[u]/2+1;j++)tmp[j]=f[u][j];
        for(int j=sz[u]/2+1;j>=0;j--)f[u][j]=0;
        for(int j=0;j<=sz[u]/2+1;j++)
        {
            if(!tmp[j])continue;
            for(int k=0;k<=sz[v]/2+1;k++)
            {
                if(!f[v][k])continue;
                f[u][j+k]=(f[u][j+k]+tmp[j]*f[v][k])%mod;
            }
        }
        sz[u]+=sz[v];
        cntA[u]+=cntA[v];
        cntB[u]+=cntB[v];
    }
    int lim=sz[u]/2;
    if(s[u]=='0')
    {
        for(int j=lim;j>=1;j--)
        {
            int use=cntB[u]-(j-1);
            if(use>0)f[u][j]=(f[u][j]+f[u][j-1]*use%mod)%mod;
        }
    }
    else
    {
        for(int j=lim;j>=1;j--)
        {
            int use=cntA[u]-(j-1);
            if(use>0)f[u][j]=(f[u][j]+f[u][j-1]*use%mod)%mod;
        }
    }
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    m=n/2;
    cin>>(s+1);
    for(int i=1;i<n;i++)
    {
        int u,v;
        cin>>u>>v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    fac[0]=1;
    for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod;
    inv[n]=quick(fac[n],mod-2);
    for(int i=n-1;i>=0;i--)inv[i]=inv[i+1]*(i+1)%mod;
    dfs(1,0);
    for(int k=0;k<=m;k++)
    {
        int ans=0;
        for(int j=k;j<=m;j++)
        {
            int h=f[1][j]*fac[m-j]%mod;
            int term=C(j,k)*h%mod;
            if((j-k)&1)ans=(ans-term+mod)%mod;
            else ans=(ans+term)%mod;
        }
        cout<<ans<<"\n";
    }
    return 0;
}

七、为什么"钦定"能这样数(正确性直觉)

有人会问:f[1][j] 钦定 \(j\) 对时,会不会重复计数?

不会。 因为 f[1][j] 是"选出 \(j\) 对具体的有序配对(哪个 A 配哪个 B)",每对是确定的。后续乘 \((m-j)!\) 是给剩余点做双射。一个完整匹配如果恰好有 \(t\) 对祖先关系,它被 \(h[j]\) 统计到 \(\binom{t}{j}\) 次(从 \(t\) 对里选 \(j\) 对当作"钦定"的),这正是二项式反演需要的系数。

八、一句话总结

树形背包数"钦定 \(j\) 对祖先关系"的方案 f[1][j] → 剩余 \(m-j\) 个 A 与 \(m-j\) 个 B 做双射,乘 \((m-j)!\) → 二项式反演得恰好 \(k\) 对 → 对每个 \(k\) 输出。

核心坑:剩余点是 A 配 B 的双射,乘 \((m-j)!\),不是 \((2m-2j)!\)。

[CF285E]Positions in Permutations

一、题意转化

排列 \(p_1\dots p_n\) 可看成位置 \(1..n\) 与值 \(1..n\) 的完美匹配。

位置 \(i\) 是好位置 \(\iff\) 它配的值是 \(i-1\)(往下)或 \(i+1\)(往上)。

求恰好 \(k\) 个好位置的匹配数。


二、冲突分析

值 \(v\) 作为好值,只能被两个位置使用:

  • 位置 \(v-1\) 往上(\(p_{v-1}=v\))
  • 位置 \(v+1\) 往下(\(p_{v+1}=v\))

所以值 \(v\) 被距离 2 的两个位置争抢。

画图(\(n=5\)):

位置:   1      2      3      4      5
好值:   2     1,3    2,4    3,5     4
  • 值 2:位置 1(往上)、位置 3(往下)抢
  • 值 3:位置 2(往上)、位置 4(往下)抢
  • 值 4:位置 3(往上)、位置 5(往下)抢

关键结论:

  • 距离 1 不冲突:位置 \(i\) 往下用 \(i-1\),位置 \(i-1\) 往上用 \(i\),值不同,可同时成立(如 \((2,1)\) 两个好位置)。
  • 距离 2 冲突:位置 \(i\) 往下用 \(i-1\),位置 \(i-2\) 往上也用 \(i-1\),抢同一个值。

三、4 状态 DP

3.1 状态设计

处理到位置 \(i\) 后,为了判断未来是否冲突,必须记住两件事:

变量 含义
a 值 \(i\) 是否被位置 \(i-1\) 使用(位置 \(i-1\) 往上)
b 值 \(i+1\) 是否被位置 \(i\) 使用(位置 \(i\) 往上)

为什么记 b:下一步处理位置 \(i+1\) 时,判断它能否往下(用值 \(i\))要看 a;而位置 \(i+1\) 若往上(用值 \(i+2\)),这个信息要传给位置 \(i+2\),所以现在就要把"值 \(i+1\) 是否被位置 \(i\) 用"记成 b,下一步它变成新的 a。

\[dp[i][j][a][b] \]

= 处理完前 \(i\) 个位置,钦定 \(j\) 个好位置,状态为 \((a,b)\) 的方案数。

3.2 转移(从位置 \(i\) 到 \(i+1\))

从 dp[i][j][a][b] 出发,决定位置 \(i+1\) 怎么放:

决策 1:位置 \(i+1\) 不放好值

新状态:值 \(i+1\) 是否被位置 \(i\) 用 = b;值 \(i+2\) 是否被位置 \(i+1\) 用 = 0。

\[dp[i+1][j][b][0]\;+=\;dp[i][j][a][b] \]

决策 2:位置 \(i+1\) 往下,用值 \(i\)

  • 前提:值 \(i\) 没被占,即 \(a=0\);且 \(i\ge1\)(值 \(i\) 存在)。
  • 新状态:\(a'=b\),\(b'=0\)。
  • 好位置数 \(j+1\)。

\[dp[i+1][j+1][b][0]\;+=\;dp[i][j][a][b]\qquad(a=0,\ i\ge1) \]

决策 3:位置 \(i+1\) 往上,用值 \(i+2\)

  • 前提:\(i+2\le n\)(值 \(i+2\) 存在)。
  • 新状态:\(a'=b\),\(b'=1\)(位置 \(i+1\) 占了值 \(i+2\))。
  • 好位置数 \(j+1\)。

\[dp[i+1][j+1][b][1]\;+=\;dp[i][j][a][b]\qquad(i+2\le n) \]

注意:新状态的 a' 永远等于旧 b,因为"值 \(i+1\) 是否被位置 \(i\) 用"这个信息在新一轮成为"值 \((i+1)\) 是否被位置 \((i+1)-1\) 用"。

3.3 边界

  • 初始 dp[0][0][0][0]=1。
  • 位置 1 不能往下:决策 2 要求 \(i\ge1\),处理位置 1 时 \(i=0\),自动排除。
  • 位置 \(n\) 不能往上:决策 3 要求 \(i+2\le n\),处理位置 \(n\) 时 \(i=n-1\),\(n+1\le n\) 不成立,自动排除。

3.4 取 f[j]

\[f[j]=\sum_{a,b}dp[n][j][a][b] \]

f[j] = 钦定 \(j\) 个好位置的方案数(其余位置不管)。


四、二项式反演

4.1 乘 \((n-j)!\)

f[j] 只钦定 \(j\) 个好位置(已指定这 \(j\) 个位置放什么值)。剩下 \(n-j\) 个位置与 \(n-j\) 个未被使用的值做任意双射,方案数 \((n-j)!\)。

\[h[j]=f[j]\cdot(n-j)! \]

= "至少钦定 \(j\) 个好位置"的计数。

4.2 反演公式

恰好 \(t\) 个好位置的排列,会被 \(h[j]\) 统计 \(\binom{t}{j}\) 次(从 \(t\) 个好位置里选 \(j\) 个当"钦定"):

\[h[j]=\sum_{t\ge j}\binom{t}{j}g[t] \]

反演:

\[g[k]=\sum_{j=k}^{n}(-1)^{j-k}\binom{j}{k}h[j] \]


五、完整代码

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=1005;
const int mod=1e9+7;
const int inf=1e18;
int dp[2][N][2][2],f[N],fac[N],inv[N];
int quick(int x,int y)
{
    int res=1;
    while(y)
    {
        if(y&1)res=res*x%mod;
        x=x*x%mod;
        y>>=1;
    }
    return res;
}
int C(int n,int m)
{
    if(n<m||n<0||m<0)return 0;
    return fac[n]*inv[m]%mod*inv[n-m]%mod;
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n,k;
    cin>>n>>k;
    int cur=0;
    dp[0][0][0][0]=1;
    for(int i=1;i<=n;i++)
    {
        int nxt=cur^1;
        memset(dp[nxt],0,sizeof(dp[nxt]));
        for(int j=0;j<=i;j++)
            for(int a=0;a<2;a++)
                for(int b=0;b<2;b++)
                {
                    int val=dp[cur][j][a][b];
                    if(!val)continue;
                    // 不选好值
                    dp[nxt][j][b][0]=(dp[nxt][j][b][0]+val)%mod;
                    // 往下:用值 i-1,要求 a=0 且 i>=2
                    if(a==0&&i>=2)
                        dp[nxt][j+1][b][0]=(dp[nxt][j+1][b][0]+val)%mod;
                    // 往上:用值 i+1,要求 i+1<=n
                    if(i+1<=n)
                        dp[nxt][j+1][b][1]=(dp[nxt][j+1][b][1]+val)%mod;
                }
        cur=nxt;
    }
    for(int j=0;j<=n;j++)
        for(int a=0;a<2;a++)
            for(int b=0;b<2;b++)
                f[j]=(f[j]+dp[cur][j][a][b])%mod;
    fac[0]=1;
    for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod;
    inv[n]=quick(fac[n],mod-2);
    for(int i=n-1;i>=0;i--)inv[i]=inv[i+1]*(i+1)%mod;
    int ans=0;
    for(int j=k;j<=n;j++)
    {
        int h=f[j]*fac[n-j]%mod;
        int term=C(j,k)*h%mod;
        if((j-k)&1)ans=(ans-term+mod)%mod;
        else ans=(ans+term)%mod;
    }
    cout<<ans;
    return 0;
}

六、一句话总结

好值被距离 2 的位置争抢,用 4 状态 (值 i 是否被位置 i-1 用, 值 i+1 是否被位置 i 用) DP 求出钦定 \(j\) 个好位置的方案 f[j],乘 \((n-j)!\),二项式反演得恰好 \(k\) 个。

[THUPC 2019]过河卒二 / chess

一、题意

\(N\times M\) 棋盘,起点 \((1,1)\)。每步可走:

  • 向上:\((x+1,y)\)
  • 向右:\((x,y+1)\)
  • 向右上:\((x+1,y+1)\)

走出棋盘(越过上边界或右边界)即成功。有 \(K\le20\) 个障碍不能经过。求方案数模 \(59393\)。

\(N\le10^9\),\(M\le10^5\),\(K\le20\)。


二、\(W(dx,dy)\):三方向走格计数

从某点走 \(dx\) 步纵向、\(dy\) 步横向(允许右上步)的方案数。设右上走 \(k\) 步:

\[W(dx,dy)=\sum_{k=0}^{\min(dx,dy)}\binom{dx}{k}\binom{dy}{k}2^k \]

为什么 \(2^k\):每对"上+右"可交换顺序,对应 \(2\) 种排列。

int W(int dx,int dy)
{
    if(dx<0||dy<0)return 0;
    if(dy>dx)swap(dx,dy);
    int res=0;
    for(int k=0;k<=dy;k++)
    {
        res+=C_mod(dx,k)*C_mod(dy,k)%mod*pow2[k]%mod;
        if(res>=mod)res-=mod;
    }
    return res;
}

\(dx\) 可达 \(10^9\),但 \(k\) 只到 \(\min(dx,dy)\le M\le10^5\),循环可控;\(\binom{dx}{k}\) 用 Lucas 算。


三、\(F(x,y)\):从 \((x,y)\) 走出棋盘

令 \(A=N-x\),\(B=M-y\)(到上边界、右边界的距离)。

3.1 枚举最后一步

  • 走到 \((N,y')\) 再出去:\(y'=y..M-1\) 各有 2 种(上、右上);\(y'=M\) 角落有 3 种(上、右、右上)。
  • 走到 \((x',M)\) 再出去:\(x'=x..N-1\) 各有 2 种;\(x'=N\) 角落有 3 种。

角落 \((N,M)\) 在两类中都被算,各贡献 3 次,实际应只算 3 次,多算 3 次需扣掉。

整理得:

\[F(x,y)=2\sum_{dy=0}^{B}W(A,dy)+2\sum_{dx=0}^{A}W(dx,B)-W(A,B) \]

3.2 Hockey-stick 化简

\[\sum_{dy=0}^{B}W(A,dy)=\sum_{k}\binom{A}{k}2^k\binom{B+1}{k+1}=S \]

\[\sum_{dx=0}^{A}W(dx,B)-W(A,B)=\sum_{k}\binom{B}{k}2^k\binom{A}{k+1}=T \]

所以:

\[\boxed{F(x,y)=2S+2T+W(A,B)} \]

int F(int x,int y)
{
    int A=N-x;
    int B=M-y;
    int S=0,T=0;
    for(int k=0;k<=B;k++)
    {
        S=(S+C_mod(A,k)*pow2[k]%mod*C_mod(B+1,k+1))%mod;
        T=(T+C_mod(B,k)*pow2[k]%mod*C_mod(A,k+1))%mod;
    }
    int WAB=W(A,B);
    int res=(2*S+2*T+WAB)%mod;
    return res;
}

注意 A=N-x、B=M-y 中的 N,M 必须是输入的棋盘大小(全局变量),不能是无关常量。


四、障碍容斥

障碍按 \(x\) 升序(同 \(x\) 按 \(y\))排序。

4.1 到障碍不经过其他障碍

\[f[i]=W(p_i.x-1,p_i.y-1)-\sum_{j<i,\ p_j\le p_i}f[j]\cdot W(p_i.x-p_j.x,p_i.y-p_j.y) \]

\(p_j\le p_i\) 指 \(p_j.x\le p_i.x\) 且 \(p_j.y\le p_i.y\)。

for(int i=1;i<=K;i++)
{
    f[i]=W(p[i].x-1,p[i].y-1);
    for(int j=1;j<i;j++)
        if(p[j].x<=p[i].x&&p[j].y<=p[i].y)
        {
            int w=W(p[i].x-p[j].x,p[i].y-p[j].y);
            f[i]=(f[i]-f[j]*w%mod+mod)%mod;
        }
}

4.2 答案

\[\text{ans}=F(1,1)-\sum_{i=1}^{K}f[i]\cdot F(p_i.x,p_i.y) \]

int ans=F(1,1);
for(int i=1;i<=K;i++)
    ans=(ans-f[i]*F(p[i].x,p[i].y)%mod+mod)%mod;

五、Lucas 定理

\(p=59393\) 是质数,\(N\le10^9<p^2\)。\(\binom{n}{m}\bmod p\) 用 Lucas:

\[\binom{n}{m}\equiv\binom{n\bmod p}{m\bmod p}\cdot\binom{\lfloor n/p\rfloor}{\lfloor m/p\rfloor}\pmod p \]

第二因子中 \(\lfloor m/p\rfloor\in\{0,1\}\)(因 \(m\le10^5\)):

  • 若 \(\lfloor m/p\rfloor=0\):因子为 1
  • 若 \(=1\):因子为 \(\lfloor n/p\rfloor\)
inline int C_mod(int n,int m)
{
    if(m<0||m>n)return 0;
    int n0=n%mod,n1=n/mod;
    int m0=m%mod,m1=m/mod;
    if(m1>n1)return 0;
    int res=1;
    if(m1==1)
    {
        if(n1==0)return 0;
        res=n1%mod;
    }
    if(m0>n0)return 0;
    res=res*fac[n0]%mod*invfac[m0]%mod*invfac[n0-m0]%mod;
    return res;
}

预处理 \(0..p-1\) 的 fac 和 invfac。


六、完整正确代码

#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
#define CCF
using namespace std;
const int mod=59393;
const int inf=1e18;
const int MAXM=100005;
int fac[mod],invfac[mod],pow2[MAXM];
int N,M,K;
int quick(int a,int b)
{
    int res=1;
    while(b)
    {
        if(b&1)res=res*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return res;
}
inline int C_small(int n,int m)
{
    if(m<0||m>n)return 0;
    return fac[n]*invfac[m]%mod*invfac[n-m]%mod;
}
inline int C_mod(int n,int m)
{
    if(m<0||m>n)return 0;
    int n0=n%mod,n1=n/mod;
    int m0=m%mod,m1=m/mod;
    if(m1>n1)return 0;
    int res=1;
    if(m1==1)
    {
        if(n1==0)return 0;
        res=n1%mod;
    }
    if(m0>n0)return 0;
    res=res*fac[n0]%mod*invfac[m0]%mod*invfac[n0-m0]%mod;
    return res;
}
int W(int dx,int dy)
{
    if(dx<0||dy<0)return 0;
    if(dy>dx)swap(dx,dy);
    int res=0;
    for(int k=0;k<=dy;k++)
    {
        res+=C_mod(dx,k)*C_mod(dy,k)%mod*pow2[k]%mod;
        if(res>=mod)res-=mod;
    }
    return res;
}
int F(int x,int y)
{
    int A=N-x;
    int B=M-y;
    int S=0,T=0;
    for(int k=0;k<=B;k++)
    {
        S=(S+C_mod(A,k)*pow2[k]%mod*C_mod(B+1,k+1))%mod;
        T=(T+C_mod(B,k)*pow2[k]%mod*C_mod(A,k+1))%mod;
    }
    int WAB=W(A,B);
    int res=(2*S+2*T+WAB)%mod;
    return res;
}
struct Point
{
    int x,y;
    bool operator<(const Point &o)const
    {
        if(x!=o.x)return x<o.x;
        return y<o.y;
    }
};
Point p[25];
int f[25];
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
#ifndef CCF
    freopen(".in","r",stdin);
    freopen(".out","w",stdout);
#endif
    cin>>N>>M>>K;
    for(int i=1;i<=K;i++)
    {
        cin>>p[i].x>>p[i].y;
    }
    fac[0]=1;
    for(int i=1;i<mod;i++)fac[i]=fac[i-1]*i%mod;
    invfac[mod-1]=quick(fac[mod-1],mod-2);
    for(int i=mod-2;i>=0;i--)invfac[i]=invfac[i+1]*(i+1)%mod;
    pow2[0]=1;
    for(int i=1;i<MAXM;i++)pow2[i]=pow2[i-1]*2%mod;
    sort(p+1,p+K+1);
    for(int i=1;i<=K;i++)
    {
        f[i]=W(p[i].x-1,p[i].y-1);
        for(int j=1;j<i;j++)
        {
            if(p[j].x<=p[i].x&&p[j].y<=p[i].y)
            {
                int w=W(p[i].x-p[j].x,p[i].y-p[j].y);
                f[i]=(f[i]-f[j]*w%mod+mod)%mod;
            }
        }
    }
    int ans=F(1,1);
    for(int i=1;i<=K;i++)
    {
        int fi=F(p[i].x,p[i].y);
        ans=(ans-f[i]*fi%mod+mod)%mod;
    }
    cout<<ans%mod<<"\n";
    return 0;
}

[HAOI2017]方案数

斯特林数

一、 题意

三维空间,起点 \((0,0,0)\),终点 \((n,m,r)\)。每步把某一个坐标换成它的超集(按位包含):

\[a\subseteq b\iff a\land b=a\quad(\text{即 }a\text{ 的每个 1 位,}b\text{ 也是 1}) \]

求不经过障碍的路径数,模 \(998244353\)。\(n,m,r\le10^{18}\),障碍 \(o\le10^4\)。


二、关键观察:三维独立 + 交错

每步只动一个坐标,所以:

一条三维路径 = 一条 \(x\) 方向的一维路径 + 一条 \(y\) 方向的一维路径 + 一条 \(z\) 方向的一维路径,三条路径的步骤交错排列。

这提示我们用"分步/交错"的组合方法。


三、一维:从 \(0\) 到 \(x\)(最重要的一步)

3.1 一维问题

从 \(0\) 走到 \(x\),每步把当前数 \(u\) 换成 \(u'\),满足 \(u\subseteq u'\) 且 \(u'\subseteq x\)(不能超出目标)。求方案数。

3.2 等价刻画

设 \(x\) 的二进制里有 \(k\) 个 \(1\)(即 \(k=\operatorname{popcount}(x)\))。把这些 \(1\) 位记作"目标位"。

一维路径的每一步,相当于:把当前还没设置的目标位里,挑一个非空子集一次性全部设上。

所以"从 \(0\) 到 \(x\)"等价于:

把 \(k\) 个目标位分成若干个有序非空块(块的顺序 = 操作顺序,每块一次操作全部设上)。

3.3 方案数

设分了 \(a\) 步(\(a\) 个块)。把 \(k\) 个不同元素分成 \(a\) 个非空无序块的方案数是第二类斯特林数 \(S(k,a)\)。再给块排顺序,乘 \(a!\):

\[\text{一维用 }a\text{ 步的方案数}=S(k,a)\cdot a! \]


四、三维交错:除以 \(a!b!c!\) 的抵消

4.1 交错数

\(x\) 方向用 \(a\) 步,\(y\) 方向 \(b\) 步,\(z\) 方向 \(c\) 步,共 \(a+b+c\) 步。把这三个方向的操作交错排列的方案数(同类步骤内部顺序已确定,只需选位置)是多重组合:

\[\frac{(a+b+c)!}{a!\,b!\,c!} \]

4.2 合并一维与交错

一维内部已经乘了 \(a!\)(块排序),交错时又除以 \(a!\),恰好抵消!

\[\text{总方案}=\sum_{a,b,c}\underbrace{S(k_1,a)a!}_{\text{一维}}\cdot S(k_2,b)b!\cdot S(k_3,c)c!\cdot\frac{(a+b+c)!}{a!b!c!} \]

\[\boxed{H(k_1,k_2,k_3)=\sum_{a,b,c}S(k_1,a)S(k_2,b)S(k_3,c)\,(a+b+c)!} \]

这就是为什么最终公式里只有 \(S\),没有 \(a!\)。 这是本题最容易错的地方——很多人以为一维系数是 \(a!S(k,a)\),代入后忘记交错时除以 \(a!\),或者记错了。


五、公式的直觉理解

\(H(k_1,k_2,k_3)\) 的物理意义:

  • \(S(k_1,a)S(k_2,b)S(k_3,c)\):三个维度各自把目标位分成 \(a,b,c\) 个无序块。
  • \((a+b+c)!\):把 \(a+b+c\) 个块排成全序(作为操作序列)。

为什么用无序块 + 全排列,而不是有序块?

因为三个方向的块混在一起全排列时,同方向的块谁先谁后也被决定了。这样"块的内容"和"块的位置"分开处理,公式最干净。如果一维用有序块(\(a!S\)),那么交错时必须除以 \(a!\) 才能去重。


六、预处理 \(H\) 表

\(k_i\le60\)(\(10^{18}<2^{60}\)),\(H\) 大小 \(61^3\approx22.7\) 万。

直接三重求和 \(a,b,c\) 是 \(O(60^6)\) 太慢。用卷积分层:

6.1 合并后两维

\[G[k_2][k_3][t]=\sum_{b+c=t}S(k_2,b)S(k_3,c) \]

6.2 与阶乘卷积

\[P[k_2][k_3][a]=\sum_{t}G[k_2][k_3][t]\,(a+t)! \]

6.3 合并第一维

\[H[k_1][k_2][k_3]=\sum_{a}S(k_1,a)\,P[k_2][k_3][a] \]

总运算量约 \(60^4\approx1.3\times10^7\)(每层常数很小),可过。

// 第二类斯特林数
S[0][0]=1;
for(int n=1;n<MAXB;n++)
    for(int k=1;k<=n;k++)
        S[n][k]=(S[n-1][k-1]+k*S[n-1][k])%MOD;

// G
for(int k2=0;k2<MAXB;k2++)
  for(int k3=0;k3<MAXB;k3++)
    for(int b=0;b<=k2;b++)
      for(int c=0;c<=k3;c++)
        G[k2][k3][b+c]+=S[k2][b]*S[k3][c];

// P
for(int k2...;k3...;a...)
    for(int t=0;t<=k2+k3;t++)
        P[k2][k3][a]+=G[k2][k3][t]*fact[a+t];

// H
for(int k1...;k2...;k3...)
    for(int a=0;a<=k1;a++)
        H[k1][k2][k3]+=S[k1][a]*P[k2][k3][a];

七、障碍容斥

7.1 筛障碍

障碍 \((x,y,z)\) 只有在 \(x\subseteq n,\ y\subseteq m,\ z\subseteq r\) 时才可能出现在某条合法路径上(否则从它出发到不了终点)。其余直接丢弃。

if((x&n)==x && (y&m)==y && (z&r)==z)
    vec.push_back(...);

7.2 排序

按 \((x,y,z)\) 升序排序。重要性质:若 \(A\subseteq B\) 且 \(A\ne B\),则 \(A\) 的坐标逐项小于等于 \(B\),字典序一定 \(A<B\)。所以所有能到 \(B\) 的点都排在 \(B\) 前面。

7.3 DP

把终点也加入 vec(终点不是障碍,只是作为求答案的对象),排序后它一定是最后一个。

\[f[i]=\text{从原点走到点 }i\text{ 且不经过前面任何障碍的方案数} \]

全集:\(H(\operatorname{popcount}(p_i))\)。

减去:经过某个障碍 \(j<i\)(\(p_j\subseteq p_i\))的方案。从 \(p_j\) 到 \(p_i\) 的"增量"是逐维 popcount 之差:

\[d_1=\operatorname{popcount}(p_i.x)-\operatorname{popcount}(p_j.x) \]

(因为 \(p_j\subseteq p_i\),\(p_i\) 的 1 位比 \(p_j\) 多出的部分正好是还要设置的位数。)

\[f[i]=H(\operatorname{pc}(p_i))-\sum_{j<i,\,p_j\subseteq p_i}f[j]\cdot H(d_1,d_2,d_3) \]

注意这里 \(f[j]\) 已经是"到 \(j\) 不经过更早障碍"的方案,所以减去的每项都代表"第一次经过的障碍是 \(j\)",正好容斥,不重不漏。

for(int i=0;i<sz;i++){
    f[i]=H[pcx][pcy][pcz];
    for(int j=0;j<i;j++){
        if((v.x&~u.x)==0 && (v.y&~u.y)==0 && (v.z&~u.z)==0){
            int d1=pcx-v.pcx, d2=pcy-v.pcy, d3=pcz-v.pcz;
            f[i]=(f[i]-f[j]*H[d1][d2][d3]%MOD+MOD)%MOD;
        }
    }
}

7.4 答案

f[sz-1](终点在最后)。


八、 一句话总结

三维独立 → 每个维度把 popcount 个目标位分成块(斯特林数)→ 三方向块全排列 \((a+b+c)!\)(\(a!\) 抵消)→ 预处理 \(H\) 表 → 障碍按字典序排序后容斥 DP,\(f[i]=H(\operatorname{pc}(p_i))-\sum_{j\subseteq i}f[j]H(\Delta)\),答案 \(f[\text{终点}]\)。

容斥

一、题意回顾

三维空间,起点 \((0,0,0)\),终点 \((n,m,r)\)。每步把某一个坐标换成它的按位超集(\(a\subseteq b\iff a\land b=a\))。有 \(o\) 个障碍不能经过。求不经过障碍的方案数模 \(998244353\)。

\(n,m,r\le10^{18}\),\(o\le10^4\)。


二、为什么三维可以"拆开理解"

每步只动一个坐标,所以:

一条三维路径 = 三条一维路径(\(x\) 方向、\(y\) 方向、\(z\) 方向)交错排列。

关键:每一步动哪个坐标是任意的,没有固定顺序。所以"交错"是本质。

传统做法用斯特林数 + 多重排列显式计算交错,但那样容易把系数搞错(多乘/漏除 \(a!\))。下面用直接递推绕开这个坑。


三、一维问题的递推

3.1 一维在做什么

从 \(0\) 走到 \(x\),每步把当前数 \(u\) 换成 \(u'\)(\(u\subseteq u'\subseteq x\))。

等价地:把 \(x\) 的二进制里那些 \(1\) 位(共 \(k=\operatorname{popcount}(x)\) 个)一批一批地设置上,每批挑还没设的若干个位一次性设。

3.2 定义

\(f[k]\) = 还剩 \(k\) 个位要设置时,到目标的一维方案数。

3.3 递推

第一步从 \(k\) 个待设位里挑 \(j\) 个(\(1\le j\le k\))一次性设上,选法 \(\binom{k}{j}\),剩下 \(k-j\) 个位继续:

\[f[k]=\sum_{j=1}^{k}\binom{k}{j}f[k-j],\qquad f[0]=1 \]

这就是 Fubini 数,不用显式算斯特林数。

验证:\(f[1]=\binom11f[0]=1\);\(f[2]=\binom21f[1]+\binom22f[0]=2+1=3\)(即 \(00\to11\)、\(00\to01\to11\)、\(00\to10\to11\))。


四、三维问题的递推(核心)

4.1 定义

\[H[k_1][k_2][k_3] \]

= 三维空间中,当前点到终点,\(x,y,z\) 方向分别还有 \(k_1,k_2,k_3\) 个位要设置时的方案数。

注意:\(H\) 只依赖剩余的位数,不依赖具体的数是多少!因为按位包含的操作只关心"哪些位还没设",而位数相同的目标,结构完全同构。

4.2 第一步枚举

从状态 \((k_1,k_2,k_3)\) 出发,第一步只能动一个坐标:

  • 动 \(x\):从 \(k_1\) 个待设位里挑 \(j\) 个(\(1\le j\le k_1\)),选法 \(\binom{k_1}{j}\),进入状态 \((k_1-j,k_2,k_3)\)。
  • 动 \(y\):\(\binom{k_2}{j}\),进入 \((k_1,k_2-j,k_3)\)。
  • 动 \(z\):\(\binom{k_3}{j}\),进入 \((k_1,k_2,k_3-j)\)。

所以:

\[H[k_1][k_2][k_3]= \underbrace{\sum_{j=1}^{k_1}\binom{k_1}{j}H[k_1-j][k_2][k_3]}_{\text{第一步动 }x} +\underbrace{\sum_{j=1}^{k_2}\binom{k_2}{j}H[k_1][k_2-j][k_3]}_{\text{第一步动 }y} +\underbrace{\sum_{j=1}^{k_3}\binom{k_3}{j}H[k_1][k_2][k_3-j]}_{\text{第一步动 }z} \]

边界:\(H[0][0][0]=1\)。

4.3 为什么这样自动处理了"交错"

传统做法要显式算"\(a\) 步 \(x\)、\(b\) 步 \(y\)、\(c\) 步 \(z\) 的全排列数"。这里不需要:

  • 每次递推"第一步动哪个坐标",就是一次交错的决策。
  • 递归下去自然枚举了所有交错顺序。
  • 没有任何"除以 \(a!\)"的麻烦,因为每一步是"这个位置动哪个坐标",天然区分。

4.4 递推顺序

每项里某个坐标严格减小,所以按 \(k_1\) 升序、再 \(k_2\)、再 \(k_3\) 顺序算,被依赖的状态一定已经算过。

H[0][0][0]=1;
for(int k1=0;k1<MAXB;k1++)
    for(int k2=0;k2<MAXB;k2++)
        for(int k3=0;k3<MAXB;k3++)
        {
            if(k1==0&&k2==0&&k3==0)continue;
            int sum=0;
            for(int j=1;j<=k1;j++)
                sum=(sum+C[k1][j]*H[k1-j][k2][k3])%mod;
            for(int j=1;j<=k2;j++)
                sum=(sum+C[k2][j]*H[k1][k2-j][k3])%mod;
            for(int j=1;j<=k3;j++)
                sum=(sum+C[k3][j]*H[k1][k2][k3-j])%mod;
            H[k1][k2][k3]=sum;
        }

五、\(P(a\to b)\) 与 \(H\) 的对应

从点 \(a\) 到点 \(b\)(\(a\subseteq b\),即 \(b\) 是 \(a\) 的超集)的路径数:

\[P(a\to b)=H[k_1][k_2][k_3] \]

其中 \(k_i=\operatorname{popcount}(b_i)-\operatorname{popcount}(a_i)\)。

为什么:因为 \(a\subseteq b\),\(a\) 的每个 1 位在 \(b\) 里也是 1。所以"从 \(a\) 到 \(b\) 要新设置的位数"就是 \(\operatorname{pc}(b_i)-\operatorname{pc}(a_i)\)。而 \(H\) 只关心这个位数。


六、障碍容斥(不变)

6.1 筛障碍

障碍 \(p=(x,y,z)\) 若 \(p\nsubseteq T=(n,m,r)\),则路径走不到它(或从它走不到终点),直接丢弃:

if((x&n)==x&&(y&m)==y&&(z&r)==z)
    vec.push_back(...);

6.2 排序

按 \((x,y,z)\) 字典序升序。性质:若 \(p_j\subseteq p_i\) 且 \(p_j\ne p_i\),则 \(p_j\) 字典序 \(<p_i\)。所以能到达 \(p_i\) 的障碍全在它前面。

6.3 DP

把终点也加入 vec(作为最后一个点)。定义:

\[f[i]=\text{从 }O\text{ 到 }p_i\text{ 且不经过前面任何障碍的路径数} \]

\[f[i]=H[\operatorname{pc}(p_i)]-\sum_{j<i,\ p_j\subseteq p_i}f[j]\cdot H[\Delta_{ji}] \]

其中 \(\Delta_{ji}\) 是逐维 popcount 差。

容斥直觉:从 \(O\) 到 \(p_i\) 的所有路径,按"第一个经过的障碍"分类。第一个障碍为 \(p_j\) 的路径数 = \(f[j]\cdot P(p_j\to p_i)\)(\(p_j\) 之前不经过障碍,\(p_j\) 之后随便走)。减去所有这类,剩下的就是不经过任何障碍的路径。

答案 = \(f[\text{终点}]\)。


七、完整代码

#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
#define CCF
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
const int MAXB=61;
int C[MAXB][MAXB];
int H[MAXB][MAXB][MAXB];
struct Node
{
    int x,y,z;
    int px,py,pz;
    bool operator<(const Node &o)const
    {
        if(x!=o.x)return x<o.x;
        if(y!=o.y)return y<o.y;
        return z<o.z;
    }
};
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
#ifndef CCF
    freopen(".in","r",stdin);
    freopen(".out","w",stdout);
#endif
    C[0][0]=1;
    for(int i=1;i<MAXB;i++)
    {
        C[i][0]=1;
        for(int j=1;j<=i;j++)
            C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
    }
    H[0][0][0]=1;
    for(int k1=0;k1<MAXB;k1++)
        for(int k2=0;k2<MAXB;k2++)
            for(int k3=0;k3<MAXB;k3++)
            {
                if(k1==0&&k2==0&&k3==0)continue;
                int sum=0;
                for(int j=1;j<=k1;j++)
                    sum=(sum+C[k1][j]*H[k1-j][k2][k3])%mod;
                for(int j=1;j<=k2;j++)
                    sum=(sum+C[k2][j]*H[k1][k2-j][k3])%mod;
                for(int j=1;j<=k3;j++)
                    sum=(sum+C[k3][j]*H[k1][k2][k3-j])%mod;
                H[k1][k2][k3]=sum;
            }
    int n,m,r;
    cin>>n>>m>>r;
    int o;
    cin>>o;
    vector<Node> vec;
    for(int i=0;i<o;i++)
    {
        int x,y,z;
        cin>>x>>y>>z;
        if((x&n)==x&&(y&m)==y&&(z&r)==z)
            vec.push_back({x,y,z,__builtin_popcountll(x),__builtin_popcountll(y),__builtin_popcountll(z)});
    }
    vec.push_back({n,m,r,__builtin_popcountll(n),__builtin_popcountll(m),__builtin_popcountll(r)});
    sort(vec.be,vec.en);
    int sz=vec.size();
    vector<int> f(sz,0);
    for(int i=0;i<sz;i++)
    {
        Node &u=vec[i];
        f[i]=H[u.px][u.py][u.pz];
        for(int j=0;j<i;j++)
        {
            Node &v=vec[j];
            if((v.x&~u.x)==0&&(v.y&~u.y)==0&&(v.z&~u.z)==0)
            {
                int d1=u.px-v.px,d2=u.py-v.py,d3=u.pz-v.pz;
                f[i]=(f[i]-f[j]*H[d1][d2][d3]%mod+mod)%mod;
            }
        }
    }
    cout<<f[sz-1]%mod<<"\n";
    return 0;
}

[清华集训 2012] 串珠子

一、 题意

\(n\) 个互不相同的点。对每对 \((i,j)\):

  • 不连边(1 种选择)
  • 或从 \(c_{i,j}\) 根不同颜色绳子中选一根连边(\(c_{i,j}\) 种选择)

所以每条边有 \(c_{i,j}+1\) 种选择。求整张图连通的方案数,模 \(10^9+7\)。

\(n\le16\)。


二、核心思路:连通 = 任意 − 不连通

2.1 先算"任意连边"

定义:

\[g[S]=\text{只考虑 }S\text{ 内部所有边,每条边随便选(连/不连/选色)的方案数} \]

边与边独立,所以:

\[g[S]=\prod_{\substack{i<j\\ i,j\in S}}(c_{i,j}+1) \]

注意:\(g[S]\) 允许图不连通,它统计的是 \(S\) 内部所有边的任意选择。

2.2 再算"连通"

定义:

\[f[S]=\text{点集 }S\text{ 连通的方案数} \]

答案就是 \(f[\text{全集}]\)。


三、容斥递推(最关键的一步)

3.1 固定一个点

\(S\) 里任取一个点 \(r\)(代码里取最低位 r=S&-S)。任意一个 \(S\) 内部的图,\(r\) 一定属于某个连通块 \(T\):

  • \(r\in T\subseteq S\)
  • 若图不连通,则 \(T\) 是 \(S\) 的真子集

3.2 按 \(r\) 的连通块分类

"\(r\) 所在连通块恰为 \(T\)"的图数 = :

  • \(T\) 内部连通:\(f[T]\)
  • \(S\setminus T\) 内部任意:\(g[S\setminus T]\)
  • \(T\) 与 \(S\setminus T\) 之间的边必须全断:每条跨边只有"不连"1 种,乘 1,不用额外因子

所以:

\[f[T]\cdot g[S\setminus T] \]

3.3 递推式

\(g[S]\) 是全部方案(含不连通),要得到连通方案,减去所有"\(r\) 所在连通块是 \(S\) 真子集"的情况:

\[\boxed{ f[S]=g[S]-\sum_{\substack{T\subsetneq S\\ r\in T}}f[T]\cdot g[S\setminus T] } \]

正确性:任意一个不连通的图,\(r\) 所在的连通块 \(T\) 是唯一确定的真子集,所以它恰好被减去一次,不重不漏。


五、代码细节

5.1 位运算约定

  • 状态 \(S\) 用 0-indexed 位表示:第 \(i\) 位(\(i=0..n-1\))对应点 \(i+1\)。
  • S&-S 取最低位,__builtin_ctz(lb) 得到最低位的下标 \(v\)(0-indexed)。
  • full=1<<n,full-1 是全 1 状态(全集)。

5.2 预处理 \(g[S]\)

直接对每个 \(S\) 枚举所有内部边是 \(O(n^2 2^n)\),也能过。但更快的做法是递推:

取 \(S\) 的最低位 \(v\),令 prev=S^lb(去掉 \(v\))。新增点 \(v\) 与 prev 中每个点 \(i\) 的边独立选择:

\[g[S]=g[prev]\cdot\prod_{i\in prev}(c_{v,i}+1) \]

g[0]=1;
for(int S=1;S<full;S++)
{
    int lb=S&-S;
    int v=__builtin_ctz(lb);
    int prev=S^lb;
    int prod=1;
    for(int i=1;i<=n;i++)
        if(prev>>(i-1)&1)
            prod=prod*((c[v+1][i]+1)%mod)%mod;
    g[S]=g[prev]*prod%mod;
}

注意 \(v\) 是 0-indexed,访问矩阵用 c[v+1][i](1-indexed)。

复杂度 \(O(n2^n)\)。

5.3 容斥求 \(f[S]\)

for(int S=1;S<full;S++)
{
    f[S]=g[S];
    int r=S&-S;
    for(int T=(S-1)&S;T;T=(T-1)&S)   // 枚举 S 的所有非空真子集
    {
        if(T&r)                        // 只取含 r 的
            f[S]=(f[S]-f[T]*g[S^T]%mod+mod)%mod;
    }
}

枚举子集的技巧:for(T=(S-1)&S; T; T=(T-1)&S) 按降序枚举 \(S\) 的所有非空真子集。

if(T&r) 只保留含 \(r\) 的子集,避免重复。

复杂度 \(\sum_S 2^{|S|}=3^n\)。\(3^{16}\approx4.3\times10^7\),可过。

5.4 模数

题目是 \(10^9+7\),不是模板默认的 \(998244353\)。


六、完整代码

#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
#define CCF
using namespace std;
const int N=(1<<16)+5;
const int mod=1000000007;
const int inf=1e18;
int c[20][20];
int g[N],f[N];
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
#ifndef CCF
    freopen(".in","r",stdin);
    freopen(".out","w",stdout);
#endif
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            cin>>c[i][j];
    int full=1<<n;
    g[0]=1;
    for(int S=1;S<full;S++)
    {
        int lb=S&-S;
        int v=__builtin_ctz(lb);
        int prev=S^lb;
        int prod=1;
        for(int i=1;i<=n;i++)
            if(prev>>(i-1)&1)
                prod=prod*((c[v+1][i]+1)%mod)%mod;
        g[S]=g[prev]*prod%mod;
    }
    for(int S=1;S<full;S++)
    {
        f[S]=g[S];
        int r=S&-S;
        for(int T=(S-1)&S;T;T=(T-1)&S)
        {
            if(T&r)
                f[S]=(f[S]-f[T]*g[S^T]%mod+mod)%mod;
        }
    }
    cout<<f[full-1]%mod<<"\n";
    return 0;
}

七、一句话总结

\(g[S]\) 用递推 \(g[S]=g[S\setminus\{v\}]\prod(c_{v,i}+1)\) 统计 \(S\) 内任意连边方案;\(f[S]=g[S]-\sum_{r\in T\subsetneq S}f[T]g[S\setminus T]\) 按最低点 \(r\) 的连通块容斥;答案 \(f[\text{全集}]\)。复杂度 \(O(3^n)\)。

posted @ 2026-10-19 21:10  Aries2011  阅读(14)  评论(0)    收藏  举报