DTOJ 2022.12.24 测试 题解
(2023 省选模拟 Round #1)
测试成果 50+0+0
太菜了)
A 御神体
这题写了四个多小时,最后还是没写出来ww
莫队一直写挂(不过对莫队的理解加深了很多
题目链接
题目大意
计算出下面这个式子的值,$ q $ 个询问
(\(85\%\) 数据,\(\ n,m\leq 10^5,q\leq10^5\),\(15\%\)数据,\(\ n,m\leq 10^6,q\leq10^4\))
题解
首先我观察到 \(n=m\) 很好做,答案即为 \(2^n\) 可惜没部分分
接着我注意到 \([i+j \equiv 0 \pmod 2]=\frac{1}{2}(1+(-1)^{i+j})\)
于是式子就可以拆成两部分做
注意这边用到了一个组合数小公式
组合数小结论 1
因为没注意到 \(k=0\) 所以推错了(还是太菜了ww
证明:
需要一些基本的组合数公式和求和号化简方法(
大家做组合数式子的时候可以画出杨辉三角,比较直观
要小心 \(\binom{n}{m}=\binom{n-1}{m-1}+\binom{n-1}{m}\) 要满足 $m\geq 0 $
第一部分——求组合数之和
也就是求 \(\sum _{i=0}^{n} \sum _{j=0}^{m} \binom{i}{j}\)
我们发现之前做过一题类似的 洛谷 P5388 [Cnoi2019]最终幻想
就是说组合数的一行的前缀和可以递推
记一行 \(row_{n,m}=\sum_{i=0}^{m}\binom{n}{i}\)
\(row_{n,m}\rightarrow row_{n,m+1}\) 是简单的,只需要加一个 \(\binom{n}{m+1}\)
\(row_{n,m}\rightarrow row_{n+1,m}\) 也不难,画出杨辉三角可以直观地看到 \(row_{n+1,m}=2row_{n,m}-\binom{n}{m}\)
(用 \(\binom{n}{m}=\binom{n-1}{m-1}+\binom{n-1}{m}\) 这个推)
我们来同理做做一列:
他甚至可以用一个组合数表示
组合数小结论 2
于是我们用莫队就可以以 \(O(q\sqrt{m})\) 回答第一部分的询问
第二部分——求组合数一列的交错和
要求这个 \(\sum _{i=0}^{n-1}(-1)^{i} \binom{i}{m}\) 这个长得和蔼可亲的式子
我们记 \(H_{n,m}=\sum _{i=0}^{n}(-1)^{i} \binom{i}{m}\)
首先 \(H_{n,m}\rightarrow H_{n+1,m}\) 跟之前那样只是改了求和上标,只需加上 \((-1)^{n+1}\binom{n+1}{m}\)
\(H_{n,m}\rightarrow H_{n,m+1}\) 我们仿照之前推一列组合数的和的式子:
这就可以递推了
莫队
注意到可以不需要做 m--
的操作,毕竟对 \(n=m\) 的答案可以直接出来
莫队之前一直没理解,之后看了这个动图就懂了,我觉得脑子里有这样的图就很直观了
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e6+5, M = 1e5+5, P = 998244353, fL = 3e3, inv2 = (P+1)>>1;
int fc[N],fci[N],pw2[N]; // 阶乘,阶乘逆元,2的幂
int ksm(int a, int b) { int res=1; for(; b; b>>=1,a=(ll)a*a%P) if(b&1) res=(ll)a*res%P; return res; }
struct Ayaka { int n,m,ans,id; } rq[M]; // 存每次询问
int C(int n, int m)
{
if(n<0 or m<0 or n<m) return 0;
return (ll)fc[n]*fci[m]%P*fci[n-m]%P;
}
inline int pown1(int a) { return (a&1)?-1:1; } // -1 的幂
struct Kurumi
{
int sum,h,row,col;
int n,m;
int query() { return (0ll+sum+1-pown1(m)*h+P)*inv2%P; } //答案可以拆成 sum 和 h 两部分
void change_n(int d) // d=1/-1 表示加入/删去一行
{
if(d<0) sum=(sum-row+P)%P; // 从 sum 删去一行
if(d>0) row=(2ll*row-C(n,m)+P)%P; // 如上述更新一行
else row=(ll)(row+C(n-1,m))*inv2%P; // 这个就是上面的式子反过来
if(d>0) sum=(sum+row)%P; // 往 sum 加入一行
if(d>0) h=(0ll+h+pown1(n)*C(n,m)+P)%P; // 更新 h
else h=(0ll+h-pown1(n-1)*C(n-1,m)+P)%P; // 这个就是上面的式子反过来
n+=d; col=C(n+1,m+1); // 上述结论
}
void change_m()
{
row=(row+C(n,m+1))%P; col=C(n+1,m+2); sum=(sum+col)%P;
h=((0ll-h+pown1(n-1)*(C(n-1,m)+C(n-1,m+1)))%P+P)*inv2%P;
m++;
}
};
signed main()
{
fc[0]=1; for(int i=1; i<N; i++) fc[i]=(ll)fc[i-1]*i%P;
fci[N-1]=ksm(fc[N-1],P-2); for(int i=N-1; i; i--) fci[i-1]=(ll)fci[i]*i%P;
pw2[0]=1; for(int i=1; i<N; i++) pw2[i]=(pw2[i-1]<<1)%P; // 预处理
int q; scanf("%d",&q);
for(int i=1; i<=q; i++) scanf("%d%d",&rq[i].n,&rq[i].m),rq[i].id=i;
sort(rq+1,rq+q+1, [&] (const Ayaka &a, const Ayaka &b) { if(a.n/fL!=b.n/fL) return a.n<b.n; return a.m<b.m; }); // 莫队分块
for(int l=1,r; l<=q; l=r+1)
{
r=l; while(r<q and rq[r+1].m>=rq[r].m) r++; // 这边记得是 >= 不然会 tle
int tmp=rq[l].m; Kurumi cur={pw2[tmp+1]-1,0,pw2[tmp],1,tmp,tmp}; // n=m 初始情况
for(int i=l; i<=r; i++)
{
while(cur.m<rq[i].m) cur.change_m();
while(cur.n<rq[i].n) cur.change_n(1);
while(cur.n>rq[i].n) cur.change_n(-1);
rq[i].ans=cur.query();
}
}
sort(rq+1,rq+q+1, [&] (const Ayaka &a, const Ayaka &b) { return a.id<b.id; });
for(int i=1; i<=q; i++) printf("%d\n",rq[i].ans);
return 0;
}