DP也要搞動態,動態DP與全局平衡二叉樹詳(lue)解

該來的還是來了,我是真的不想寫這個狗博客,是真的煩。

參考文獻

很詳細:https://www.luogu.com.cn/blog/ShadowassIIXVIIIIV/solution-p4643
ZFY:抄襲文獻

術語

u.lightsonu.lightsonuu的輕兒子。u.heavysonu.heavysonuu的重兒子。

例題

例題
(#`O′)喂,普通都沒講,爲什麼直接加強了!!!

思路

這道題目我們一看特別容易矇蔽,修改,但是當我們想到鏈分治後,一大坨的奇思妙想就來了。

看看普通的DP方程式。

fu,0f_{u,0}爲不在獨立集中的最大值,那麼fu,1f_{u,1}就是在的最大值,aua_{u}uu點權值。

那麼就有DP方程式:

fu,0=vu.sonmax(fv,1,dpv,0)f_{u,0}=\sum\limits_{v∈u.son}max(f_{v,1},dp_{v,0})
fu,1=vu.sonfv,0+auf_{u,1}=\sum\limits_{v∈u.son}f_{v,0}+a_{u}

而這個搜索的順序是無關緊要的,所以我們可以優先重鏈,然後輕鏈。

也就是先DFS重鏈並且記錄所有輕兒子,然後再按順序DFS輕兒子。

所以我們可以把樹抽象成這樣:

在這裏插入圖片描述
然後我們對於每個點,我們記錄一個lflf值:
lfu,0=vu.lightsonmax(fv,0,fv,1)lf_{u,0}=\sum\limits_{v∈u.lightson}max(f_{v,0},f_{v,1})
lfu,1=vu.lightsonfv,0lf_{u,1}=\sum\limits_{v∈u.lightson}f_{v,0}

然後我們就可以單獨對於重鏈跑DP了,成功把樹上轉換成鏈上,DP也就是:

fu,0=lfu,0+max(fu.heavyson,0,fu.heavyson,1)f_{u,0}=lf_{u,0}+max(f_{u.heavyson,0},f_{u.heavyson,1})
fu,1=lfu,1+fu.heavyson,0+auf_{u,1}=lf_{u,1}+f_{u.heavyson,0}+a_{u}

然後我們就可以用鏈分治(樹鏈剖分)了,然後用線段樹。

等會等會,怎麼用線段樹?

考慮一些DPDP的本質,有一些DP其實就是一個數組乘上一個矩陣然後變成了DP後的數組,只不過有時候有優化罷了。

這個也是一樣,不過矩陣乘法的形式跟以前不一樣,是:
ci,j=maxk(Ai,k+Bk,j)c_{i,j}=max_{k}(A_{i,k}+B_{k,j})

可以證明,這個矩陣乘法的形式依舊滿足結合律,這就夠了,單位矩陣爲:(0,0)(0,0)(n,n)(n,n)全是00,其他全是inf-inf

那麼我們把矩陣寫出來!!!(v=u.heavyson)(v=u.heavyson)

[fv,0fv,1]×[lfu,0lfu,1+aulfu,0inf]\begin{bmatrix} f_{v,0} & f_{v,1} \end{bmatrix}\times\begin{bmatrix} lf_{u,0}&lf_{u,1}+a_{u} \\ lf_{u,0} &-inf \end{bmatrix}

爲了方便,下文的lfu,1+=aulf_{u,1}+=a_{u}

用樹鏈剖分維護的話,現在就是O(nlog2n)O(n\log^2{n})(線段樹維護後面的那個矩陣)。

全局平衡二叉樹


以下摘自參考文獻。

可以去翻07年的論文”QTREE 解法的一些研究",(“全局平衡二叉樹"這個中二的名字是論文裏說的)

衆所周知把剛纔的樹剖換成lct就可以做到一個log了,但是我們發現lct實在是常數太!大!了!絕對是跑不過實現的優秀的一點的樹剖的

但是我們對於lct的複雜度證明卻很感興趣,爲啥同樣是操作了lognlogn個數據結構,把線段樹換成常數更大的splay複雜度反而少了一個loglog呢?(剛纔這句話嚴格來講是病句,常數和複雜度沒有任何關聯)

具體證明需要用到勢能分析,但是感性理解一下就是如果我們把lct上的虛邊也看成splay的邊的話,我們發現整棵lct變成了一棵大splay,只是有些點度數不是2了

但是這些點度不是2的點並未破壞splay的勢能分析換句話說勢能分析對整顆大splay仍然生效,所以你的loglog次splay在整個大splay上只是一次splay而已

複雜度自然是均攤O(logn)O(\log{n})

但是,我們發現這是顆靜態樹,使用splay實在是大(常)材(數)小(過)用(大)了

於是我們考慮將lct強行靜態化,換句話說,建一個像splay一樣的全局平衡的樹

觀察到線段樹只是局部平衡的,在碰到專業卡鏈剖的數據–鏈式堆(根號n個長度爲根號n的鏈連成完全二叉樹的形狀)的時候會導致算上虛邊之後的整顆樹左傾或者右傾

此時我們發現如果在建線段樹的時候做點手腳,我們把線段樹換成二叉查找樹bst,並且這個bst不是嚴格平衡的話,我們可以做到更加優秀的複雜度,使得算上虛邊之後的樹樹高達到O(logn)O(log{n})級別

我們還是在樹上dfs,但是對於重鏈建bst的時候我們並不建一個完美的bst,而是將每一個節點附上一個權值,權值爲它所有輕兒子的siz之和+1,然後我們每次找這個鏈的帶權重心,把他作爲這一級的父親,然後遞歸兩邊進行建bst

當然我們發現最壞情況下我們可以建出一個嚴重左傾或者右傾的bst

但是,我們考慮算上虛邊的整顆樹我們會發現一個神奇的性質,無論是經過一條重的二叉樹邊還是虛邊,所在子樹的siz至少翻一倍,而這個性質在原來的線段樹上是沒有的

所以這個大bst的高度是O(logn)O(\log{n})

當然,這個bst既不能旋轉也不能splay,所以維護區間信息會比較吃力,但是,我們爲什麼要維護區間信息呢?這是動態dp啊,我們只需要維護這整個重鏈的矩陣連乘積就行了……,所以維護整個重鏈的連乘積還是可以做到的

另外這個東西看起來聽玄學其實比樹剖還好寫……


這裏對於對於全局平衡二叉樹的時間複雜度進行證明。

在全局平衡二叉樹上,每跳一個虛邊,那麼在原樹上對應的就是子數大小乘2(最壞情況),同時每條一條實邊,也就是重邊(全局平衡二叉樹上),由於我們每次的根都是帶權重心,左右兒子相對平衡,所以也是子數大小乘2(最壞情況),那麼就是loglog層了。

關於構造方法其實shadowice1984講的特別好,Orz,奆佬還是奆佬,我還是蒟蒻QAQ。

代碼與細節

上代碼!

#include<cstdio>
#include<cstring>
#define  inf  999999999
#define  N  1100000
#define  M  2100000
using  namespace  std;
template  <class  T>
inline  void  getz(register  T  &x)
{
	x=0;
	register  T  f=1;register  char  c=getchar();
	while(c<'0'  ||  c>'9')c=='-'?f=-1:0,c=getchar();
	while(c<='9'  &&  c>='0')x=(x<<3)+(x<<1)+(c^48),c=getchar();
	x*=f;
}
template  <class  T>
inline  T  mymin(register  T  x,register  T  y){return  x<y?x:y;}//最小值 
template  <class  T>
inline  T  mymax(register  T  x,register  T  y){return  x>y?x:y;}//最大值 
int  zhi[N],n,m;//點值 
//---------------------------------------------------------
struct  bian
{
	int  y,next;
}a[M];int  last[N],len;
inline  void  ins(int  x,int  y){len++;a[len].y=y;a[len].next=last[x];last[x]=len;}
//邊目錄 
int  h[N],size[N],lsiz[N];
inline  void  dfs(int  x,int  fa)
{
	size[x]=1;
	for(int  k=last[x];k;k=a[k].next)
	{
		int  y=a[k].y;
		if(y!=fa)
		{
			dfs(y,x);
			size[x]+=size[y];if(size[y]>size[h[x]])h[x]=y;//改變重兒子 
		}
	}
	lsiz[x]=size[x]-size[h[x]];//表示的是在BST上他的權值 
}
//建立重心以及size 
struct  node
{
	int  mp[2][2];
	inline  node(){mp[0][0]=mp[0][1]=mp[1][0]=mp[1][1]=-inf;}
	inline  int*  operator [](int  x){return  mp[x];}
};
inline  node  operator*(node  x,node  y)//重定義乘法 
{
	node  ans;
	ans[0][0]=mymax(x[0][0]+y[0][0],x[0][1]+y[1][0]);
	ans[0][1]=mymax(x[0][0]+y[0][1],x[0][1]+y[1][1]);
	ans[1][0]=mymax(x[1][0]+y[0][0],x[1][1]+y[1][0]);
	ans[1][1]=mymax(x[1][0]+y[0][1],x[1][1]+y[1][1]);
	return  ans;
}
inline  void  mer(node  &x){x[0][0]=x[1][1]=0;x[0][1]=x[1][0]=-inf;}//等於單位矩陣 
inline  int  gtw(node  x){return  mymax(mymax(x[0][0],x[0][1]),mymax(x[1][0],x[1][1]));}
//矩陣 
struct  Tree
{
	node  mul[N]/*表示的是每個點在BST上管理的權值*/,w[N]/*每個點本身的權值*/;
	int  son[N][2],fa[N],root;
	//一下是基礎函數 
	void  pre_do()//初始化
	{
		mer(mul[0]);mer(w[0]);//初始化
		for(int  i=1;i<=n;i++)w[i][0][0]=w[i][1][0]=0,w[i][0][1]=zhi[i];
	}
	void  ud(register  int  x){mul[x]=mul[son[x][0]]*w[x]*mul[son[x][1]];}//表示的是維護自己管理的值
	void  giv(register  int  x,register  int  v){w[x][1][0]=w[x][0][0]+=gtw(mul[v]);w[x][0][1]+=mymax(mul[v][0][0],mul[v][1][0]);fa[v]=x;}//表示v是x的輕兒子,讓v認x父親 
	bool  pd(register  int  f,register  int  x){return  !(son[f][0]!=x  &&  son[f][1]!=x);}//判斷x是不是f的重兒子 
	//以下是建樹 
	int  sta[N],top;//表示的是重鏈區間 
	bool  v[N];//是否被訪問 
	int  bt(int  l,int  r)//返回根 
	{
		if(l>r)return  0;
		int  sum=0;
		for(register  int  i=l;i<=r;i++)sum+=lsiz[sta[i]];
		int  now=lsiz[sta[l]];
		for(register  int  i=l;i<=r;i++,now+=lsiz[sta[i]])
		{
			if(now*2>=sum)
			{
				int  rs=bt(l,i-1),ls=bt(i+1,r);fa[ls]=fa[rs]=sta[i];
				son[sta[i]][0]=ls;son[sta[i]][1]=rs;ud(sta[i]);
				return  sta[i];
			}
		}
	}
	int  sbuild(int  x)
	{
		for(register  int  i=x;i;i=h[i])v[i]=1;
		for(register  int  i=x;i;i=h[i])
		{
			for(register  int  k=last[i];k;k=a[k].next)
			{
				int  y=a[k].y;
				if(!v[y])giv(i,sbuild(y));
			}
		}
		top=0;for(register  int  i=x;i;i=h[i])sta[++top]=i;
		return  bt(1,top);
	}
	void  modify(int  x,int  W)//表示修改權值 
	{
		w[x][0][1]+=W-zhi[x];zhi[x]=W;//表示修改權值
		for(register  int  i=x;i;i=fa[i])
		{
			if(!pd(fa[i],i)  &&  fa[i])
			{
				w[fa[i]][0][0]-=gtw(mul[i]);w[fa[i]][1][0]=w[fa[i]][0][0];w[fa[i]][0][1]-=mymax(mul[i][0][0],mul[i][1][0]);
				ud(i);
				w[fa[i]][0][0]+=gtw(mul[i]);w[fa[i]][1][0]=w[fa[i]][0][0];w[fa[i]][0][1]+=mymax(mul[i][0][0],mul[i][1][0]);
			}
			else  ud(i);
		}
	}
}bst;
//全局平衡二叉樹 
int  main()
{
	getz(n);getz(m);
	for(register  int  i=1;i<=n;i++)getz(zhi[i]);
	for(register  int  i=1;i<n;i++)
	{
		int  x,y;getz(x);getz(y);
		ins(x,y);ins(y,x);
	}
	dfs(1,0);
	bst.pre_do();
	bst.root=bst.sbuild(1);//初始化 
	register  int  lastans=0;
	for(register  int  i=1;i<=m;i++)
	{
		int  x,W;getz(x);getz(W);x^=lastans;
		bst.modify(x,W);
		printf("%d\n",lastans=gtw(bst.mul[bst.root]));
	}
	return  0;
}

問題1

在處理一條重鏈的時候,整個鏈的連乘我們知道,但是怎麼知道單單對於這個重鏈以及他們輕兒子的答案?

這個問題很簡單,就是直接提取出來,可以認爲乘了一個矩陣:[0,0][0,0],也就是這一行。

inline  int  gtw(node  x){return  mymax(mymax(x[0][0],x[0][1]),mymax(x[1][0],x[1][1]));}

問題2

在處理lflf的時候,如何從兒子節點的矩陣中找到一點有用的信息?

例如知道一個節點的其中一個輕兒子在BST中的矩陣,但是如何從矩陣中翻出一點有用的東西???

也可以認爲就是乘了一個[0,0][0,0]矩陣然後的得到了輕兒子的f0,1f_{0,1}

發佈了75 篇原創文章 · 獲贊 17 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章