1. 程式人生 > 實用技巧 >nflsoj705 【2020五校聯考NOIP #5】Warrior

nflsoj705 【2020五校聯考NOIP #5】Warrior

題目來源:南京外國語學校 OJ,2020五校聯考NOIP 模擬賽題,nflsoj705 【2020五校聯考NOIP #5】Warrior。作者:長郡中學。

題目大意

題目連結

菜菜的 \(\text{Jayce}\) 有一個長為 \(n\) 的序列 \(\{S\}\)

菜菜的 \(\text{Jayce}\) 拿出了這個序列的每一個連續子序列(共 \(\frac{n(n+1)}{2}\) 個),對於每一個都從小到大排序,轉化成一個數組。菜菜的 \(\text{Jayce}\) 想知道字典序第 \(k\) 小的那個陣列。

我們說一個數組 \(A\) 比陣列 \(B\) 字典序小,當且僅當對於兩個可重集中出現次數不同的最小元素 \(x\)

,元素 \(x\)\(A\) 中出現次數更多。

友情提醒:請注意本題中字典序的定義。

資料範圍:\(1\leq S_i\leq n\leq 10^5\)\(1\leq k\leq \frac{n(n+1)}{2}\)

本題題解

核心觀察:如果固定 \(l\),則隨著 \(r\) 的增大,\([l,r]\) 的字典序單調下降。

推論:如果確定了一個區間 \([l,r]\),可以使用雙指標 + 線段樹在 \(O(n\log n)\) 的時間內確定字典序比它更小的那些區間。即,對於每個左端點 \(i\),求出一個 \(\text{curR}_i\),表示當右端點 \(\in [i,\text{curR}_i]\)

時字典序大於等於 \([l,r]\),右端點 \(\in [\text{curR}_i +1,n]\) 時字典序小於 \([l,r]\)

具體來說,把線段樹建在值域上,用它維護一個序列 \(w_{1\dots\text{maxv}}\)。初始時,\(\forall v:w_{v}=-\sum_{j=l}^{r}[a_j = v]\),即 \(v\) 在區間 \([l,r]\) 裡的出現次數的相反數。在使用雙指標的過程中,每加入一個數,就令線段樹對應位置的 \(w\)\(+1\),刪除則 \(-1\)。因此,線段樹需要支援單點修改(\(\pm1\))。用它維護全域性第一個 \(w_p\neq 0\)

的位置 \(p\)。我們只要看 \(w_p\)\(<0\) 還是 \(>0\),就能確定當前區間的字典序是大於 \([l,r]\) 還是小於 \([l,r]\)

現在,對於任意 \([l,r]\),我們可以 \(O(n\log n)\) 求出它的排名。考慮二分答案。因為一開始,我們並不知道所有 \(\frac{n(n+1)}{2}\) 個區間的單調性(廢話,如果知道了不就直接輸出答案了),所以只能對每個左端點 \(l\) 分別二分(因為根據“核心觀察”,當確定左端點後,就有單調性了)。這樣做的時間複雜度是 \(O(n^2\log^2 n)\)

繼續優化,需要用到一種非常巧妙的技巧:隨機化二分。考慮隨機一個區間 \([l,r]\),用上述方法求出它的排名:

  • 如果剛好第 \(k\) 名,就相當於已經求出答案了。
  • 如果排名小於 \(k\),那麼排名比它還小的區間(也就是所有 \(l = i,r\in [\text{curR}_i+1,n]\)),就絕不會成為答案。可以將它們“刪除”(即下次隨機時不隨它們了)。
  • 如果排名大於 \(k\),那麼排名比它還大的區間(也就是所有 \(l=i,r\in[i,\text{curR}_i]\)),就絕不會成為答案。可以將它們“刪除”。

具體來說,對每個左端點 \(i\),我們維護兩個值 \(L_i,R_i\),表示右端點 \(\in[L_i,R_i]\) 的區間還沒有被刪除。

這樣,隨機二分時,每次隨機出一個未被刪除的區間 \([l,r]\),用 \(O(n\log n)\) 的時間,求出它的排名,以及 \(\text{curR}_{1\dots n}\) 陣列,然後更新所有 \(L_i,R_i\) 的值。隨機時,每次期望刪掉一般的區間,所以期望隨機次數是 \(\log(\frac{n(n+1)}{2})=O(\log n)\)

總時間複雜度 \(O(n\log^2n)\)

參考程式碼

提交時建議加上讀入優化,詳見本部落格公告。

// problem: nflsoj705
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;

template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }

#ifdef ONLINE_JUDGE
	const int RAND_SHIFT = 31;
#else
	const int RAND_SHIFT = 15;
#endif
inline int Rand(int l, int r) {
	return (((ll)rand() << RAND_SHIFT) + rand()) % (r - l + 1) + l;
}

const int MAXN = 1e5;
ll K;
int n, a[MAXN + 5], L[MAXN + 5], R[MAXN + 5], alive[MAXN + 5], cnt_alive;
int buc[MAXN + 5], cur_r[MAXN + 5], eq[MAXN + 5];
struct SegmentTree {
	int f[MAXN * 4 + 5];
	void push_up(int p) {
		f[p] = (f[p << 1] != 0 ? f[p << 1] : f[p << 1 | 1]);
	}
	void build(int p, int l, int r) {
		if(l == r) {
			f[p] = buc[l];
			return;
		}
		int mid = (l + r) >> 1;
		build(p << 1, l, mid);
		build(p << 1 | 1, mid + 1, r);
		push_up(p);
	}
	void modify(int p, int l, int r, int pos, int v) {
		if(l == r) {
			buc[l] += v;
			f[p] = buc[l];
			return;
		}
		int mid = (l + r) >> 1;
		if(pos <= mid) {
			modify(p << 1, l, mid, pos, v);
		} else {
			modify(p << 1 | 1, mid + 1, r, pos, v);
		}
		push_up(p);
	}
	SegmentTree(){}
}T;

bool solve(int l, int r) {
	// cerr << "solve " << l << " " << r << endl;
	ll cnt_front = 0; // 字典序嚴格小於 [l, r] 的區間有多少個
	ll cnt_equal = 0; // 字典序等於 [l, r] 的區間有多少個
	memset(buc, 0, sizeof(int) * (n + 1));
	for(int i = l; i <= r; ++i) {
		buc[a[i]]--;
	}
	T.build(1, 1, n);
	for(int i = 1, j = 0; i <= n; ++i) {
		// i: 左端點
		// j: 以 i 為左端點時, 最後一個字典序大於等於 [l, r] 的右端點
		if(j < i - 1) {
			assert(j == i - 2);
			++j;
		} else if(i != 1) {
			T.modify(1, 1, n, a[i - 1], -1);
		}
		eq[i] = 0;
		while(j + 1 <= n) {
			++j;
			T.modify(1, 1, n, a[j], 1);
			if(T.f[1] == 0) {
				eq[i] = 1;
				break;
			}
			if(T.f[1] > 0) {
				T.modify(1, 1, n, a[j], -1);
				--j;
				break;
			}
		}
		// cerr << j << endl;
		cur_r[i] = j;
		cnt_front += n - j;
		cnt_equal += eq[i];
	}
	// cerr << cnt_front << " " << cnt_equal << endl;
	if(cnt_front < K && cnt_front + cnt_equal >= K) {
		return true;
	}
	if(cnt_front < K) {
		for(int i = 1; i <= n; ++i) {
			ckmin(R[i], cur_r[i] - eq[i]);
		}
	} else {
		for(int i = 1; i <= n; ++i) {
			ckmax(L[i], cur_r[i] + 1);
		}
	}
	return false;
}
int main() {
	srand((ull)time(0) ^ (ull)(new char));
	cin >> n >> K;
	for(int i = 1; i <= n; ++i) {
		cin >> a[i];
		L[i] = i;
		R[i] = n;
		alive[++cnt_alive] = i;
	}
	int l, r;
	do {
		for(int i = 1; i <= cnt_alive; ++i) {
			if(L[alive[i]] > R[alive[i]]) {
				// 刪除
				if(i != cnt_alive)
					swap(alive[i], alive[cnt_alive]);
				--cnt_alive; --i;
			}
		}
		assert(cnt_alive >= 1);
		l = alive[Rand(1, cnt_alive)];
		r = Rand(L[l], R[l]);
	} while(!solve(l, r));
	
	sort(a + l, a + r + 1);
	for(int i = l; i <= r; ++i) cout << a[i] << " ";
	cout << endl;
	return 0;
}