双线反射容斥学习笔记
询问每次向上或向右走一步,从 \((0,0)\) 走到 \((n,m)\) 的方案数,要求不能经过 \(y=x+a\) 和 \(y = x +b\),其中 \(a>0>b\)。
考虑容斥,用总方案减去不合法方案。总方案为 \(\binom{n+m}{n}\)。将不合法的情况分成两种,第一次经过的是直线 \(A\) 和第一次经过的是直线 \(B\)。
考虑像单线反射那样,把 \((n,m)\) 对称到直线 \(A\) 另一侧的 \((m-a,n+a)\)。注意图里的点横纵坐标都是反的。那么到达 \((n',m')\) 的方案数就是经过了 \(A\) 到达 \((n,m)\) 的方案数了。

但是这样会有问题,就是我们没有约束经过 \(A\) 之前到过哪里,如果先经过了 \(B\) 再经过的 \(A\),也会被计入到这个第一次经过 \(A\) 的数中,所以我们又要减去先过 \(B\) 再过 \(A\) 的方案数,那就把 \((n',m')\) 对称到 \(B\) 的另一侧 \((n'’,m'')\) 上。可知到达 \((n'',m'')\) 的方案数就是过了 \(B\) 再过 \(A\) 的方案数,同理又会多减去 \(\dots ABA \dots\) 这样的方案数,继续对称直到坐标离开第一象限。复杂度是 \(O(\frac{n+m}{|a-b|})\)
P3266 [JLOI2015] 骗我呢
模板例题,首先发现一行只能空缺一个数,设 \(dp_{i,j}\) 表示第 \(i\) 行 \(j\) 空缺的方案数,则 \(dp_{i,j} = \sum_{k=0}^{j+1}dp_{i-1,k}\)。优化一下发现 \(dp_{i,j}=dp_{i,j-1}+dp_{i-1,j+1}\),最终答案为 \(dp_{n+1,m}\)。
发现这就是在一个坐标系中做格路计数,每一部转移都可以抽象成向上走一步或向右走一步,最终从 \((0,0)\) 到达 \((n+m,n)\) 的方案数,不能经过 \(y=x+2\) 和 \(y=x-m-1\)。直接做即可。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define _int __int128
#define ull unsigned long long
#define pii pair<int,int>
#define fst first
#define scd second
#define pq priority_queue
#define mkp make_pair
#define popcount(x) __builtin_popcount(x)
#define endl '\n'
int n,m;
const int N = 1e6,mod=1e9+7;
int fac[3*N+10],inv[3*N+10];
int qpow(int a,int b){
int res=1;
while(b){
if(b&1)res=(res*a)%mod;
a=(a*a)%mod;
b>>=1;
}
return res;
}
int C(int n,int m){
if(n<m)return 0;
return fac[n]%mod*inv[m]%mod*inv[n-m]%mod;
}
void add(int &a,int b){
a+=b;
if(a>=mod)a-=mod;
}
void sub(int &a,int b){
a=(a-b+mod);
if(a>=mod)a-=mod;
}
void flp(int &x,int &y,int b){
swap(x,y);
x-=b,y+=b;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
fac[0]=inv[0]=1;
for(int i=1;i<=3*N;i++){
fac[i]=(fac[i-1]*i)%mod;
}
inv[3*N]=qpow(fac[3*N],mod-2);
for(int i=3*N-1;i>=0;i--){
inv[i]=(inv[i+1]*(i+1))%mod;
}
int ans=C(2*n+m,n);
int ilg=0;
int x=n+m,y=n;
while(x>=0&&y>=0){
flp(x,y,2);if(x>=0&&y>=0)add(ilg,C(x+y,x));
flp(x,y,-m-1);if(x>=0&&y>=0)sub(ilg,C(x+y,x));
}
sub(ans,ilg);
ilg=0;
x=n+m,y=n;
while(x>=0&&y>=0){
flp(x,y,-m-1);if(x>=0&&y>=0)add(ilg,C(x+y,x));
flp(x,y,2);if(x>=0&&y>=0)sub(ilg,C(x+y,x));
}
sub(ans,ilg);
cout<<ans;
return 0;
}
QOJ414 一般难度的推式子题
询问所有 \(n\) 个节点的无标号树的高度之和,高度定义为从根节点走出的最长简单路径长度。
我们要知道,一棵无标号树的结构唯一对应一种欧拉序,且第一位一定是加入一个数,最后一位一定是删除一个数,那我们就不看这两个位置,把这个括号序列中加入视作在坐标系里向右走一步,删除视作向上走一步,则要求任意时刻不能到达 \(y=x+1\)。
然后我们发现,如果把括号序列看作 \(1\) 和 \(-1\) 的话,树的高度等于序列的前缀和最大的一个位置的前缀和,即坐标 \(x-y\) 最大的位置。如果设 \(f_k\) 表示高度为 \(k\) 的树有几种生成方式,那么就是问经过 \(y=x-k\) 且不超过 \(y=x-k\) 的方案数,非常地难算,那我们就令 \(f_k\) 表示高度不超过 \(k\) 的树有几种,那就是问不碰到 \(y=x-k-1\) 有几种。
对每一个 \(k\) 都做一遍双线反射容斥,单次是 \(O(\frac{n}{k})\) 的,可以证明总复杂度是 \(O(n \log n)\) 级别的。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define _int __int128
#define ull unsigned long long
#define pii pair<int,int>
#define fst first
#define scd second
#define pq priority_queue
#define mkp make_pair
#define popcount(x) __builtin_popcount(x)
#define endl '\n'
int n,mod;
const int N = 1e5;
int fac[3*N+10],inv[3*N+10];
int qpow(int a,int b){
int res=1;
while(b){
if(b&1)res=(res*a)%mod;
a=(a*a)%mod;
b>>=1;
}
return res;
}
int C(int n,int m){
if(n<m)return 0;
return fac[n]%mod*inv[m]%mod*inv[n-m]%mod;
}
void add(int &a,int b){
a+=b;
if(a>=mod)a-=mod;
}
void sub(int &a,int b){
a=(a-b+mod);
if(a>=mod)a-=mod;
}
void flp(int &x,int &y,int b){
swap(x,y);
x-=b,y+=b;
}
int f[N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>mod;
fac[0]=inv[0]=1;
for(int i=1;i<=3*N;i++){
fac[i]=(fac[i-1]*i)%mod;
}
inv[3*N]=qpow(fac[3*N],mod-2);
for(int i=3*N-1;i>=0;i--){
inv[i]=(inv[i+1]*(i+1))%mod;
}
for(int k=1;k<=n-1;k++){
f[k]=C(2*n-2,n-1);
int ilg=0,x=n-1,y=n-1;
while(x>=0&&y>=0){
flp(x,y,1);if(x>=0&&y>=0)add(ilg,C(x+y,x));
flp(x,y,-k-1);if(x>=0&&y>=0)sub(ilg,C(x+y,x));
}
sub(f[k],ilg);
ilg=0;x=n-1,y=n-1;
while(x>=0&&y>=0){
flp(x,y,-k-1);if(x>=0&&y>=0)add(ilg,C(x+y,x));
flp(x,y,1);if(x>=0&&y>=0)sub(ilg,C(x+y,x));
}
sub(f[k],ilg);
}
int ans=0;
for(int i=1;i<n;i++){
add(ans,(f[i]-f[i-1]+mod)%mod*i%mod);
}
cout<<ans;
return 0;
}

浙公网安备 33010602011771号