数据结构学习之整体二分&&bzoj2738矩阵乘法题目分析

数据结构学习之整体二分

前言:

整体二分是一个很神的东西,特点是代码短小精悍而且能解决一系列神数据结构的题目,他的来源是整体二分,思想从贡献下手,是一个非常好用的东东。

从二分答案到整体二分:

二分答案想必大家都熟悉,对于一个询问,若我们已知答案区间l,r,我们二分出一个答案mid;如果mid恰好符合答案则结束程序,否则判断答案落在(l,mid)还是(mid,r)继续二分下去。当然我们要求答案是可二分的。

二分答案单次的复杂度为O(f(n)logn)其中f(n)为判定答案合法性的复杂度

如果对于很多询问,显然是会爆炸的

所以,我们希望有一种算法可以支持多询问且复杂度为log。

因此有了整体二分

整体二分:整体?二分?

整体二分承袭了二分答案的思想,对答案进行二分,可是相对于二分答案,我们多了很多很多的询问,这个时候,我们不得不对询问区间进行处理。我们从询问的角度考虑二分答案,他其实把每次二分都把单个询问扔到了左区间或右区间中。那么我们现在搞的是大事情,我们要把整体进行二分,因此我们选择,把整个询问序列进行操作,把大于当前mid的询问扔到后面,小于mid的答案扔到前面进行,然后递归。来一段伪代码

Solve(Q, L, R) { //Q为待询问序列,L,R为答案区间 
	if(L > R) return;
	if(L == R) get_ans(Q); //将Q中询问答案置为L
	int mid = L + R >> 1; //二分答案
	for(Q) { //枚举每个Q中的询问,判断每个询问属于的区间
		int value = get_f(cur); //计算当前询问答案
		Dived(cur) //根据当前答案划分其答案区间 
	} 
	get_new(Q) //更新划分后的询问序列
	Solve(S0, L, mid); Solve(Q - S0, mid + 1, R) //分治处理 
}

然而我们意识到了一个严重的问题,我们在get_f的函数时其复杂度往往是O(n)的,因此我们只是将二分答案合起来写了而已,时间复杂度并没有任何变化。

修改操作:神奇的贡献值

我们考虑二分答案的时候,我们验证合法性常常需要一定的限定性。拿K小数来说,假设答案是mid,那么我们只计算小于mid的数的个数。此时,我们发现,二分的答案每变化一次,之前计算过的所有mid值都可以计入答案,我们只需要计算比mid更大数的数值。换一个更专业的说法,就是(l,mid)的数据对答案的贡献值可以传递到(mid, r)中。所以,我们可以在划分区间的时候,我们首先加入mid之前的所有数据,并计算所有数据对答案的贡献值,同时划分数据。对于(l,mid)我们不做处理,对于(mid, r)我们更新他们的当前贡献值。之后照常划分就可以了。这样我们成功省去了冗余的计算。我们对之前的伪代码修改一下。

Solve(Q, L, R) { //Q为待询问序列,L,R为答案区间 
	if(L > R) return;
	if(L == R) get_ans(Q); //将Q中询问答案置为L
	int mid = L + R >> 1; //二分答案
	for(A) Change(cur); 	//对于输入数据区间A,修改所有满足mid限制条件的A的贡献值,并用某个数据结构维护 
	for(Q) { //枚举每个Q中的询问,判断每个询问属于的区间
		int value = query(cur); //计算当前询问答案
		Dived(cur) //根据当前答案划分其答案区间 
		Update(cur) //更新询问的当前贡献值 
	} 
	for(A) Rechange(cur); //还原数据A 
	get_new(Q) //更新划分后的询问序列
	Solve(S0, L, mid); Solve(Q - S0, mid + 1, R) //分治处理 
}

这个代码的关键部分是,query的复杂度变了,由于之前的修改操作,我们可以通过某个数据结构在比较短的时间内询问答案。因此总复杂度发生了质的改变。

顺便说一句,如果题目中有修改怎么破?很简单,我们将初始数据转化为修改操作,将所有询问和修改操作合并在一起,在计算贡献值的时候边修改边询问即可。

 

复杂度分析:

二分复杂度是logC,C为答案区间

剩下的是处理函数的复杂度,也就是修改+计算贡献值的操作复杂度,假设其复杂度为f(n)

总复杂度为O(f(n)logn)

总结:

整体二分主要是在二分答案的基础上,以贡献值的修改与传递为基础的离线数据处理利器,经典应用是K小数,常常可以起到简化数据结构的效果。

整体二分差不多学完啦!上题目!

 

2738: 矩阵乘法

Time Limit: 20 Sec   Memory Limit: 256 MB
Submit: 1647   Solved: 717
[ Submit][ Status][ Discuss]

Description

  给你一个N*N的矩阵,不用算矩阵乘法,但是每次询问一个子矩形的第K小数。

Input

 
  第一行两个数N,Q,表示矩阵大小和询问组数;
  接下来N行N列一共N*N个数,表示这个矩阵;
  再接下来Q行每行5个数描述一个询问:x1,y1,x2,y2,k表示找到以(x1,y1)为左上角、以(x2,y2)为右下角的子矩形中的第K小数。

Output

  对于每组询问输出第K小的数。

Sample Input

2 2
2 1
3 4
1 2 1 2 1
1 1 2 2 3

Sample Output

1
3

HINT

  矩阵中数字是109以内的非负整数;

  20%的数据:N<=100,Q<=1000;

  40%的数据:N<=300,Q<=10000;

  60%的数据:N<=400,Q<=30000;

  100%的数据:N<=500,Q<=60000。

Source

二维树状数组+整体二分裸题。

每个数据的贡献值,就是小于等于答案的贡献为1否则为0, 每次添加小于等于mid的数据进入树状数组,询问和查询都是log2N的,把小于mid和大于mid的分为两个区间。

总复杂度是O(nlog2NlogC),下面是代码

#include<iostream>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 550;
const int M = 66000;
int read()
{
	char ch = getchar(); int x = 0, f = 1;
	while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9') {x = x * 10 + ch - '0'; ch = getchar();}
	return x * f;
}
struct data {int x, y, val;}a[N * N];
struct Query {int x1, x2, y1, y2, K;} q[M];
int lowbit(int x) {return (-x)&x;}
bool cmp(data a, data b) {return a.val < b.val;}

int n, m, top, tail, ans[M], id[M], tmp[M], sum[N][N];
bool mark[M];
int add(int x, int y, int add) {
	for(int i = x; i <= n; i += lowbit(i))
		for(int j = y; j <= n; j += lowbit(j))
			sum[i][j] += add;
}

int query(int x, int y) {
	int ret = 0;
	for(int i = x; i; i -= lowbit(i))
		for(int j = y; j; j -= lowbit(j))
			ret += sum[i][j];
	return ret;
}

int ask(int i) {
	int x1 = q[i].x1, x2 = q[i].x2, y1 = q[i].y1, y2 = q[i].y2;
	return query(x2, y2) - query(x1 - 1, y2) - query(x2, y1 - 1) + query(x1 - 1, y1 - 1);
}

void solve(int l, int r, int L, int R) {
	if(l > r) return;
	if(L == R) return;
	int mid = L + R >> 1;
	while(a[tail + 1].val <= mid && tail < top) {++tail; add(a[tail].x, a[tail].y, 1);}
	while(a[tail].val > mid) {add(a[tail].x, a[tail].y, -1); --tail;}
	int cnt = 0;
	for(int i = l; i <= r; ++i) {
		if(ask(id[i]) >= q[id[i]].K) {mark[i] = true; ans[id[i]] = mid; ++cnt;}
		else mark[i] = false;
	}
	int l1 = l, l2 = l + cnt;
	for(int i = l;i <= r; ++i)
	if(mark[i]) tmp[l1++] = id[i];
	else tmp[l2++] = id[i];
	for(int i = l;i <= r; ++i) id[i] = tmp[i];
	solve(l, l1 - 1, L, mid); solve(l1, l2 - 1, mid + 1, R);
}

int main()
{
	n = read(); m = read();
	int mx = 0;
	for(int i = 1;i <= n; ++i) 
		for(int j = 1;j <= n; ++j) {
			a[++top].x = i; a[top].y = j; 
			mx = max(a[top].val = read(), mx);
		}
	sort(a + 1, a + top + 1, cmp);
	for(int i = 1;i <= m; ++i) {
		q[i].x1 = read(); q[i].y1 = read(); q[i].x2 = read(); 
		q[i].y2 = read(); q[i].K = read();
	}
	for(int i = 1;i <= m; ++i) id[i] = i;
	solve(1, m, 0, mx + 1);
	for(int i = 1;i <= m; ++i) printf("%d\n", ans[i]);
	return 0;
}

参考博客:

啃论文

巨佬的介绍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值