倍增与ST表闲话
在算法竞赛中,我们一般都被一件事情所困扰:如果从 \(\mathcal O(n^2)\rightarrow \mathcal O(n\log n)\) 呢?
其实很简单。我们发现如果想要有 \(\mathcal O(n\log n)\) 做法,我们一般考虑分治或倍增(即使是线段树等高级数据结构,绝大部分都是分治)。
分治其实也很简单,我们发现如果这一道题可以二进制拆分,我们肯定会对于二进制位做;如果这道题序列有序,我们大概率会去二分,若序列无序,我们就会用分治做法。
而倍增就是一个与分治过程相反的过程,他要求你去二进制扩充。
由于在算法竞赛中,倍增出现次数较少,所以很多人都头疼:什么时候、怎么才能使用倍增算法呢?
先来举个例子。
我们知道,莫队如果能 \(\mathcal O(1)\) 的从 \(l\rightarrow l\pm 1\) 以及 \(r\rightarrow r\pm 1\),我们就可以使用莫队。
我们先来看看倍增的一些性质。我们知道,倍增其实和二分很像。倍增只不过是把二分过程逆序过来。我们可以将 \(2\) 个 \(2^{j-1}\) 的序列的答案合并为一个长度 \(2^j\) 的序列,那么我们就可以使用倍增算法。
那么,我们就发现:1.在一个算法中,我们可以选择一位一位的去扩展合并序列 2.符合可加性(即两个序列答案可合并)。那么我们可以考虑倍增。
我们可以举几个例题。
P3865 【模板】ST 表 && RMQ 问题
这道题虽然叫做 ST 表模板题,但是我们还是可以从中看出倍增的味道,我们来品一品。
首先第一个条件一定符合,即裸的区间 dp 即可。
至于第二个条件,想一想也是符合的。一个序列的最小值,即为左右两半序列的最小值的较小值。
于是,st 表诞生了。
P1081 [NOIP 2012 提高组] 开车旅行
这道题也是很容易看出倍增,我们来看看。
我们发现,我们可以一步一步的从一个城市跳到最后结束的城市,符合倍增第一条性质。
而且我们发现,我们可以把两步和为一块去处理,因为路程是可加的,可以合并。
看到这里直接上倍增做完了,体验水紫的乐趣。
难点在于如何预处理最小值与次小值,我们可以按照海拔排序后建立双向链表,然后我们就可以从表头开始找最小值与次小值。最小值一定是在 \(i\pm 1\) 的位置,次小值同理即可。
P3295 [SCOI2016] 萌萌哒
这道题很新颖。我们先考虑暴力做法。
可以每一次对 \(l1-r1\) 以及 \(l2-r2\) 的每一位置进行并查集合并操作,设集合个数为 \(S\),则最后答案为 \(9\times 10^{S-1}\)。
这么做复杂度为 \(\mathcal O(nm)\),考虑优化。
一个最具引导性的事情是:并查集可以合并,并且可以一个元素一个元素的合并!
一眼在并查集上倍增。我们假设 \(f_{x,k}\) 表示以 \(x\) 为起点,长度为 \(2^k\) 的序列在哪一个集合里,然后倍增统计即可,复杂度 \(\mathcal O(n\log m)\)。
可能会太抽象,所以赋个代码。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,MOD=1e9+7;
int n,m,f[N][25];
int find(int x,int k)
{
if(x==f[x][k]) return x;
return f[x][k]=find(f[x][k],k);
}
void merge(int x,int y,int k)
{
int fx=find(x,k),fy=find(y,k);
if(fx!=fy) f[fx][k]=fy;
}
int qsm(int x,int k)
{
int res=1;
for(;k;k>>=1,x=x*x%MOD)
if(k&1) res=res*x%MOD;
return res;
}
signed main()
{
cin>>n>>m;
for(int j=0;j<=20;j++)
for(int i=1;i<=n;i++)
f[i][j]=i;
for(int i=1;i<=m;i++)
{
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
for(int j=20;~j;j--)
if(l1+(1<<j)-1<=r1)
merge(l1,l2,j),l1+=(1<<j),l2+=(1<<j);
}
for(int j=20;j;j--)
for(int i=1;i+(1<<j)-1<=n;i++)
{
int fi=find(i,j);if(fi==i) continue;
merge(i,fi,j-1),merge(i+(1<<(j-1)),fi+(1<<(j-1)),j-1);
}
int cnt=0;
for(int i=1;i<=n;i++)
if(find(i,0)==i)
cnt++;
cout<<qsm(10,cnt-1)*9%MOD;
return 0;
}
P3292 [SCOI2016] 幸运数字 做题记录
这道题是一道线性基合并的题目。
首先发现 LCA 可以倍增实现,而这道题又是集合最大异或和,一眼线性基。
参考并查集倍增,我们可以考虑线性倍增与合并。
我们假设维护一个倍增线性基 \(BASE[x][i]\) 表示从 \(x\) 开始,向上跳 \(2^i\) 个祖先,这一条路径上的线性基。
那么很好转移:\(BASE[x][i]=merge(BASE[x][i-1],BASE[f[x][i-1]][i-1])\)。
考虑 merge 函数的实现就是将线性基 \(A\) 的所有元素插入到 \(B\) 内即可。
注意 corner case。如果 \(x=y\),那么要直接输出 \(a[x]\)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e4+5;
vector<int> G[N];
int n,m,dep[N],f[N][16],a[N];
struct LB
{
int p[65];
void clear(){memset(p,0,sizeof(p));}
void ins(int x)
{
for(int i=61;~i;i--)
{
if(~(x>>i)&1) continue;
if(!p[i])
{
p[i]=x;
break;
}
x^=p[i];
}
}
friend LB operator +(const LB &A,const LB &B)
{
LB T=A;
for(int i=61;~i;i--)
if(B.p[i]) T.ins(B.p[i]);
return T;
}
}B[N][16];
void dfs(int x,int fa)
{
f[x][0]=fa,dep[x]=dep[fa]+1;B[x][0].clear();
B[x][0].ins(a[x]),B[x][0].ins(a[fa]);
for(int i=1;i<=15;i++)
{
f[x][i]=f[f[x][i-1]][i-1];
B[x][i].clear();
B[x][i]=B[x][i-1]+B[f[x][i-1]][i-1];
}
for(int v:G[x])
{
if(v==fa) continue;
dfs(v,x);
}
}
LB LCA(int x,int y)
{
if(dep[x]<dep[y]) swap(x,y);
LB T;T.clear();
for(int i=15;~i;i--)
if(dep[f[x][i]]>=dep[y])
T=T+B[x][i],x=f[x][i];
if(x==y) return T;
for(int i=15;~i;i--)
{
if(f[x][i]==f[y][i]) continue;
T=T+B[x][i]+B[y][i];
x=f[x][i],y=f[y][i];
}
return T+B[x][0]+B[y][0];
}
signed main()
{
// freopen("a.in","r",stdin);
// freopen("a.out","w",stdout);
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<n;i++)
{
int x,y;cin>>x>>y;
G[x].push_back(y);
G[y].push_back(x);
}
dfs(1,0);
while(m--)
{
int x,y;cin>>x>>y;
if(x==y)
{
cout<<a[x]<<"\n";
continue;
}
LB T;T.clear();
T=LCA(x,y);
int res=0;
for(int i=61;~i;i--)
res=max(res,res^T.p[i]);
cout<<res<<"\n";
}
return 0;
}

浙公网安备 33010602011771号