Java教程

Rmp(Mex) & Destiny & 楼房重建(线段树上二分)

本文主要是介绍Rmp(Mex) & Destiny & 楼房重建(线段树上二分),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Rmq Problem / mex

题面

解析

分块很容易想,不过这道题的需要靠脸卡常。
于是考虑维护一个可持久化值域线段树,树上维护每个值最后一次出现的位置,每个版本作时间维,即表示序列的前 \(i\) 个。
所以我们直接在询问区间的右端点的版本对应的线段树上找到最小的最后一次出现的位置小于询问区间的左端点的点。
在线段树上二分即可,即每个维护区间信息的最小值,如果当前区间的左儿子的最小值小于询问区间的左端点,则答案肯定在左儿子的区间内,递归进入左儿子,因为必有解,所以否则答案肯定在右儿子对应的区间内,递归进入右儿子对应的区间即可。
因为答案肯定是 \(0\) 或 \(a_i + 1\),所以离散化时应把 \(0\) 和 \(a_i + 1\) 也加入进来。

代码

#include<cstdio>
#include<cctype>
#include<algorithm>

using namespace std;

const int N = 2e5 + 5, M = N << 5;

int n, m, a[N], b[N << 1], rt[N], len = 0;

int ls[M], rs[M], val[M], tot = 0;

void update(int pre, int &p, int l, int r, int pos, int k) {
	p = ++tot;
	ls[p] = ls[pre], rs[p] = rs[pre];
	if(l == r) return (void)(val[p] = k);
	int mid = (l + r) >> 1;
	if(pos <= mid) update(ls[pre], ls[p], l, mid, pos, k);
	else update(rs[pre], rs[p], mid + 1, r, pos, k);
	val[p] = min(val[ls[p]], val[rs[p]]);
}

int query(int p, int l, int r, int k) {
	if(!p || l == r) return l;
	int mid = (l + r) >> 1;
	if(val[ls[p]] < k) return query(ls[p], l, mid, k);
	else return query(rs[p], mid + 1, r, k);
}

inline void read(int &x) {
	x = 0; int c = getchar();
	for(; !isdigit(c); c = getchar());
	for(; isdigit(c); c = getchar())
		x = x * 10 + c - 48;
}

int main() {
	read(n), read(m); b[++len] = 0;
	for(int i = 1; i <= n; i++) read(a[i]), b[++len] = a[i], b[++len] = a[i] + 1;
	sort(b + 1, b + 1 + len);
	len = unique(b + 1, b + 1 + len) - b - 1;
	for(int i = 1; i <= n; i++) {
		a[i] = lower_bound(b + 1, b + 1 + len, a[i]) - b;
		update(rt[i - 1], rt[i], 1, len, a[i], i);
	}
	for(int i = 1, l, r; i <= m; i++) {
		read(l), read(r);
		printf("%d\n", b[query(rt[r], 1, len, l)]);
	}
	return 0;
}

Destiny

题面

解析

做法类似于上一题,也就是直接在线段树上二分即可。
维护每个值在当前版本的出现次数,那么一个区间内的每个值的出现次数直接用询问区间的右端点对应的版本减去左端点减一对应的版本即可。
线段树上维护的是区间出现次数的和,因此二分的时候直接像上一题那样就会出现错误。
最直接的思路肯定是当前区间的左儿子出现次数的和大于 \(\frac{r - l + 1}{k}\) 就递归进入左儿子,否则递归进入右儿子,但是因为我们保存的是区间和,但其实可能左儿子的每个叶子节点的值都不超过 \(\frac{r - l + 1}{k}\),所以就会出现无解的情况,这时我们返回 \(-1\),所以对于递归进入左儿子的返回值特判一下,如果大于 \(0\) (\(0\) 也是不可能的答案,判大于 \(0\) 避免出锅),我们就返回左儿子的答案,否则递归进入右儿子,但同样也会有无解的情况,同样采用刚才的方法,如果右儿子也无解,当前区间也返回 \(-1\)。
根据我们的递归方法,显然有解的话,一定是最小的,满足题意。

代码

/*I AK IOI*/
#include<cstdio>
#include<cctype>

using namespace std;

const int N = 3e5 + 5, M = N << 5;

int n, m, a[N], rt[N];

int ls[M], rs[M], val[M], tot = 0;

void update(int pre, int &p, int l, int r, int pos) {
	p = ++tot;
	val[p] = val[pre] + 1, ls[p] = ls[pre], rs[p] = rs[pre];
	if(l == r) return ;
	int mid = (l + r) >> 1;
	if(pos <= mid) update(ls[pre], ls[p], l, mid, pos);
	else update(rs[pre], rs[p], mid + 1, r, pos);
}

int query(int pre, int p, int l, int r, int k) {
	if(l == r) return l;
	int mid = (l + r) >> 1, ans;
	if(val[ls[p]] - val[ls[pre]] > k)
		if((ans = query(ls[pre], ls[p], l, mid, k)) > 0) return ans;
	if(val[rs[p]] - val[rs[pre]] > k)
		if((ans = query(rs[pre], rs[p], mid + 1, r, k)) > 0) return ans;
	return -1;
}

inline void read(int &x) {
	x = 0; int c = getchar();
	for(; !isdigit(c); c = getchar());
	for(; isdigit(c); c = getchar())
		x = x * 10 + c - 48;
}

int main() {
	read(n), read(m);
	for(int i = 1; i <= n; i++) {
		read(a[i]);
		update(rt[i - 1], rt[i], 1, n, a[i]);
	}
	for(int i = 1, l, r, k; i <= m; i++) {
		read(l), read(r), read(k);
		k = (r - l + 1) / k;
		printf("%d\n", query(rt[l - 1], rt[r], 1, n, k));
	}
	return 0;
}

楼房重建

题面

解析

也是一道经典的线段树二分的题。
根据题意将斜率转化为序列的值之后,用讲课老师的话来说就是询问区间不退栈的单点递增的栈的大小。
就是如果当前斜率大于栈顶元素就入栈,否则就跳过。
其实就等价于求斜率比之前位置的斜率都大的位置的个数,因为满足这个条件才不会被前面的遮住,而后面的则不用管。
所以这道题的难点就在于区间信息的合并上了。
当我们合并左右两个区间时,根据那个栈的性质,栈中已保存了左区间的信息,而我们要做的就是把右区间的元素加进去即可,当然这个栈和加元素都是虚拟的,我们只用求出有多少个元素可以被加入其中即可。
所以我们二分右区间,即当前区间的右儿子。
维护一个区间最大值,和区间这个栈的大小 \(tot\)。
如果当前递归到的区间的最大值都小于了最开始的当前区间的左儿子的最大值,那肯定不会有元素能加进去。
如果当前递归到的区间的左端点的值大于了最开始的当前区间的左儿子的最大值,那么显然当前区间的已经处理出来了的 \(tot\) 个元素都可以被加进去,因为这个左端点的值对于整个当前递归到的区间的约束更强。
叶子节点直接判这个点能不能加进去即可。
如果当前递归到的区间的左儿子的最大值小于等于最开始的当前区间的左儿子的最大值,那么整个左儿子对应的区间对答案不会有贡献,所以直接进入右儿子,否则进入左儿子,因为左儿子对右儿子的约束更强,所以右儿子对答案的贡献直接加上一个当前递归到的区间的 \(tot\) 减去左儿子的 \(tot\) 即可,注意不能是右儿子的 \(tot\),因为右儿子的 \(tot\) 的起始点是右儿子的区间的左端点,而我们现在计算对答案的贡献时,起始点是当前这个区间的左端点,因为左儿子的左端点也是这个点,所以两者相减,才是以这个点为起始点考虑的答案。

代码

#include<cstdio>
#include<iostream>
#include<cstring>

using namespace std;

const int N = 1e5 + 5;

int n, m; double a[N];

struct SegmenTree {
	#define M N << 2
	double Max[M]; int tot[M];
	inline int pushup(double lm, int p, int L, int R) {
		if(Max[p] <= lm) return 0;
		if(a[L] > lm) return tot[p];
		if(L == R) return a[L] > lm;
		int mid = (L + R) >> 1;
		if(Max[p << 1] <= lm) return pushup(lm, p << 1 | 1, mid + 1, R);
		else return pushup(lm, p << 1, L, mid) + tot[p] - tot[p << 1];
	}
	inline void update(int p, int l, int r, int x) {
		if(l == r) {
			Max[p] = a[l];
			tot[p] = 1; return ;
		}
		int mid = (l + r) >> 1;
		if(x <= mid) update(p << 1, l, mid, x);
		else update(p << 1 | 1, mid + 1, r, x);
		Max[p] = max(Max[p << 1], Max[p << 1 | 1]);
		tot[p] = tot[p << 1] + pushup(Max[p << 1], p << 1 | 1, mid + 1, r);
	}
}tr;

int main() {
	scanf("%d%d", &n, &m);
	for(int i = 1, x, y; i <= m; i++) {
		scanf("%d%d", &x, &y);
		a[x] = 1.0 * y / x;
		tr.update(1, 1, n, x);
		printf("%d\n", tr.tot[1]);
	}
	return 0;
}
这篇关于Rmp(Mex) & Destiny & 楼房重建(线段树上二分)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!