【Note】倍增

真的不會。QAQ

簡介

倍增法。不知道算是分治還是滴批的一種東西。這裏把倍增和 \(\text{ST}\) 表放一起(\(\text{ST}\) 表本質也是倍增),原理:

  1. 倍增原理主要是二進制拆分。\(N=\sum(bit_i\times2^i)\)

  2. \(ST\) 表原理主要是區間分治/合併,即 \(2^n=2^{n-1}+2^{n-1}\)

  3. 兩者都需要滿足拆分後的區間合併運算的\(\color{green}{結合律}\)

預處理:\(\text{log2}\)

lg2[1]=0;
for (int i=2;i<=n;i++) lg2[i]=lg2[i>>1]+1;

首先是倍增。用於解決快速的\(\color{orange}{定起點}\color{pink}{定向}\)跳躍。至於如何考慮跳躍的步數,一般分爲以下兩種情況:

  1. 固定某個關於步數具有單調性的值大小,具有類似二分需要滿足的標準;
for (int i=lg2[/*maxstep*/];i>=0;i--)
{
	if (!check(f[x][i])) continue;
	x=f[x][i];
}
  1. 固定步長。
//當然,也可以用上面那段的寫法。
while (step) x=f[x][lowbit(step)],step^=lowbit(step);

然後是 \(\text{ST}\) 表。一般有以下兩種情況:

  1. 快速預處理合並區間信息,查找拆分爲至多 \(log\) 個區間信息。//其實我不知道他要是 log 有啥具體應用()可能就是個倍增吧。
//st 表模板:RMQ O(nlogn) - O(1)
//拆成兩個可以重疊的區間,從而做到 O(1)
max[l,r]=max(f[l,lg2[r-l+1]],f[r-(1<<lg2[r-l+1])+1,lg2[r-l+1]]);
//p+lg2[r-l+1]-1=r
  1. 對區間進行操作,類似線段樹分治的最後處理,將所有區間信息下傳拆分爲兩個子區間信息。

大家都見過的應用:倍增求 \(\text{LCA}\)

先將兩個點跳到相同深度。(固定深度的跳躍)

接着,將兩個點一起向上跳相同步數。

這題可以倍增做的關鍵在於,跳超過其到 \(\text{LCA}\) 的步數的話,兩個點就會相同。

於是倍增關注的標準就是\(\color{orange}{使得跳躍步數最大,並且跳後兩結點不同}\),這樣兩結點的父親就是 \(\text{LCA}\)

倍增求 \(\text{LCA}\) ,但是動態加點,但是不會 \(lct\)

另外倍增求 \(\text{LCA}\) 還可以用於動態加點的樹(新點必須是葉節點)。

例題:[ZJOI2012]災難(DAG 上的支配樹)

雜想
這題給我一種巨大trie樹的感覺()
如果在trie上做一個查詢最長且字典序最小串的話,大概可以遍歷每一個孩子,找到最小並且連接最深鏈的?

例題:[APIO2009] 會議中心(亂序添加元素 \(\text{DP}\) 最小字典序方案)

最大不重疊覆蓋線段數量,但是線段編號選擇字典序最小方案。屬實 nb

求數量就是傻逼題,可以線段樹維護 \(\text{DP}\) 或者貪心。\(\text{DP}\) 的話就是先按線段右端點排序那個

沒法通過 \(DP\) 直接標記來求字典序最小方案,因爲加邊的順序並非按邊的編號來加。

考慮:\(\text{DP}\) 轉移時,從答案相等的可轉移狀態中裏面找出字典序最小的。或者說,在線段樹中的大小比較,以 \(\text{DP}\) 值爲第一關鍵字,以字典序大小爲第二關鍵字。

我們把 \(\text{DP}\) 的狀態轉移關係當作一棵樹。那麼某個節點答案的序列就是根節點到該節點的路徑。

於是比較兩個狀態,可以轉化爲比較他們到 \(lca\) (開區間)路徑的 min 值大小。

然後就可以倍增了。跟 災難 那題一樣,倍增可以用來做動態加葉,並且在 log 時間內完成加這個點的信息處理,吊打樹剖


倍增法裸應用

注意 \(\color{orange}{定起點}\color{pink}{定向}\) 的問題。

\(\color{pink}{定向定終點,使用二分確定起點。定起點定向無\color{lightblue}{限制},使用二分確定限制。}\)

強制定向,固定終點,比如在樹上跳鏈,一般只能往祖先的方向跳。下面有個題說這個

例題:[NOIP2012 提高組] 開車旅行

可以用 \(set\) 或雙向鏈表快速處理出路徑。可以看出,在\(\color{orange}{確定起點}\)的情況下,\(\color{pink}{路徑}\)\(\color{pink}{確定}\)的。

對於 \(Q1\),只有一個詢問,給定了路程\(\color{lightblue}{限制}\) \(x\),以每個點爲起點跑一遍倍增即可。

對於 \(Q2\),有多組詢問,但是給定了\(\color{orange}{起點}\)和路程\(\color{lightblue}{限制}\),直接跑倍增即可。

這題以全路程中某個點開始,兩者都有可能是先手,所以得多設一維分別表示。(比如下面子序列問題,從任意點開始,起點都是確定的當前。)

\(f/w[i][k][0/1]([0/1])\) 表示由點 \(i\) 出發,\(A/B\) 先走,共走 \(2^k\) 步, \(A/B\) 的路程終點/路程長度。轉移:

\[f[i][0][0]=nex[i][0]\ \ \ \ f[i][0][1]=nex[i][1] \]

\[f[i][k][0]=f[f[i][k-1][0]][k-1][0\oplus[k=1]]\ \ \ \ f[i][k][1]=f[f[i][k-1][1]][k-1][1\oplus[k=1]] \]

\[w[i][0][0][0]=dis[i][0]\ \ \ \ w[i][0][1][1]=dis[i][1] \]

\[w[i][k][0][0]=w[i][k-1][0][0]+w[f[i][k-1][0]][k-1][0\oplus[k=1]][0] \]

\[w[i][k][0][1]=w[i][k-1][0][1]+w[f[i][k-1][0]][k-1][0\oplus[k=1]][1] \]

\[w[i][k][1][0]=w[i][k-1][1][0]+w[f[i][k-1][1]][k-1][1\oplus[k=1]][0] \]

\[w[i][k][1][1]=w[i][k-1][1][1]+w[f[i][k-1][1]][k-1][1\oplus[k=1]][1] \]

Code - [NOIP2012 提高組] 開車旅行
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
#define MAXN (int)(1e5+233)
int lg[MAXN];
int n;
int a[MAXN];
inline void INIT() { lg[1]=0; for (int i=2;i<=n;i++) lg[i]=lg[i>>1]+1; }
struct qwq { int x,id,pre,nex; } e[MAXN];
inline bool cmp(qwq A,qwq B) { return A.x<B.x; }
int pos[MAXN];
int ta[MAXN],tb[MAXN];
inline void dlt(int P) { int nex=e[P].nex,pre=e[P].pre; if (nex) e[nex].pre=pre; if (pre) e[pre].nex=nex; }
inline long long ABS(long long A) { return A>0?A:-A; }
int f[MAXN][19][2];
long long w[MAXN][19][2][2];
long long x0;
long long A[MAXN],B[MAXN];

inline void R(int s)
{
	int i=s,D=0; long long X=x0;
	A[s]=B[s]=0;
	for (int j=lg[n-s];j>=0;j--)
	{
		if (f[i][j][D]==0||w[i][j][D][0]+w[i][j][D][1]>X) continue;
		X-=(w[i][j][D][0]+w[i][j][D][1]);
		A[s]+=w[i][j][D][0]; B[s]+=w[i][j][D][1];
		i=f[i][j][D]; D=(j==0)?(D^1):D;
	}
}

int main()
{
	scanf("%d",&n); INIT();
	for (int i=1;i<=n;i++) scanf("%d",&a[i]),e[i]=(qwq){a[i],i,0,0};
	sort(e+1,e+n+1,cmp);
	for (int i=1;i<=n;i++) e[i].pre=i-1,e[i].nex=i+1,pos[e[i].id]=i;
	e[1].pre=e[n].nex=0;
	for (int i=1,p,pre,nex;i<n;i++)
	{
		p=pos[i];
		pre=e[p].pre; nex=e[p].nex;
		if (nex==0)
		{
			tb[i]=e[pre].id;
			ta[i]=e[e[pre].pre].id;
		}
		else if (pre==0)
		{
			tb[i]=e[nex].id;
			ta[i]=e[e[nex].nex].id;
		}
		else
		{
			if (e[p].x-e[pre].x<=e[nex].x-e[p].x) tb[i]=e[pre].id,pre=e[pre].pre;
			else tb[i]=e[nex].id,nex=e[nex].nex;
			if (nex==0) ta[i]=e[pre].id;
			else if (pre==0) ta[i]=e[nex].id;
			else ta[i]=(e[p].x-e[pre].x<=e[nex].x-e[p].x)?e[pre].id:e[nex].id;
		}
		dlt(p);
	}
	for (int i=1;i<=n;i++)
	{
		f[i][0][0]=ta[i];
		f[i][0][1]=tb[i];
		w[i][0][0][0]=ABS(a[ta[i]]-a[i]);
		w[i][0][1][1]=ABS(a[tb[i]]-a[i]);
	}
	for (int j=1;(1<<j)<=n;j++)
	{
		for (int i=1;i+(1<<j)<=n;i++)
		{
			f[i][j][0]=f[f[i][j-1][0]][j-1][j==1?1:0];
			if (f[i][j][0])
			{
				w[i][j][0][0]=w[i][j-1][0][0]+w[f[i][j-1][0]][j-1][j==1?1:0][0];
				w[i][j][0][1]=w[i][j-1][0][1]+w[f[i][j-1][0]][j-1][j==1?1:0][1];
			}
			f[i][j][1]=f[f[i][j-1][1]][j-1][j==1?0:1];
			if (f[i][j][1])
			{
				w[i][j][1][1]=w[i][j-1][1][1]+w[f[i][j-1][1]][j-1][j==1?0:1][1];
				w[i][j][1][0]=w[i][j-1][1][0]+w[f[i][j-1][1]][j-1][j==1?0:1][0];
			}
		}
	}
	scanf("%lld",&x0);
	int ansn=1; R(1);
	for (int i=2;i<=n;i++)
	{
		R(i);
		if (B[ansn]==0) { if ((B[i]==0&&a[i]>a[ansn])||B[i]!=0) ansn=i; }
		else if (B[i]==0) continue;
		else if (A[i]*B[ansn]==A[ansn]*B[i]&&a[i]>a[ansn]) ansn=i;
		else if (A[i]*B[ansn]<A[ansn]*B[i]) ansn=i;
	}
	printf("%d\n",ansn);
	int m;
	scanf("%d",&m);
	for (int i=1,s;i<=m;i++)
	{
		scanf("%d%lld",&s,&x0);
		R(s); printf("%lld %lld\n",A[s],B[s]);
	}
	return 0;
}

例題:[SCOI2015]國旗計劃

先破環成鏈,線段按左端點排序。由於線段不相包含,右端點也會遞增。

優良特性,任意定起點,方向固定(從任何一個點開始的最優路徑是固定的,離散化一下可以 \(O(n)\) ?)

\(f_{i,k}\) 表示從第 \(i\) 個線段開始,跳 \(2^k\) 次到達的線段。倍增的邊界即 編號 \(+n\)

例題:[NOIP2012 提高組] 疫情控制

有起點,有方向(貪心向根),兩類路徑\(\color{lightblue}{限制}\)都沒有。所以二分答案。

image

我自己調不出來,不講


常見題型:\(\color{orange}{在串中查找給定的子序列}\)

好像很套路。設 \(f_{i,k}\) 表示以 \(i\) 爲起點,搜出來長度爲 \(2^k\) 子序列的終點位置。

例題:2022.9.29 模擬賽 T3 - 特殊字符串

當一個字符串B是由a~z連續循環組成時,稱他爲特殊的字符串。
例如 長度M=6時 B 爲 abcdef,長度M=29時 B 爲 abcdefghijklmnopqrstuvwxyzabc
現在給你一個長度爲N的字符串S,求最少刪去多少個字符,才能找到長度爲M的特殊子串。

如果欽定了某個位置的 \(a\) 作爲開頭,就要儘可能使得終點位置最前。

於是,狀態定義和上面一樣(

當然,還要預處理每個點之後最近的每個字母的位置。

\[f_[i][k]=f[p[f[i][k-1]][nex[S[f[i][k-1]]]]][k-1] \]

最後以每個 \(a\) 開頭找一遍答案即可。

例題:「聯合省選 2021 A | B」寶石

給定一棵樹,樹上每個節點有顏色。

再給定一個無重複元素的序列。

多次詢問 \(s\)\(t\) 路徑的以給定序列前綴爲子序列的最長答案。

這同樣是找子序列的問題,有固定的起點。但是如果方向變成向下,倍增將變得難以定向。

於是我們把詢問拆成:正序 \(s \longrightarrow lca(s,t)\) 以及 倒序 \(t \longrightarrow lca(s,t)\)

前者跳完後,後者起點顏色將固定,但具體位置還要看怎麼處理。

縫合狀態:

\(f_{x,k}\) 表示以節點 \(x\) 爲序列起點,向上擴展長度爲 \(2^k\) 的子序列 最後停在的位置;

\(g_{x,k}\) 表示以節點 \(x\)序列起點,向上擴展長度爲 \(2^k\) 的子序列 最後停在的位置。

\(f_{x,0}\)\(g_{x,0}\) 都可以通過一遍 \(dfs\) 處理出來。然後常規:

\[f_{x,0}=pre_{pos_{c_x}+1}\ \ \ \ f_{x,i}=f_{{f_{x,i-1}\ \ \ },i-1} \]

\[g_{x,0}=pre_{pos_{c_x}-1}\ \ \ \ g_{x,i}=g_{{g_{x,i-1}\ \ \ },i-1} \]

\[pre_{pos_{c_i}}=x \]

正序的部分就可以很好的用 -- 倍增 \(\text{1}\) -- 解決了。

\(\color{pink}{定向定終點,使用二分確定起點。}\)

倒序部分,考慮到問題的本質還是求最長的子序列,我們用二分。

只要二分了這一段的長度,就可以得到起點,然後又可以跳倍增力

其實對於 \(t\),要找到其祖先中多個起點也要平方級,於是我們

離線這個部分的詢問(

做完了。真 jb 難寫

Code - 「聯合省選 2021 A | B」寶石
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#include <vector>
using namespace std;
#define MAXN (int)(2e5+233)
#define MAXC (int)(2e5+233)
int book[MAXC],lg2[MAXN];
int n,m,C;

//E
struct qwq
{
	int nex,to;
}e[MAXN<<1];
int h[MAXN],tot=0;
inline void add(int x,int y)
{
	e[++tot].to=y;
	e[tot].nex=h[x];
	h[x]=tot;
}

int siz[MAXN],fa[MAXN],son[MAXN],dep[MAXN],top[MAXN];
void dfs_1(int x)
{
	siz[x]=1;
	for (int i=h[x],y;i;i=e[i].nex)
	{
		y=e[i].to;
		if (fa[x]==y) continue;
		fa[y]=x;
		dep[y]=dep[x]+1;
		dfs_1(y);
		siz[x]+=siz[y];
		if (siz[y]>siz[son[x]]) son[x]=y;
	}
}
void dfs_2(int x,int F)
{
	top[x]=F;
	if (!son[x]) return;
	dfs_2(son[x],F);
	for (int i=h[x],y;i;i=e[i].nex)
	{
		y=e[i].to;
		if (y==fa[x]||y==son[x]) continue;
		dfs_2(y,y);
	}
}
inline int lca(int x,int y)
{
	while (top[x]!=top[y])
	{
		if (dep[top[x]]<dep[top[y]]) swap(x,y);
		x=fa[top[x]];
	}
	return dep[x]<dep[y]?x:y;
}

int b[MAXN][17],c[MAXN][17];
int p[MAXC],a[MAXN],LIN[MAXC];
int stt[MAXN];

//offline
void dfs1(int x)
{
	b[x][0]=book[p[LIN[a[x]]+1]];
	c[x][0]=book[p[LIN[a[x]]-1]];
	for (int i=1;i<=lg2[dep[x]];i++)
	{
		 
		b[x][i]=b[b[x][i-1]][i-1];
		c[x][i]=c[c[x][i-1]][i-1];
	}
	int tmp=book[a[x]];
	book[a[x]]=x;
	stt[x]=book[p[1]];
	for (int i=h[x],y;i;i=e[i].nex)
	{
		y=e[i].to;
		if (y==fa[x]) continue;
		dfs1(y);
	}
	book[a[x]]=tmp;
}

struct que
{
	int x,tp,endc,ID;
};
vector<que> v[MAXN];
int answer[MAXN];

void sol1(int x,int lca,int y,int ID)
{
	//x=stt[x] dep[stt[x]] < dep[lca] ?
	if (dep[stt[x]]<dep[lca])
	{
		v[y].push_back((que){y,lca,0,ID});
		return;
	}
	x=stt[x];
	int ed=x;
	int LG=min(lg2[dep[x]],lg2[m]);
	for (int i=LG;i>=0;i--)
	{
		if (dep[b[ed][i]]<dep[lca]) continue;
		ed=b[ed][i];
	}
	v[y].push_back((que){y,lca,a[ed],ID});
	answer[ID]=LIN[a[ed]];
}
inline bool check(int s,int tlca,int x)
{
	x-=1;
	if (dep[s]<=dep[tlca]) return false;
	while (x&&s) { s=c[s][lg2[x]]; x-=(1<<lg2[x]); }
	return dep[s]>dep[tlca];
}
inline int bina(int s,int tlca,int lasted)
{
	int l=1,r=min(dep[s],m),mid;
	bool flag=0;
	while (l<r)
	{
		mid=((l+r+1)>>1);
		if (check(book[p[LIN[lasted]+mid]],tlca,mid)) l=mid,flag=1;
		else r=mid-1;
	}
	if (flag) return l;
	bool W_=check(book[p[LIN[lasted]+1]],tlca,1);
	return W_;
}
void dfs2(int x)
{
	int tmp=book[a[x]];
	book[a[x]]=x;
	int L=v[x].size();
	for (int i=0;i<L;i++)
		answer[v[x][i].ID]+=bina(v[x][i].x,v[x][i].tp,v[x][i].endc);
	for (int i=h[x],y;i;i=e[i].nex)
	{
		y=e[i].to;
		if (y==fa[x]) continue;
		dfs2(y);
	}
	book[a[x]]=tmp;
}
int main()
{
	scanf("%d%d%d",&n,&m,&C); 
	lg2[1]=0; for (int i=2;i<=n;i++) lg2[i]=lg2[i>>1]+1;
	for (int i=1;i<=C;i++) scanf("%d",&p[i]),LIN[p[i]]=i;
	for (int i=1;i<=n;i++) scanf("%d",&a[i]);
	for (int i=1,x,y;i<n;i++)
	{
		scanf("%d%d",&x,&y);
		add(x,y); add(y,x);
	}
	
	dep[1]=1;
	dfs_1(1);
	dfs_2(1,1);
	dfs1(1);
	int Q; int x,y,LCA;
	scanf("%d",&Q);
	for (int i=1;i<=Q;i++)
	{
		scanf("%d%d",&x,&y);
		LCA=lca(x,y); 
		sol1(x,LCA,y,i);
	}
	dfs2(1);
	for (int i=1;i<=Q;i++) printf("%d\n",answer[i]);
	return 0;
	//	for (int i=1;i<=n;i++) printf("-- %d:  %d\n",i,b[i][0]);
}

\(ST\)

【模板】ST 表

\(O(nlogn)\ -\ O(1)\ 區間\ RMQ\) 感覺只是因爲 \(max\) 運算的性質能 \(O(1)\) ? 感覺和倍增差不多。

例題:[SCOI2016]萌萌噠

每次修改,兩段等長的區間,點要一一對應連邊。實際上最後要求並查集個數然後計數就可以。

注意到單個區間之間的元素不互相影響,所以也可以把這些區間拆分爲 log 個區間兩兩連邊。

\(f_{i,k}\) 表示以元素 \(i\) 爲起點,長度爲 \(2^k\) 的區間。在這些區間連邊,並不會破壞我們修改的信息。

類似線段樹分治,最後再下傳所有的信息。枚舉 \(k\),將每層的區間並查集關係,拆成兩對下層的區間並查集關係。

很喵喵!


倍增,但是不是跳鏈題,是變形結合律題

例題:[CTSC2011]幸福路徑

跳的步數能滿足精度需求即可。大概是:

image

可以暴力,設 \(f_{i,k}\) 表示走 \(k\) 步到達 \(i\) 點的最大收益。

\[f_{i,k}=max([j\rightarrow i]f_{j,k-1})+\rho^k w_i \]

因爲每步貢獻係數不均等而且並非求和而是max所以不能矩陣之類的...對吧()

考慮類似弗洛伊德的做法有沒有比較好的性質。(但好像是用上面那個定義倍增沒法轉移())

如果 \(\rho=1\) 這題其實就滿足簡單結合律(然而實際上 \(\rho\neq1\) 大概是爲了滿足答案有極限),可以直接設 \(f_{i,j,k}\) 表示 \(i\)\(j\) 走了 \(2^k\) 步的最大收益。枚舉斷點 \(o\) 則:

\[f_{i,j,k}=max(f_{i,o,k-1}+f_{o,j,k-1}) \]

\(\rho<1\) 的時候滿足的是變形的結合律,就變成:

\[f_{i,j,k}=max(f_{i,o,k-1}+\rho^{2^{k-1}}f_{o,j,k-1}) \]

\(O(n^3log_{1e7})\) 差不多

例題:[NOIP2018 提高組] 保衛王國

換根 \(\text{DP}\) + 變形結合律倍增,但是是區間答案的可結合性。其實這個結合律還是挺廣義的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章