在线查询 - 分块思想和树状数组

前言

查询分为在线和离线两种,在离线查询中数据是不进行更改操作的,可以使用一些较为暴力的方法,但是在线查询中,由于数据也需要实时变更,需要一些更为巧妙的方法降低更改操作的复杂度。本篇博客的两个部分均为在线查询,在查询的过程中亦有更新操作,分块思想中有实时插入删除操作,树状数组中有实时更新操作,那么该如何降低更新操作的复杂度呢?

Part one:分块思想

问题引入:给出一个非负整数序列A,在有可能随时添加或删除元素的情况下,实时查询序列元素第k大的数。如果用暴力,我们会发现,每次添加或者删除都需要重新进行排序,赋予每个元素新的排位,十分的麻烦,由此引入分块思想,先分为多个集合处理,找临界集合再细致到元素

一、算法思想

  1. 该题属于在线查询,如果直接暴力去做,则插入或者删除元素十分难以处理,时间复杂度高
  2. 可以把所有可能范围的值分为多块,比如最大范围为N,则可将此分为sqrt(N)上取整块,则可以知道前sqrt(N)上取整-1块中的范围都是sqrt(N)下取整
  3. 这样分有什么好处:当需要找第k大的数的时候先累加块中元素数,到临界条件后,再取临界块中一一查找块中元素即可得到该元素是什么
  4. 具体如何实现:开一个block数组存块中元素,开一个table数组存每个元素个数

二、算法代码

问题描述:给出一个栈,实现Push(入栈)、Pop(出栈,并输出出栈的数)、PeekMedian(输出栈中间大小的数),当没有元素的时候Pop和PeekMedian都应该输出Invalid(范围:10e5)

#include<bits/stdc++.h>

using namespace std;

const int maxn = 1e5 + 10;
const int sqrN = 316; //sqrt(1e5+1)下取整

int block[sqrN];
int table[maxn];
stack<int> st;

void peekMedian(int k)
{
	int sum = 0;
	int idx = 0;
	while(sum + block[idx] < k)
		sum += block[idx ++ ];
	int num = idx * sqrN;
	while(sum + table[num] < k)
		sum += table[num ++ ];
	cout << num << endl;
}

void Push(int x)
{
	st.push(x);
	block[x / sqrN] ++ ;
	table[x] ++ ;
}

void Pop()
{
	int x = st.top();
	st.pop();
	block[x / sqrN] -- ;
	table[x] -- ;
	cout << x << endl;
}

int main()
{
	ios_base::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	
	int x, n;
	memset(block, 0, sizeof(block));
	memset(table, 0, sizeof(table));
	string cmd;
	cin >> n;
	while(n -- )
	{
		cin >> cmd;
		if(cmd.compare("Push") == 0)
		{
			cin >> x;
			Push(x);
		}
		else if(cmd.compare("Pop") == 0)
		{
			if(st.empty() == true) cout << "Invalid" <<endl;
			else Pop();
		}
		else
		{
			if(st.empty() == true) cout << "Invalid" <<endl;
			else
			{
				int k = (st.size() + 1) / 2;
				peekMedian(k);
			}
		}
	}
	return 0;
}

Part two:树状数组

问题引入:对于一个数组,查询区间和,且在此过程中有更新操作,比如将数组某个元素增加一定的值。我们可以看出,如果用前缀和思想来暴力做,每次元素更新后需要更新的前缀和十分多,时间复杂度很高,所以我们可以采取树状数组的策略,下面将会讲述树状数组是什么以及如何用(单点更新,区间查询)

一、算法思想

  1. lowbit(x) = x & -x,等价于取x的二进制最右边的1和它右边所有的0,结果一定是2的倍数(需要自己去define)
  2. 什么是树状数组:树状数组是用来记录和的数组,它存放i以及i前lowbit(i) - 1个整数的和,即树状数组一个单元C[i]的覆盖长度为lowbit(i)
  3. 为了得出某个区间和,我们需要实现函数getSum(x),得到前x个整数的和;为了实现在线查询进行实时更改,我们需要实现函数update(x,v),在第x个数上加上v
  4. getSum(x):我们可以从此处往前遍历,每次覆盖lowbit(i)即可,由此不断更新i
  5. update(x,v):我们可以从此处往后遍历,由于要使得跨越的范围a满足,lowbit(x+a)>lowbit(x)才可以覆盖,显然最小的a == lowbit(x)

二、算法代码

问题描述:给定一个有N个正整数的序列A(N<=10e5,A[i]<=10e5),对序列中的每个数,求出序列中它右边比它小的数的个数

  • 非离散:
#include<bits/stdc++.h>
#define lowbit(i) ((i)&(-i))

using namespace std;

const int maxn = 100010;

int c[maxn]; //树状数组

void update(int x, int v)
{
	for(int i = x; i < maxn; i += lowbit(i))
		c[i] += v;
}

int getSum(int x)
{
	int sum = 0;
	for(int i = x; i > 0; i -= lowbit(i))
		sum += c[i];
	return sum;
}

int main()
{
	ios_base::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	
	int n, x;
	cin >> n;
	memset(c, 0, sizeof(c));
	for(int i = 0; i < n; i ++ )
	{
		cin >> x;
		update(x, 1);
		cout << getSum(x - 1) << endl;
	}
	
	return 0;
}
  • 离散化:
  • 如果A[i]范围改为<=10e9,数据太大,会导致c树状数组爆内存,所以可以将数组离散化存储,核心思想就是将原本十分发散的下标聚集起来,在此题中离散处理输入后用了一个新的数组A来存储输入(非离散情况,值即为下标非常发散,离散化后将下标聚集)
#include<bits/stdc++.h>
#define lowbit(i) ((i)&(-i))

using namespace std;

const int maxn = 100010;

int a[maxn], c[maxn]; //离散化后数组和树状数组

struct Node
{
	int val; //元素值,在非离散情况下是下标
	int pos; //下标,离散化赋予其新下标
	
	bool operator < (const Node &W)const //重载小于号
	{
		return val < W.val;
	}
} temp[maxn];

void update(int x, int v)
{
	for(int i = x; i < maxn; i += lowbit(i))
		c[i] += v;
}

int getSum(int x)
{
	int sum = 0;
	for(int i = x; i > 0; i -= lowbit(i))
		sum += c[i];
	return sum;
}

int main()
{
	ios_base::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	
	int n;
	cin >> n;
	memset(c, 0, sizeof(c));
	
	//将输入离散化
	for(int i = 0; i < n; i ++ )
	{
		cin >> temp[i].val;
		temp[i].pos = i;
	}
	
	sort(temp, temp + n);
	
	for(int i = 0; i < n; i ++ )
	{
		if(i == 0 || temp[i].val != temp[i - 1].val)
			a[temp[i].pos] = i + 1;
		else 
			a[temp[i].pos] = a[temp[i - 1].pos];
	}
	
	//树状数组操作区间
	for(int i = 0; i < n; i ++ )
	{
		update(a[i], 1);
		cout << getSum(a[i] - 1) << endl;
	}
	
	return 0;
}

三、算法进阶

  • 上面我们做到了单点更新、区间查询,如果想要进行区间更新、单点查询该如何做呢?
  • 上面我们树状数组存的是区间和,此处我们存区间更新累加值,即这一段被加了多少值
    • 单点查询getSum(x):即可直接套用上一题的update,累加所有包含此元素的累加值进行累加
    • 区间更新update(x,v):即可套用上一题的getSum寻找完美覆盖,让此覆盖包含的几个小覆盖区间都累加即可
int getSum(int x)
{
	int sum = 0;
	for(int i = x; i < maxn; i += lowbit(i))
		sum += c[i];
	return sum;
}

void update(int x, int v)
{
	for(int i = x; i > 0; i -= lowbit(i))
		c[i] += v;
}
  • 25
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值