【数据结构与算法】RMQ+ST及线段树

昨天做了一道华为的机试题,关于RMQ。用了自己想的算法,无奈内存超了限制。


题目如下:

老师想知道从某某同学当中,分数最高的是多少,现在请你编程模拟老师的询问。当然,老师有时候需要更新某位同学的成绩. 

输入描述:
输入包括多组测试数据。
每组输入第一行是两个正整数N和M(0 < N <= 30000,0 < M < 5000),分别代表学生的数目和操作的数目。
学生ID编号从1编到N。
第二行包含N个整数,代表这N个学生的初始成绩,其中第i个数代表ID为i的学生的成绩
接下来又M行,每一行有一个字符C(只取‘Q’或‘U’),和两个正整数A,B,当C为'Q'的时候, 表示这是一条询问操作,他询问ID从A到B(包括A,B)的学生当中,成绩最高的是多少
当C为‘U’的时候,表示这是一条更新操作,要求把ID为A的学生的成绩更改为B。


输出描述:
对于每一次询问操作,在一行里面输出最高成绩.

输入例子:
5 7
1 2 3 4 5
Q 1 5
U 3 6
Q 3 4
Q 4 5
U 4 5
U 2 9
Q 1 5

输出例子:
5
6
5
9

我的题解如下:

#include <iostream>
#include <vector>
using namespace std;

inline int findmax(int a, int b)
{
    return (a > b ? a : b);
}

void Init(int** rmq, int N)
{
    int i, j;
    for(i = 1; i < N; ++i)
    {
        for(j = 1; j <= N - i; ++j)
        {
            rmq[j][j + i] = findmax(rmq[j][j + i - 1], rmq[j + 1][j + i]);
        }
    }
}

void update(int** rmq, int id, int score, int N)
{
    rmq[id][id] = score;
    int i, j;
    for(i = 1; i < N; ++i)
    {
        for(j = 1; j <= N - i; ++j)
        {
            rmq[j][j + i] = findmax(rmq[j][j + i - 1], rmq[j + 1][j + i]);
        }
    }
}

int query(int** rmq, int left, int right)
{
    return rmq[left][right];
}

int main()
{
    int N, M, i, score, left, right, id;
    char command;
    while(cin >> N >> M)
    {
        int** rmq = new int* [N + 1];
	for(i = 0; i < N + 1; i++)
		rmq[i] = new int[N + 1];

	i = 1;
        while(i <= N)
        {
           cin >> score;
     	  rmq[i][i] =  score;
	  i++;
        }
        
	Init(rmq, N);
 	while(M--)
        {
            cin >> command;
            if(command == 'Q')
            {
                cin >> left >> right;
                cout << query(rmq, left, right) << endl;
            }
            else if(command == 'U')
            {
                cin >> id >> score;
                update(rmq, id, score, N);
            }
        }
        
    }
    return 0;
}

解法主要使用了rmq[N+1][N+1]这个二维矩阵来存储区间最值,比如 rmq[1][4] 表示学生 id 从1到4之间的最值,而 rmq[1][1] 就简单表示学生1的成绩。算法过程形象描述如下(数字表示第i轮):



算法时间复杂度O(N^2)(指一次预处理或一次更新,其实参照题目要求的那个修改频率,时间复杂度蛮差的,在更新上还需剪枝优化),空间复杂度O(N^2),考虑到N可能很大,内存就有可能用完了。。然后。。。


然后决定学习下 Sparse Table(稀疏表)方法。

学习自:http://noalgo.info/489.html(NOALGO博客)


算法思想:

ST(Sparse Table,稀疏表)算法是求解RMQ问题的经典在线算法,以O(nlogn)时间预处理,然后在O(1)时间内回答每个查询。


ST算法本质上是动态规划算法,定义了一个二维辅助数组st[n][n],st[i][j]表示原数组a中从下标i开始,长度为2^j的子数组中的最值(以最大值为例)。注意:可以发现这样的话,空间复杂度一下减了很多!!

要求解st[i][j]时,即求下标i开始,长度为2^j的子数组的最大值时,可以把这段子数组再划分成两半,每半的长度为2^(j-1),于是前一半的最大值为st[i][j-1],后一半的最大值为st[i+2^(j-1)][j-1],于是动态规划的转移方程为:

st[i][j] = max(st[i][j-1], st[i+2^(j-1)][j-1])
长度为2^j的情况只和长度为2^(j-1)的情况有关,只需要初始化长度为2^0=1的情况即可。而长度为1时的最大值是显然的(为其本身)。


现在问题是,st数组可以怎样加速我们的查询呢?

这也是算法的巧妙之处,假设求下标在u到v之间的最大值。先求u和v之间的长度len=v-u+1,然后求k=log2(len),则u到v之间的子数组可以分为两部分:
1)以u开始,长度为2^k的一段
2)以v结束,长度为2^k的一段(可以计算得到起始位置为v-2^k+1)
注意,一般情况下这两段是重叠的,但是这两段的最大值中较小的一个仍然是u到v的最大值。于是

RMQ(u,v) = max(st[u][k], st[v-2^k+1][k])

核心算法如下:

const int mx = 10000 + 10;  //数组最大长度
int n, a[mx]; //数组长度,数组内容

int st[mx][30]; //DP数组
void initRMQ()
{
	for (int i = 0; i < n; i++) st[i][0] = a[i];
	for (int j = 1; (1 << j) <= n; j++) //使用位运算加速
		for (int i = 0; i + (1 << j) - 1 < n; i++)
			st[i][j] = max(st[i][j-1], st[i+(1<<(j-1))][j-1]);
}

int RMQ(int u, int v)
{
	int k = (int)(log(v-u+1.0) / log(2.0)); //类型转换优先级高,除法整体要加括号
	return max(st[u][k], st[v-(1<<k)+1][k]);
}
其中使用位运算替代2的幂次的计算,加快运算速度。
使用时需要先调用initRMQ()进行初始化,然后再调用RMQ(u,v)进行查询。


有时候需要得到最值的下标而不是最值内容,这时可以使用以下的下标版本:

const int mx = 10000 + 10;  //数组最大长度
int n, a[mx]; //数组长度,数组内容

int st[mx][30]; //DP数组
void initRMQIndex() 
{
	for (int i = 0; i < n; i++)		st[i][0] = i;
	for (int j = 1; (1 << j) <= n; j++)
		for (int i = 0; i + (1 << j) - 1 < n; i++)
			st[i][j] = a[st[i][j-1]] > a[st[i+(1<<(j-1))][j-1]] ? st[i][j-1] : st[i+(1<<(j-1))][j-1];
}

int RMQIndex(int s, int v) //返回最大值的下标
{
	int k = int(log(v-s+1.0) / log(2.0));
	return a[st[s][k]] > a[st[v-(1<<k)+1][k]] ? st[s][k] : st[v-(1<<k)+1][k];
}



下面用此法改进之前的华为题目:

#include <iostream>
#include <math.h>
using namespace std;

#define MAXN 30010
int rmq[MAXN][30];

inline int findmax(int a, int b)
{
    return (a > b ? a : b);
}

void Init(int N)
{
    int i, j;
    for(j = 1; (1 << j) <= N; ++j)
        for(i = 1; i + (1 << j) - 1 <= N; ++i)
        	rmq[i][j] = findmax(rmq[i][j - 1], rmq[i + (1 << (j - 1))][j - 1]);
}

void update(int id, int score, int N)
{
    rmq[id][0] = score;
	int i, j;
    for(j = 1; (1 << j) <= N; ++j)
        for(i = 1; i + (1 << j) - 1 <= N; ++i)
        	rmq[i][j] = findmax(rmq[i][j - 1], rmq[i + (1 << (j - 1))][j - 1]);
}
   
int query(int left, int right)
{
    int k = (int) (log(right - left + 1.0) / log(2.0));
    return findmax(rmq[left][k], rmq[right - (1 << k) + 1][k]);
}

int main()
{
    int N, M, i, score, left, right, id;
    char command;
    while(cin >> N >> M)
    {
		i = 1;
        while(i <= N)
        {
            cin >> score;
            rmq[i][0] = score;
            i++;
        }
        
	Init(N);
 	while(M--)
    	{
        	cin >> command;
        	if(command == 'Q')
        	{
			cin >> left >> right;
            		cout << query(left, right) << endl;
        	}
        	else if(command == 'U')
        	{
			cin >> id >> score;
			update(id, score, N);
        	}
    	}
        
    }
    return 0;
}

题目样例通过,但在网上提交报Segment Fault,还没找到原因。


 感觉很不爽,再找个题目验证下,hihocoder #1068 RMQ-ST算法,算法完全一样:

#include <stdio.h>
#include <math.h>
using namespace std;

#define MAXN 1000010
int rmq[MAXN][30];

inline int findmin(int a, int b)
{
    return (a < b ? a : b);
}

void Init(int N)
{
    int i, j;
    for(j = 1; (1 << j) <= N; ++j)
        for(i = 1; i + (1 << j) - 1 <= N; ++i)
        	rmq[i][j] = findmin(rmq[i][j - 1], rmq[i + (1 << (j - 1))][j - 1]);
}
   
int query(int left, int right)
{
    int k = (int) (log(right - left + 1.0) / log(2.0));
    return findmin(rmq[left][k], rmq[right - (1 << k) + 1][k]);
}

int main()
{
    int N, Q, i, score, left, right;
    
    scanf("%d", &N);
   if(N == 0)
	return 0;
	    
   i = 1;
    while(i <= N)
    {
        scanf("%d", &score);
        rmq[i][0] = score;
        i++;
    }
        
   Init(N);
   scanf("%d", &Q);
 	    
   while(Q--)
   {
	scanf("%d %d", &left, &right);
        printf("%d\n", query(left, right));
    }
        
    return 0;
}


开始用的 cin 和 cout,结果超时了,试着用了下 scanf 和 printf ,AC!

可能的主要原因是例子中的输入输出很多,用 scanf 和 printf 能够节省不少时间。



继续往下一看,#1070 : RMQ问题再临 这个问题中涉及了修改个别点的值以及更新问题,不过该题需用到另一种数据结构线段树

关于更新:如果是在修改 rmq[][] 后再次执行一次 Init 的操作,无疑将会超时。而事实也是这样。




线段树


这篇博文简单易懂:http://harryguo.me/2015/12/15/%E7%BA%BF%E6%AE%B5%E6%A0%91%E8%AE%B2%E8%A7%A3%E4%B8%80/(HarryGuo)


看完发现和自己最初的想法有点相似,不过线段树最重要的思想,也是我没想到的是“分治思想”


线段树使得不论是修改还是查询,时间复杂度都是O(logN)。

按照线段树的思想,做了#1070 : RMQ问题再临,时间和空间复杂度较之前的RMQ+ST都小了很多,线段树太棒了!

//1MB 26ms 
#include <stdio.h>
#include <math.h>
#include <set>
using namespace std;

#define MAXN 10010
int data[MAXN];
int tree[4 * MAXN];
int N;
set<int> ans;

void pushUp(int v)
{
	tree[v] = min(tree[v << 1], tree[v << 1 | 1]);
	return;
}

void build(int L, int R, int v)
{
	if(L == R)
	{
		tree[v] = data[L];
		return;
	}
	
	int m = (L + R) >> 1;
	build(L, m, v << 1);
	build(m + 1, R, v << 1 | 1);
	pushUp(v);
	return;
}

void update(int id, int val, int L, int R, int v)
{
	if(L == R)
	{
		tree[v] = val;
		return;
	}
	
	int m = (L + R) >> 1;
	if(id <= m)
		update(id, val, L, m, v << 1);
	else
		update(id, val, m + 1, R, v << 1 | 1);
	pushUp(v);
	return;
}

void query(int left, int right, int L, int R, int v)
{
	if(L >= left && R <= right)
	{	
		ans.insert(tree[v]);
		return;
	}
	int m = (L + R) >> 1;
	if(left <= m)
		query(left, right, L, m, v << 1);
	if(right > m)
		query(left, right, m + 1, R, v << 1 | 1);
	return;
}


int main()
{
    int Q, i, score, left, right;
    set<int>::iterator it;

    scanf("%d", &N);
		if(N == 0)
	return 0;
		
	i = 1;
    while(i <= N)
    {
        scanf("%d", &score);
        data[i++] = score;
    }

	build(1, N, 1);
	scanf("%d", &Q);
	while(Q--)
	{
	    scanf("%d %d %d", &i, &left, &right);
	    if(i == 0)
		{
			ans.clear();
			query(left, right, 1, N, 1);
			it = ans.begin();
			printf("%d\n", *it);
		}
		else
            update(left, right, 1, N, 1);
    }
        
    return 0;
}

见识到了线段树的优点,我会在之后专门再开一贴学习线段树的用法!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值