1. 程式人生 > 其它 >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;
}