1. 程式人生 > 實用技巧 >【洛谷6630】[ZJOI2020] 傳統藝能(動態規劃+矩乘)

【洛谷6630】[ZJOI2020] 傳統藝能(動態規劃+矩乘)

點此看題面

  • 給定一棵定義在長度為\(n\)的序列上的廣義線段樹(即每次分割區間不一定取中點)。
  • \(k\)次操作,每次隨機選取一個區間進行一次\(Modify\)(詳見原題)。
  • 求最終有標記的節點數期望。
  • \(n\le2\times10^5,k\le10^9\)

畢竟是半年前考場上切掉的題目啊。。。

由於沒有考場程式碼,這篇部落格就一直坑著,直到今天下定決心重寫一遍。

應該說實力還是沒有太大退步,稍微想了一下便推出來了,接著輕鬆過樣例,然後就一遍\(A\)掉了。

套路設立狀態

真的想不到ZJOI會抄自己的題目,發現前一年的Day1T2(【洛谷5280】[ZJOI2019] 線段樹)和今年的Day1T2完全就是一個套路,甚至今年這題可能還要更簡單。

按照套路,對於一個點\(x\)(假設它對應區間為\([l,r]\),其父節點對應區間為\([fl,fr]\))我們設立三個狀態:

  • \(f_{x,i,0}\)表示\(i\)次操作後\(x\)及其祖先都沒有標記的方案數。
  • \(f_{x,i,1}\)表示\(i\)次操作後\(x\)沒有標記,但\(x\)的祖先有標記的方案數。
  • \(f_{x,i,2}\)表示\(i\)次操作後\(x\)有標記的方案數。

方便起見,我們定義\(S(x)=\frac{x\times(x+1)}2\)

\(T=S(n)\)(一次操作的總方案數),\(P=S(l-1)+S(n-r)\)(和當前區間無交的方案數),\(A=l\times (n-r+1)\)

(包含當前區間的方案數,也即當前點及其祖先中的某一個點作為終止節點的方案數)。

並類似地定義\(Pf=S(fl-1)+S(n-fr),Af=fl\times(n-fr+1)\)

然後考慮狀態之間的轉移:

  • \(U(0\rightarrow0)\):那麼這次操作要麼和當前區間無交,要麼走進了當前區間子樹(和當前區間有交但是不包含)。總結一下就是\(T-A\)
  • \(U(0\rightarrow 2)\):那麼這次操作要恰好停在當前點,也就是說要包含當前區間,但不能包含父節點對應的區間。總結一下就是\(A-Af\)
  • \(U(0\rightarrow1)\):自然就是除了\(U(0\rightarrow0)\)
    \(U(0\rightarrow2)\)外的所有情況,即\(T-U(0\rightarrow0)-U(0\rightarrow2)=Af\)
  • \(U(1\rightarrow0)\):只有走進了當前區間子樹的情況,和\(U(0\rightarrow0)\)的區別就是少了和當前區間無交的方案數\(P\)。總結一下就是\(T-P-A\)
  • \(U(1\rightarrow2)\):當前點被打上標記既可能是因為父節點標記上傳,也可能是因為直接被打上標記。無論如何首先我們必須要走到父節點,要減去與父節點區間無交的方案數\(Pf\);其次,父節點及其祖先都不能作為終止節點,要減去包含父節點區間的方案數\(Af\);再次,不能走進當前區間子樹,要減去這一部分的方案數\(U(1\rightarrow0)\)。總結一下就是\(T-Pf-Af-U(1\rightarrow0)=P+A-Pf-Af\)
  • \(U(1\rightarrow1)\)\(T-U(1\rightarrow0)-U(1\rightarrow2)=Pf+Af\)
  • \(U(2\rightarrow0)\):同\(U(1\rightarrow0)\),為\(T-P-A\)
  • \(U(2\rightarrow2)\):發現除了走進當前區間子樹的情況,已有的標記是不可能消失的。總結一下就是\(T-U(2\rightarrow0)=P+A\)
  • \(U(2\rightarrow1)\):顯然為\(0\)

\(k\)這麼大自然而然想到矩乘優化,於是這道題就做完了。

程式碼:\(O(27nlogk)\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#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 N 200000
#define X 998244353
using namespace std;
int n,k;I int QP(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}
struct M
{
	int a[3][3];I M() {memset(a,0,sizeof(a));}
	I int* operator [] (CI x) {return a[x];}
	I Con int* operator [] (CI x) Con {return a[x];}
	I M operator * (Con M& o) Con//矩陣乘法
	{
		M t;RI i,j,k;for(i=0;i^3;++i) for(j=0;j^3;++j)
			for(k=0;k^3;++k) t[i][j]=(1LL*a[i][k]*o[k][j]+t[i][j])%X;return t;
	}
}U,t;
int ans;I void Solve(CI l,CI r,CI fl=0,CI fr=n+1)
{
	#define S(x) (1LL*(x)*((x)+1)/2%X)
	#define G(x,y) (1LL*(x)*(n-(y)+1)%X)
	RI T=S(n),P=(S(l-1)+S(n-r))%X,A=G(l,r),Pf=(S(fl-1)+S(n-fr))%X,Af=G(fl,fr);
	U[0][0]=(T-A+X)%X,U[0][2]=(A-Af+X)%X,U[0][1]=Af,//構造轉移矩陣
	U[1][0]=(T-P-A+X)%X,U[1][2]=((0LL+P+A-Pf-Af)%X+X)%X,U[1][1]=(Pf+Af)%X,
	U[2][0]=(T-P-A+X)%X,U[2][2]=(P+A)%X,U[2][1]=0;
	RI y=k;(t=M())[0][0]=1;W(y) y&1&&(t=t*U,0),U=U*U,y>>=1;ans=(ans+t[0][2])%X;//矩陣快速冪,ans統計答案
	if(l==r) return;RI x;scanf("%d",&x),Solve(l,x,l,r),Solve(x+1,r,l,r);//遞迴
}
int main()
{
	return scanf("%d%d",&n,&k),Solve(1,n),printf("%d\n",1LL*ans*QP(QP(S(n),k),X-2)%X),0;//期望=總和/總方案數
}