[ZJOI2020]传统艺能
Description:
Bob 喜欢线段树。
众所周知,ZJOI 的第二题有很多线段树。
Bob 有一棵根为 ([1,n]) 的广义线段树。Bob 需要在这个线段树上执行 (k) 次区间懒标记操作,每次操作会等概率地从 ([1,n]) 的所有 (dfrac{n(n+1)}{2}) 个子区间中随机选择一个。对于所有在该次操作中被访问到的非叶子节点,Bob 会将这个点上的标记下推;而对于所有叶子节点(即没有继续递归的节点),Bob 会给这个点打上标记。
Bob 想知道,(k) 次操作之后,有标记的节点的期望数量是多少。
【具体定义】
线段树:线段树是一棵每个节点上都记录了一个线段的二叉树。根节点记录的线段是 ([1,n])。对于每个节点,若它记录的线段是([l,r]) 且 (l eq r),取 (m = lfloor dfrac{l+r}{2} floor),则它的左右儿子节点记录的线段分别是 ([l,m]) 和 ([m+1,r]);若 (l = r),则它是叶子节点。
广义线段树:在广义的线段树中,(m) 不要求恰好等于区间的中点,但是 (m) 还是必须满足 (l leq m < r)的。不难发现在广义的线段树中,树的深度可以达到 (O(n)) 级别。
线段树的核心是懒标记,下面是一个带懒标记的广义线段树的伪代码,其中 tag
数组为懒标记:
注意,在处理叶子节点时,一旦他获得了一个标记,那么这个标记会一直存在。
你也可以这么理解题意:有一棵广义线段树,每个节点有一个 (m) 值。一开始 tag
数组均为 (0),Bob 会执行 (k) 次操作,每次操作等概率随机选择区间 ([l,r]) 并执行 MODIFY(root,1,n,l,r);
。 最后所有 Node
中满足 tag[Node]=1
的期望数量就是需要求的值。
Input:
第一行输入两个整数 (n, k)。
接下来输入一行包含 (n - 1) 个整数 (a_i):按照先序遍历的顺序,给出广义线段树上所有非叶子节点的划分位置 (m)。你也可以理解为从只有 ([1,n]) 根节点开始,每次读入一个整数后,就将当前包含这个整数的节点做一次拆分,最后获得一棵有 (2n - 1) 个节点的广义线段树。
保证给定的 (n - 1) 个整数是一个排列,不难发现通过这些信息就能唯一确定一棵 ([1,n]) 上的广义线段树。
Output:
输出一行一个整数,代表期望数量对 (p = 998244353) 取模后的结果。即,如果期望数量的最简分数表示为 (dfrac{a}{b}),你需要输出一个整数 (c) 满足 (c imes b equiv a pmod p)。
Sample Input1:
3 1
1 2
Sample Output1:
166374060
Sample Input2:
5 4
2 1 3 4
Sample Output2:
320443836
Hint:
样例输入输出 (3) 见下发文件。
样例解释 (1)
输入的线段树为 ([1, 3], [1, 1], [2, 3], [2, 2], [3, 3])。
若操作为 ([1, 1]/[2, 2]/[3, 3]/[2, 3]/[1, 3]),标记个数为 (1)。若操作为 ([1, 2]),标记个数为 (2)。故答案为 (dfrac{7}{6})。
测试点 | n | k | 其他约定 |
---|---|---|---|
1 | (leq 10) | (leq 4) | 无 |
2 | (leq 10) | (leq 100) | 无 |
3 | (leq 5) | 无 | 无 |
4 | 无 | (=1) | 无 |
5 | (=32) | 无 | 输入的线段树为完全二叉树 |
6 | (=64) | 无 | 输入的线段树为完全二叉树 |
7 | (=4096) | 无 | 输入的线段树为完全二叉树 |
8 | (leq 5000) | 无 | 每个 (m) 均在 ([l, r - 1]) 内均匀随机 |
9 | (leq 100000) | 无 | 无 |
10 | 无 | 无 | 无 |
对于 (100\%) 的数据,(1 leq n leq 200000, 1 leq k leq 10^9)。
附件下载
segment.zip (423.35KB)
题目分析:
([l,r])为线段树中一个节点储存的区间;
([L,R])为它的父亲节点;
([x,y])为选中的区间;
(p_0)表示本身有标记的概率,(p_1)表示本身或祖先有标记的概率
PS:以下概率都乘上了 (n imes (n+1))
1.与([L,R])无交集,(y<L) 或 (x>R) ,(p_0'=p_0,p_1'=p_1),概率 (L imes (L-1) + (n-R+1) imes (n-R)) ;
2.与([l,r])无交集且与([L,R])有交集,则祖先标记下传到该节点,(L<=y<l) 或 (r<x<=R),(p_0'=p_1'=p_1),概率 ((2n-r-R+1) imes (R-r) + (L+l-1) imes (l-L));
3.在祖先上打了一个标记,(x<=L)且(y>=R),(p_0'=p_0,p_1'=1),概率 (2L imes (n-R+1));
4.在该节点打了一个标记,(x<=l,r<=y<R) 或 (L<x<=l,r<=y),(p_0'=p_1'=1),概率 (2(l imes (R-r) + (l-L) imes (n-r+1)));
5.把该节点的标记下传,(l<x<=r) 或 (l<=y<r),(p_0'=p_1'=0),概率 ((r-l) imes (2n-r+l+1));
于是我们列出矩阵,矩阵快速幂跑一下就完事了。
PS:我写了很多诡异无用的卡常,仅供参考,留给读者自行思考(当然不卡常也能过,但是人要有信仰不是吗)
代码如下(马蜂很丑,不喜勿喷)——
#include<bits/stdc++.h>
#define Tp template<typename T>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define maxn 200005
#define LL long long
using namespace std;
int inv,n,K,anss;const int p=998244353;
inline int power(int x,int y){int z=1;while(y){if(y&1) z=1ll*z*x%p;y>>=1,x=1ll*x*x%p;}return z;}
struct node{
int g[5][5];
}tmp,res,ans;
inline void mul(node x,node y,node &z){
z.g[1][1]=1ll*x.g[1][1]*y.g[1][1]%p;z.g[2][2]=1ll*x.g[2][2]*y.g[2][2]%p;
z.g[1][2]=(1ll*x.g[1][1]*y.g[1][2]+1ll*x.g[1][2]*y.g[2][2])%p;
z.g[1][3]=(1ll*x.g[1][1]*y.g[1][3]+1ll*x.g[1][2]*y.g[2][3]+x.g[1][3])%p;
z.g[2][3]=(1ll*x.g[2][2]*y.g[2][3]+x.g[2][3])%p,z.g[3][3]=1;
// 每天一个卡常 trick1
// for(register int i=1;i<=2;i++) for(register int j=1;j<=3;j++) z.g[i][j]=0;z.g[3][3]=1;
// for(register int i=1;i<=2;i++) for(register int j=1;j<=3;j++) for(register int k=1;k<=3;k++)
// z.g[i][j]+=1ll*x.g[i][k]*y.g[k][j]%p,(z.g[i][j]>=p)&&(z.g[i][j]-=p);
}
inline void qpow(int m){
for(register int i=1;i<=3;i++) for(register int j=1;j<=3;j++) if(i==j) ans.g[i][j]=1;else ans.g[i][j]=0;
while(m){if(m&1) mul(ans,res,tmp),ans=tmp;m>>=1,mul(res,res,tmp),res=tmp;}
}
class FileInputOutput
{
private:
static const int S=1<<21;
#define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,S,stdin),A==B)?EOF:*A++)
#define pc(ch) (Ftop!=Fend?*Ftop++=ch:(fwrite(Fout,1,S,stdout),*(Ftop=Fout)++=ch))
char Fin[S],Fout[S],*A,*B,*Ftop,*Fend; int pt[25];
public:
FileInputOutput(void) { Ftop=Fout; Fend=Fout+S; }
Tp inline void read(T& x)
{
x=0; char ch; while (!isdigit(ch=tc()));
while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
}
Tp inline void write(T x,const char& ch)
{
if (x<0) pc('-'),x=-x; RI ptop=0; while (pt[++ptop]=x%10,x/=10);
while (ptop) pc(pt[ptop--]+48); pc(ch);
}
inline void flush(void)
{
fwrite(Fout,1,Ftop-Fout,stdout);
}
#undef tc
#undef pc
}F;
inline void get(int L,int R,int l,int r){
if(L){
res.g[2][2]=(1ll*L*(L-1)+1ll*(n-R+1)*(n-R)+1ll*(2*n-r-R+1)*(R-r)+1ll*(L+l-1)*(l-L))%p;res.g[1][1]=(1ll*L*(L-1)+1ll*(n-R+1)*(n-R)+2ll*L*(n-R+1))%p;
res.g[1][2]=(1ll*(2*n-r-R+1)*(R-r)+1ll*(L+l-1)*(l-L))%p;res.g[1][3]=2ll*(1ll*l*(R-r)+1ll*(l-L)*(n-r+1))%p;res.g[2][3]=2ll*(1ll*L*(n-R+1)+1ll*l*(R-r)+1ll*(l-L)*(n-r+1))%p;res.g[3][3]=1;
res.g[1][1]=1ll*res.g[1][1]*inv%p;res.g[1][2]=1ll*res.g[1][2]*inv%p;res.g[1][3]=1ll*res.g[1][3]*inv%p;res.g[2][3]=1ll*res.g[2][3]*inv%p;res.g[2][2]=1ll*res.g[2][2]*inv%p;
// for(register int j=1;j<=3;j++) for(register int k=1;k<=3;k++)
// if(res.g[j][k]&&j!=3||k!=3) res.g[j][k]=1ll*res.g[j][k]*inv%p;卡常 trick2
qpow(K),anss+=ans.g[1][3],(anss>=p)&&(anss-=p);
}
if(l==r) return;int mid;F.read(mid);get(l,r,l,mid),get(l,r,mid+1,r);
}
int main(){
// freopen("data.in","r",stdin);
F.read(n),F.read(K);inv=power(1ll*n*(n+1)%p,p-2);anss=2ll*inv%p;get(0,n,1,n);
F.write(anss,'
');return F.flush(),0;
}
/*
[l,r]为线段树中一个节点储存的区间;
[L,R]为它的父亲节点;
[x,y]为选中的区间;
p0表示本身有标记的概率,p1表示本身或祖先有标记的概率
以下概率都乘上了 n(n+1)
1.与[L,R]无交集,y<L or x>R ,p0'=p0,p1'=p1,概率 L(L-1)+(n-R+1)(n-R) ;
2.与[l,r]无交集且与[L,R]有交集,则祖先标记下传到该节点,L<=y<l or r<x<=R,p0'=p1'=p1,概率 (2n-r-R+1)(R-r)+(L+l-1)(l-L);
3.在祖先上打了一个标记,x<=L且y>=R,p0'=p0,p1'=1,概率 2L(n-R+1);
4.在该节点打了一个标记,x<=l,r<=y<R or L<x<=l,r<=y,p0'=p1'=1,概率 2(l(R-r)+(l-L)(n-r+1));
5.把该节点的标记下传,l<x<=r or l<=y<r,p0'=p1'=0,概率 (r-l)(2n-r+l+1);
*/