[ZJOI2020]传统艺能 题解 [DP+矩阵+分类讨论]
[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 \neq r\),取 \(m = \lfloor \dfrac{l+r}{2} \rfloor\),则它的左右儿子节点记录的线段分别是 \([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 \times 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 \times (n+1)\)
1.与\([L,R]\)无交集,\(y<L\) 或 \(x>R\) ,\(p_0'=p_0,p_1'=p_1\),概率 \(L \times (L-1) + (n-R+1) \times (n-R)\) ;
2.与\([l,r]\)无交集且与\([L,R]\)有交集,则祖先标记下传到该节点,\(L<=y<l\) 或 \(r<x<=R\),\(p_0'=p_1'=p_1\),概率 \((2n-r-R+1) \times (R-r) + (L+l-1) \times (l-L)\);
3.在祖先上打了一个标记,\(x<=L\)且\(y>=R\),\(p_0'=p_0,p_1'=1\),概率 \(2L \times (n-R+1)\);
4.在该节点打了一个标记,\(x<=l,r<=y<R\) 或 \(L<x<=l,r<=y\),\(p_0'=p_1'=1\),概率 \(2(l \times (R-r) + (l-L) \times (n-r+1))\);
5.把该节点的标记下传,\(l<x<=r\) 或 \(l<=y<r\),\(p_0'=p_1'=0\),概率 \((r-l) \times (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,'\n');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);
*/