st 算法学习笔记

前言

在看这篇文章之前,请先自行了解以下几项东西:

  • 1.倍增思想。

  • 2.动态规划思想。

  • 3.乘方位运算实现

如有错误,欢迎各位 dalao 批评指出。

什么是 \(st\) 算法?

st 算法是一种解决 RMQ 问题的算法。RMQRange Minimum/Maximum Query,即区间最大最小值查询。

该算法采用了 动态规划和倍增 的思想,预处理 \(O(nlogn)\),查询 \(O(1)\),适用于一些查询较多的题目。

\(st\) 算法具体实现

接下来给大家讲解如何实现 \(st\) 算法。

我们假定是要求出 \(a\) 数组上的区间最大值,

我们定义 \(dp[i][j]\) 表示从 \(i\) 开始,长度为 \(2^j\) 的区间的最大值。

则对于初始值,\(dp[i][0]=a[i]\),这个比较容易想到,即从 \(i\) 开始,长度为 \(1\) 的区间就是它本身,所以是 \(a[i]\)

接着是状态转移。显然,对于一个长度为 \(2^j\) 的区间,我们可以把它划分为两个长度为 \(2^{j-1}\) 的区间,然后再求最大值。则状态转移为:\(dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1])\),其中 \(1<<(j-1)\),就表示 \(2^{j-1}\)

但是,当我们状态转移在枚举循环时,我们需要注意的是,我们要先枚举区间长度越小的在枚举区间长度越大的,所以 \(j\) 应该放在外层循环,而 \(i\) 就应该放在内层循环。

则此时,我们就可以先预处理出所有 \(dp[i][j]\)。时间复杂度也就是 \(O(nlogn)\)

核心代码:

void init()//预处理函数
{
	for(int i=1;i<=n;++i)
	dp[i][0]=a[i];//初始值
	for(int j=1;(1<<j)<=n;++j)//第二维j,注意 2^j 要小于n
	{
		for(int i=1;i+(1<<j)-1<=n;++i)//枚举i时要保证从i开始,有一个长度为 2^j 的区间时,不能超出 n。
		{
			dp[i][j]=max(dp[i][j-1],dp[i+(1<<j-1)]][j-1]);//状态转移
		}
	}
}

最后就是如何查询的问题。

假设我们要查询的是 \([l,r]\) 这个区间的区间最大值。我们可以发现,我们查询最大值的话,我们可以将 \([l,r]\) 分成两个区间来查询,并且这两个区间总体合起来要包含 \([l,r]\),且这两个区间可以不刚好包含,两个区间可以存在交集。

此时,我们可以定义一个数字 \(k\),表示 \(\log_2^{r-l+1}\)(向下取整) ,则我们就可以根据 \(k\),来求出我们要寻找的 \([l,r]\) 的区间最大值,即求出 \(max(dp[l][k],dp[r-(1<<k)+1][k])\) 。这个查询的正确性也是十分明显,因为两个区间长度都是 \(2^k\),并且 \(2^k+2^k=2^{k+1}>(r-l+1)\),所以这两个区间一定可以完全覆盖 \([l,r]\)

查询核心代码:

int rmq(int l,int r)
{
	int k=log2(r-l+1);
	return max(dp[l][k],dp[r-(1<<k)+1][k]);
}

可以发现,\(st\) 算法是无法完成修改操作的,即使要去完成也效率过慢。但是对于只查询的题目来说,\(st\) 算法一定是当之无愧的神!

\(st\) 算法的一些扩展

\(st\) 算法是解决 RMQ 问题的,但是这种算法的思想我们却可以继续延伸下去。

\(st\) 算法之所以不能解决区间和是因为他在查询时,取 \(max\) 两个区间可能存在交集,也就是可能会重复计算。那么是不是可以重复计算的无修改的问题都可以运用 \(st\) 算法的思想解决呢?of course!

为了让大家更好理解,我这里先抛给大家一道例题,相信你做出来一定可以领会到 \(st\) 算法思想的精髓所在。

洛谷 P1890 gcd区间

显而易见,这个题目就是求区间 GCD ,并且我们可与发现,GCD 我们是可以重复计算的。举个例子,\(gcd(10,5)=gcd(10,10,5)\)

既然可以重复计算,那就肯定可以用 \(st\) 算法的思想进行求解了!

我们只需要把 RMQ 问题中的 \(max\) 改为 \(gcd\) 就可以了。(代码中为了方便,我直接调用了 c++ 的内置函数 __gcd

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[500005];
int dp[500005][35];
void init()
{
	for(int j=1;(1<<j)<=n;++j)
	{
		for(int i=1;i+(1<<j)-1<=n;++i)
		{
			dp[i][j]=__gcd(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);//直接求 i~i+2^j-1 这个区间的gcd
		}
	}
}
int rmq(int l,int r)
{
	int k=log2(r-l+1);
	return __gcd(dp[l][k],dp[r-(1<<k)+1][k]);//我们只需要把 max 改为 __gcd 即可。
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i)
	scanf("%d",&a[i]),dp[i][0]=a[i];
	init();
	while(m--)
	{
		int l,r;
		scanf("%d%d",&l,&r);
		printf("%d\n",rmq(l,r));
	}
	return 0;
}

除了在算法思想上的扩展,对于算法本身,我们也可以进行扩展。

例如 \(st\) 算法本身其实是可以有二维形式的,例题:洛谷 P2216 [HAOI2007]理想的正方形

这个题目的话思路还是比较清晰的,至于具体代码实现请读者自己实践,如果实在做不出来就看一看题解吧。

后记

今天的算法就讲到这里,希望大家能有所收获,如有不懂的地方,也可以私信我。

posted @ 2024-03-04 15:05  Saltyfish6  阅读(10)  评论(0编辑  收藏  举报
Document