局部递推
局部递推
Problem A. 部分错排问题
对满足以下条件的 排列 \(A\) 计数:
- 长度为 $n+m $;
- 对于任意在 $[1,m] $ 中的整数 \(i\),\(A_i\ne i\)。
也就是排列中有 \(n\) 个自由元素,\(m\) 个限制元素。
我们称这样的排列 \(A\) 的个数为 部分错排数,记为 \(B_{n,m}\)。
Version I
给出一组 \(n,m\) 求 \(B_{n,m}\),其中 \(0\leq n,m\leq 10^6\)。
根据容斥原理,答案即为
Version II
对所有 \(n,m\) 求 \(B_{n,m}\),其中 \(0\leq n,m \leq 2000\)。
首先有 \(B_{n,0}=n!,B_{0,m}=D_m\)。
对于其他情况,考虑 \(A_{n+m}\) 填什么。记 \(A_{n+m}=k\),分情况讨论:
- \(1\leq k\leq m\)。此时 \(A_k\ne k\) 这个限制不再生效,那么 \(A_k\) 填什么数字不再有限制。则在前 \(n+m-1\) 个位置中,还有 \(m-1\) 个位置有限制,\(n\) 个位置没限制,贡献为 \(m\times B_{n,m-1}\);
- \(m<k\leq m+n\)。这对于所有的 $ A_i\ne i$ 的限制没有任何影响。则在前 \(n+m-1\) 个位置中,还有 \(m\) 个位置有限制,\(n-1\) 个位置没限制,贡献为 \(n\times B_{n-1,m}\)。
于是得出递推式 1:
还有一种考虑方式:
- \(k=n+m\)。显然贡献为 \(B_{n-1,m}\);
- \(k\neq n+m\)。此时增加了一个 \(A_{n+m}\ne n+m\) 的限制,那么 \(n+m\) 变得有限制,贡献为 \(B_{n-1,m+1}\);
得到递推式 2:\(B_{n,m}=B_{n-1,m}+B_{n-1,m+1}\)。
Version III
\(n\) 固定,对所有的 $ m$ 求 \(B_{n,m}\),其中 \(1\leq n \leq 10^7,1\leq m \leq 10^5\)。
首先有 \(B_{n,0}=n!\),\(B_{n,1}=n\times n!\)。
当 \(m\geq 2\) 时,考虑 \(A_m\) 填什么。记 \(A_m=k\),分情况讨论:
- \(1\leq k \leq m\)。类似于 \(D_n\) 的递推式 I,贡献为 \((m-1)(B_{n,m-1}+B_{n,m-2})\);
- \(m<k\leq m+n\)。此时 \(A_m\) 这个位置没有限制,那么还剩下 \(m-1\) 个位置有限制,贡献为 \(n\times B_{n,m-1}\)。
于是得出递推式 3:
Version IV
\(Q\) 组询问,每次询问一个 \(B_{n,m}\) 的值,\(1\leq n,m,Q\leq 10^5\)。
假如我们已经知道 \(B_{n,m},B_{n,m+1}\) 的值。
由递推式 3,可以直接求出 \(B_{n,m-1},B_{n,m+2}\):
由递推式 1,
由递推式 2,
联立递推式 1,2,
我们可以 \(O(1)\) 求出 \((n,m),(n,m+1)\) 相邻 \(6\) 个位置的值,那么我们就可以 \(O(1)\) 地移动 \(n\) 或 \(m\)。
将所有询问离线后莫队求解。复杂度 \(O(N\sqrt{Q})\),其中 \(N=\max(n_i,m_i)\)。
这种 trick 也叫作 局部递推,即列出若干个线性无关方程组实现参数的微调。
Problem B. P8367 [LNOI2022] 盒
首先考虑对于固定的 \(b\) 怎么求答案。
我们对 \(a,b\) 做前缀和得到 \(A,B\),将贡献拆到每一个 \(i\) 上,答案即为 \(\sum_{i=1}^{n-1} w_i|A_i-B_i|\)。
接下来考虑如何计数。
分两种情况:\(A_i\geq B_i\) 和 \(A_i<B_i\)。两种情况是对称的,我们先考虑第一种。
对于每一个位置 \(i\),我们需要知道 \(A_i\leq B_i\) 的方案数,权且设为 \(f\);还需要知道所有这样 \(B_i\) 的和,权且即为 \(g\)。
那么 \(i\) 位置的贡献就为 \(w_i(f\times a_i-g)\)。
记 \(\sum A_i = m\)。我们把 \(B_i\) 刻画为从 \((0,0)\) 走到 \((n,m)\) 的折线,第一次触碰直线 \(x=i\) 时的 \(y\) 坐标即为 \(B_i\)。
枚举 \(B_i=j\),对应的折线为 \((0,0)\rightarrow (i-1,j)\rightarrow (i,j)\rightarrow (n-1,m)\)。
那么:
\(g\) 里面乘的 \(j\) 看上去有些难受,所以我们展开二项式系数消掉它:
代入原式:
发现等式右边和 \(f\) 形式是一样的,那么设:
则 \(g_{n,m,i,k}=i\times f_{n+1,m-1,i+1,k-1}\)。
接下来只需要处理 \(f\) 即可。
可以发现,\(i\) 增大的过程中,\(k\) 单调不降。如果可以快速实现 \(i,k\) 增大 \(1\),就可以在 \(O(n+m)\) 内解决问题。
显然有:
当 \(i\) 增大时,回到 \(f\) 的组合意义: \((0,0)\rightarrow (i-1,j)\rightarrow (i,j)\rightarrow (n-1,m)\) 的折线数量,其中 \(0\leq j\leq k\)。
那么此时增加了 \(B_{i+1}\leq k\) 这一条件,且原来合法的折线现在依然合法。
所以我们只要减去符合原来条件但不符合新条件的折线数即可。
可以发现,这样的折线必定会从 \((i,k)\) 走到 \((i,k+1)\),再任意地走到 \((n-1,m)\)。
于是得到:
下面来解决 \(A_i<B_i\) 的情况。
类似地,设:
同理可得:
\(k\) 增大时,显然有:
\(i\) 增大时,相当于减少了 \(B_i\geq k\) 这一条件。所以只要减去原来不合法但现在合法的折线数。
可以发现,这样的折线一定从 \((i,k-1)\) 走到 \((i,k)\) 再走到 \((n-1,m)\),则有:
所以,我们只需要当 \(i,k\) 移动时,维护出两个 \(f\) 和两个 \(h\) 的变化,就可以在 \(O(n+m)\) 内解决一组数据。
struct Value1{
int vn,vm,vi,vk;
ll res;
inline void MoveK(){(res+=C(vi+vk,vi-1)*C(vn+vm-vi-vk-2,vn-vi-1))%=mod;vk++;}
inline void MoveI(){(res+=mod-C(vi+vk,vi)*C(vn+vm-vi-vk-2,vn-vi-1)%mod)%=mod;vi++;}
}F1,F2;
struct Value2{
int vn,vm,vi,vk;
ll res;
inline void MoveK(){(res+=mod-C(vi+vk-1,vi-1)*C(vn+vm-vi-vk-1,vn-vi-1)%mod)%=mod;vk++;}
inline void MoveI(){(res+=C(vi+vk-1,vi)*C(vn+vm-vi-vk-1,vn-vi-1))%=mod;vi++;}
}G1,G2;
void Solve(){
read(n);
for(int i=1;i<=n;i++){
read(a[i]);
a[i]+=a[i-1];
}
m=a[n];
for(int i=1;i<n;i++) read(w[i]);
F1=Value1{n,m,1,0,C(n+m-2,n-2)};
F2=Value1{n+1,m-1,1,-1,0};
ll ans=0;
for(int i=1;i<n;i++){
while(F1.vi<i) F1.MoveI();
while(F1.vk<a[i]) F1.MoveK();
while(F2.vi<i+1) F2.MoveI();
while(F2.vk<a[i]-1) F2.MoveK();
(ans+=w[i]*(F1.res*a[i]%mod+mod-i*F2.res%mod))%=mod;
}
G1=Value2{n,m,1,0,0};
G2=Value2{n+1,m-1,1,0,0};
for(int i=0;i<=m;i++) (G1.res+=C(n+m-i-2,n-2))%=mod;
for(int i=0;i<m;i++) (G2.res+=C(n+m-i-2,n-1))%=mod;
for(int i=1;i<n;i++){
while(G1.vk<a[i]+1) G1.MoveK();
while(G1.vi<i) G1.MoveI();
while(G2.vk<a[i]) G2.MoveK();
while(G2.vi<i+1) G2.MoveI();
(ans+=w[i]*(i*G2.res%mod+mod-a[i]*G1.res%mod))%=mod;
}
printf("%lld\n",ans);
}
signed main(){
int T; read(T);
Init();
while(T--){
Solve();
}
return 0;
}

浙公网安备 33010602011771号