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\)
友情提醒:請注意本題中字典序的定義。
資料範圍:\(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]\)
具體來說,把線段樹建在值域上,用它維護一個序列 \(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\)
現在,對於任意 \([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;
}