大家好
我想告诉你们一个数据结构,我认为它应该在CP社区中被广泛了解。您不会在标准库中找到这些数据结构,因此我们需要自己实现这些结构。我们将用稀疏表解决CP中两个非常有用的问题:最低共同祖先(LCA)问题和范围最小查询(RMQ)问题。
稀疏表是一种数据结构,允许回答静态范围查询。它可以在0(log N)时间内回答大多数范围查询,但它可以在0(1)时间内有效地回答范围最小查询(或等效的范围最大查询)。它预先进行预处理,以便能够有效地回答查询。
例如,你有一个数组" arr "你想执行一些查询。每次查询都要在子数组[L, R]上计算函数F: F(arr[L], arr[L + 1],…,arr[R])。对于稀疏表,您可以在O(log(N)) (N是arr的大小)中执行每个查询,并进行初始O(N * log(N))预处理。在不可变数据的情况下,它经常作为段树的替代品。
. . .
限制条件
稀疏表可以用来当且仅当:
数组是不可变的(即查询不会改变它)
函数F是结合律:-F(a, b, c) = F(F(a, b), c) = F(a, F(b, c))
理解稀疏表
想象一下,你正在看一部电影,你的朋友向你保证,某个时期的情节没什么有趣的。他还讲了那个时期的故事。现在你不需要看完整部电影,而且你已经节省了你宝贵的时间。现在想象一下,你非常想看电影《蜘蛛侠:英雄无归》。但是因为日程安排,你不能去电影院了。假设你有很多朋友,他们都看过这部电影的部分内容,如果把他们加在一起,他们就看完了整部电影。现在你不必去看戏了,只要你的朋友有足够的口才。你只要简单地问他们关于电影的事,他们就会一点一点地给你讲述。
和这个稀疏表类似,它可以帮助你节省很多时间,它通常以块的形式存储数据,并在需要的时候有效地回答问题。
. . .
意识形态
我们知道任何数都可以用二进制表示为2的不同次幂的和。
例如 19=(10011)2=16+2+1.
用同样的思想,任何区间都可以唯一地表示为长度为2的不同次幂的区间的并集。
例如 [2,14]= [2,9] ∪ [10,13] ∪ [14,14]
这里,完全音程的长度为13,而单个音程的长度分别为8、4和1。这里的并集最多由log2(R-L+1)多个区间组成。
为了计算某个长度为2^k的区间的值,只需将两个大小为2^(k - 1)的区间连接在一起,因此每个区间都可以在恒定的时间内计算。
稀疏表背后的主要思想是预先计算所有长度为2的范围查询的答案。然后,可以通过将范围分割为两个长度幂的范围来回答不同的范围查询,查找预先计算的答案,并将它们组合起来以获得一个完整的答案。
. . .
构建稀疏表
稀疏表可以使用动态规划以自底向上的方式构建。我们将使用一个二维数组来存储预先计算的查询的答案,表[i][j]将存储长度为2^j的范围[i,i+(1<二维数组的大小为N×(K+1),其中N为数组长度,K = ceil(log2(N))。
让我们以Range Sum查询为例来了解稀疏表的构建。
. . .
应用
最小范围查询(RMQ)
这些查询正是稀疏表的亮点所在。范围最小查询解决了在数组范围内查找最小值的问题。
算法
在计算一个范围的最小值时,在这个范围内处理一个值一次还是两次并不重要。因此,与将一个范围分割为多个范围不同,我们还可以将该范围分割为两个长度幂重叠的范围。这样我们可以在0(1)时间复杂度下有效地回答RMQ。
因此,我们可以用以下方法计算出范围[L,R]的最小值:
这里,在给定的图中,我们要求在[0,8]范围内找到最小的元素。在这里,范围的长度是log2(8 - 0+1)~3,因此新的范围将被除为min([0,8])=min(min([0,7]),min([5,8])),从预先计算的 2- d稀疏表中得到min([0,8])=min(2,1)=1。因此全量程[0,8]的最小值为1,也可以手动验证。
c++实现
const int N=1e5;
int K=log2(N);
int lookup[N][K+1];
int arr[N];
void build()
{
for(int i=0;i<N;i++)
lookup[i][0]=arr[i];
for(int j=1;j<=K;j++)
{
for (int i = 0; i + (1 << j) <= N; i++)
lookup[i][j] = min(lookup[i][j-1],lookup[i + (1 << (j - 1))][j - 1]);
}
}
int query(int L,int R)
{
int j = log2(R-L+1);
int minimum =min(lookup[L][j], lookup[R-(1 << j) + 1][j]);
return minimum;
}
. . .
最近的共同先祖
假设G是一棵树。为每个查询表单的LCA (u, v)我们应该找到最低的共同祖先u和v的节点,即我们想要找到一个节点w,位于从u到根节点的路径,这位于v到根节点的路径,如果有多个节点选择一个最远的离根节点
算法
对于每个节点,我们将预先计算其在他之上的祖先,其两个节点之上的祖先,其四个节点之上的祖先,其八个节点之上的祖先,等等,并存储它。
我们将使用2d数组,比如up, up[i][j]存储节点的2^j祖先,其中i=1…N, j = 0…装天花板(log (N))。
我们可以使用树遍历技术(最好是DFS)来计算这个数组。
对于每个节点,我们也会记得第一次遇到这个节点的时间(“in time”),以及离开它的时间(即在我们访问了它在子树中的所有节点并退出DFS函数后)(“out time”)。我们可以使用这个信息来确定一个节点是否是另一个节点在常数时间内的祖先。
- 当且仅当节点u的“in”时间小于或等于节点v的“in”时间,且节点u的“out”时间大于或等于节点v的“out”时间时,节点u将成为节点v的祖先。
我们首先检查是否一个节点从两个其他的祖先,如果一个节点的祖先其他则这两个节点的LCA否则我们找到一个节点不u和v的共同祖先和最高(即。一个节点x,它不是u和v的共同祖先,而是向上的[x][0])。在找到这样一个节点(设为x)之后,我们打印x的第一个祖先,即up[x][0],这将是所需的LCA。
*当构建稀疏表时,我们将使用
up[i][j]= up[up[i][j-1]][j-1];
这里,在给定的图中,我们可以看到,我们将检查节点14和节点15。从节点15开始,我们首先跳4个单元,检查节点3是否是节点14的祖先,是。我们将检查2的幂的递减顺序。然后我们将进行2个单元的小跳跃,检查节点7是否是节点14的祖先,不,它不是。然后从节点7开始,我们将跳跃几个不同的2次方。但每次它都会降落在节点14的祖先节点上。然后我们得出结论,节点7是最高的节点,它不是节点14和节点15的祖先。然后我们会得出结论,节点7的第2个祖先是节点14和节点15的最低共同祖先。因此节点4就是上图中节点14和节点15的LCA。
C++实现
int n,k;
vector<vector<int>> adj; //Adjacency List
vector<vector<int>> up;//Sparse Table
int timer=0; //Variable for time
vector<int> tin, tout;//for storing entry and exit time for a particular.
void dfs(int node,int parent)
{
tin[node] = ++timer;
up[node][0] = parent;
for(int i=1;i<=k;i++)
up[node][i]=up[up[node][i-1]][i-1];
for(auto &it: adj[node])
{
if(it!=parent)
dfs(it,node);
}
tout[node]= ++timer;
}
bool is_ancestor(int u, int v)
{
return (tin[u]<=tin[v] && tout[u]>=tout[v]); //Condition for checking if u is ancestor of v
}
int lca(int u,int v)
{
if (is_ancestor(u, v))
return u;
if (is_ancestor(v, u))
return v;
for(int i=k;i>=0;i--)
{
if (!is_ancestor(up[u][i], v))
u=up[u][i];
}
return up[u][0];
}
void solve(int root)
{
tin.clear();
tin.resize(n+1);
tout.clear();
tout.resize(n+1);
timer=0;
k=ceil(log2(n));
up.resize(n,vector<int>(k+1));
dfs(root, root);
}
. . .
感谢你的阅读
欢迎在下面的评论中发表你的观点。