浅谈ST表(一)

一、前言

ST表是一种基于倍增思想的数据结构,它能够在O(nlogn)的时间复杂度下预处理出答案然后仅仅需要O(1)的时间复杂度回答查询。它经常用于处理RMQ(Range Maximum/Minimum Query)的类问题,更加概括的说,它能用于处理这一类可重复贡献问题:规定一种运算opt,有任意x满足x opt x=x,支持也仅支持多次差询区间的opt值。而这个“仅”也正是它的局限所在,它只支持静态区间的查询,而不支持动态查询。动态查询则通常借助线段树或者树形数组。

二、ST表的原理理解以及例题

ST表可以理解为利用了倍增思想的区间DP问题;接下来先分析倍增思想,然后是DP部分。

1.倍增思想

倍增 - OI Wiki (oi-wiki.org)

1.定义

(来自OI Wiki):

倍增法(英语:binary lifting),顾名思义就是翻倍。它能够使线性的处理转化为对数级的处理,大大地优化时间复杂度。这个方法在很多算法中均有应用,其中最常用的是 RMQ 问题和求 LCA(最近公共祖先)

2.例题

1.现在你有各种重量的砝码,问:如何用尽可能少的砝码表示出[0~31]之间的所有重量;

解析:将31转换成二进制形式 0001111 如果0表示不选,1表示选我们可以发现此时至多需要4个砝码。其他质量的表示方法类似,因此至多只需要4个砝码;通过这道题不难发现倍增思想通过二进制大大减少我们要处理的情况数。

2.区间DP

首先我们规定一种运算opt,对于任意x满足x opt x=x;

1.状态定义

区间DP求各个区间的最大值,我们将状态 opt[i][j] 定义为从i开始的长度为2^j的区间的opt运算的值(最大值/最小值/公约数)。

2.确定初始状态

根据opt运算的定义我们可知 opt[i][0]=x[i],即从区间第i个数开始,长度为2^0(1)的opt值为这个数本身;

3.状态转移方程

从下图的推导中我们不难得出状态转移方程f[i][j]=opt(f[i][j-1],f[i+2^(j-1)][j-1]);

4.代码实现

/*本代码为模版伪代码,仅供参考*/

#include<iostream>
using namespace std;
const int N=2000010;

int n,q;
int mn[N],st[N][22],a[N];

void init()
{
    mn[0]=-1;
    for(int i=1;i<=n;i++)
    {
        mn[i]=((i&(i-1))==0)?mn[i-1]+1:mn[i-1];
        st[i][0]=a[i];  //这一步真的很奇妙
    }
    
    for(int j=1;j<=mn[n];j++)
        for(int i=1;i+(1<<j)-1<=n;i++)
            st[i][j]=opt(st[i][j-1],st[i+(1<<(j-1))][j-1]);     //此处可以替换成具体的opt运算

    
}

inline int read()
{
    int x=0,f=1;char ch=getchar();
    while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
    while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
    return x*f;
}

int rmq_opt(int l,int r)
{
    int k=mn[r-l+1];
    return opt(st[l][k],st[r-(1<<k)+1][k]);
}

int main()
{
    
    n=read(),q=read();
    
    for(int i=1;i<=n;i++)a[i]=read();
    
    init();
    
    while(q--)
    {
        int l,r;
        scanf("%d%d",&l,&r);
        int x=rmq_opt(l,r);
       printf("%d\n",x);
    }
    return 0;
}


3.例题:求区间公约数

P1890 gcd区间 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

1、题目描述

给定 n 个正整数 1,2,…a1​,a2​,…,an​。

m 次询问,每次询问给定一个区间 [l,r],输出 al​,al+1​,…,ar​ 的最大公因数。

输入格式

第一行两个整数 n,m。
第二行 n 个整数表示a1​,a2​,…,an​。
以下 m 行,每行两个整数 l,r 表示询问区间的左右端点。

输出格式

共 m 行,每行表示一个询问的答案。

输入输出样例

输入 #1复制

5 3
4 12 3 6 7
1 3
2 3
5 5

输出 #1复制

1
3
7

说明/提示

  • 对于 30%30% 的数据,1≤n≤100,1≤m≤10;
  • 对于 60%60% 的数据,1≤m≤1000;
  • 对于 100%100% 的数据,1≤l≤r≤n≤1000,1≤m≤106,1≤ai​≤109。

2.解析

将模版中的opt用gcd实现即可,模板题;

3.ac代码

#include<iostream>
#include<algorithm>
using namespace std;
const int N=2000010;

int n,q;
int st[N][22],a[N],mn[N];

int gcd(int a,int b)
{
	if(a<b)swap(a,b);
	if(a%b==0)return b;
	else return gcd(b,a%b);
}

void init()
{
	mn[0]=-1;
	
	for(int i=1;i<=n;i++)
	{
		st[i][0]=a[i];
		mn[i]=((i&(i-1))==0)?mn[i-1]+1:mn[i-1];
	}
	
	for(int j=1;j<=mn[n];j++)
		for(int i=1;i+(1<<j)-1<=n;i++)
			st[i][j]=gcd(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}

int query(int l,int r)
{
	int k=mn[r-l+1];
	return gcd(st[l][k],st[r-(1<<k)+1][k]);
}

int main()
{
	cin>>n>>q;
	
	for(int i=1;i<=n;i++)cin>>a[i];
	
	init();
	while(q--)
	{
	    int l,r;
	    cin>>l>>r;
	    cout<<query(l,r)<<endl;
	}
	
}
  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值