题目
题目描述
在
2016
2016
2016 年,佳媛姐姐喜欢上了数字序列。因而
T
a
\tt{Ta}
Ta 经常研究关于序列的一些奇奇怪怪的问题,现在他在研究一个难题,需要你来帮助他。
这个难题是这样子的:给出一个 1 1 1 到 n n n 的全排列,现在对这个全排列序列进行 m m m 次局部排序,排序分为两种:
- 操作 ( 0 , l , r ) (0,l,r) (0,l,r) :表示将区间 [ l , r ] [l,r] [l,r] 的数字升序排序。
- 操作 ( 1 , l , r ) (1,l,r) (1,l,r) :表示将区间 [ l , r ] [l,r] [l,r] 的数字降序排序。
排序后询问第 q q q 位置上的数字。
数据范围与约定
对于全部的数据,
1
≤
q
,
l
,
r
≤
n
,
m
≤
1
0
5
1\le q,l,r\le n,m\le 10^5
1≤q,l,r≤n,m≤105 且
j
∈
{
0
,
1
}
j\in\{0,1\}
j∈{0,1} 。
思路壹
考虑二分答案,只检查答案是否为一个不小于 x x x 的数。
那么我们很容易发现,对于两个数字 i , j ( i , j ⩾ x ) i,j\;(i,j\geqslant x) i,j(i,j⩾x),二者的效果是相同的,可以统一用 1 1 1 表示。类似的,比 x x x 小的数字,可以统一用 0 0 0 表示,问题转化为结果的第 q q q 位是否为 1 1 1,那么两个数字 1 1 1 是否需要交换,就无足轻重了。
对 0 / 1 0/1 0/1 序列排序,可以用线段树统计 1 1 1 的数量,然后进行区间赋值。时间复杂度 O ( n log 2 n ) \mathcal O(n\log^2 n) O(nlog2n) 。
代码壹
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
inline int readint(){
int a = 0, f = 1; char c = getchar();
for(; c<'0' or c>'9'; c=getchar())
if(c == '-') f = -1;
for(; '0'<=c and c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
const int MaxN = 100000;
int n, m, __BOUND, a[MaxN], q;
class SegmentTree{
struct Node{
int l, r, cnt;
}node[MaxN<<2];
void modifyNode(int pos,int v){
node[pos].cnt = (node[pos].r-node[pos].l+1)*v;
}
void pushUp(int pos){
node[pos].cnt = node[pos<<1].cnt+node[pos<<1|1].cnt;
}
void pushDown(int pos){
if(node[pos].cnt != node[pos].r-node[pos].l+1 and node[pos].cnt)
return ;
int v = node[pos].cnt/(node[pos].r-node[pos].l+1);
modifyNode(pos<<1,v), modifyNode(pos<<1|1,v);
}
public:
void buildTree(int l=1,int r=n,int pos=1){
node[pos].l = l, node[pos].r = r;
if(l != r){
buildTree(l,(l+r)>>1,pos<<1);
buildTree((l+r)/2+1,r,pos<<1|1);
pushUp(pos);
}else
node[pos].cnt = (a[l] >= __BOUND);
}
int count(int l,int r,int pos=1){
if(l <= node[pos].l and node[pos].r <= r)
return node[pos].cnt;
pushDown(pos);
int mid = (node[pos].l+node[pos].r)>>1;
if(r <= mid) return count(l,r,pos<<1);
if(l > mid) return count(l,r,pos<<1|1);
return count(l,r,pos<<1)+count(l,r,pos<<1|1);
}
void modifyRange(int l,int r,int v,int pos=1){
if(l <= node[pos].l and node[pos].r <= r)
return modifyNode(pos,v);
pushDown(pos);
int mid = (node[pos].l+node[pos].r)>>1;
if(l <= mid) modifyRange(l,r,v,pos<<1);
if(r > mid) modifyRange(l,r,v,pos<<1|1);
pushUp(pos);
}
void SORT(int l,int r,int d){
int ppl = count(l,r);
if(not ppl or ppl == r-l+1) return ;
if(d == 1){ // dropping
modifyRange(l,l+ppl-1,1);
modifyRange(l+ppl,r,0);
}else{ // rising
modifyRange(r-ppl+1,r,1);
modifyRange(l,r-ppl,0);
}
}
}ppl;
struct Command{
int TYPE, L, R;
void input(){
TYPE = readint();
L = readint(), R = readint();
}
}zxy[MaxN];
void init(){
n = readint(), m = readint();
for(int i=1; i<=n; ++i)
a[i] = readint();
for(int i=0; i<m; ++i)
zxy[i].input();
q = readint();
}
bool check(int mid){
__BOUND = mid; ppl.buildTree();
for(int i=0; i<m; ++i)
ppl.SORT(zxy[i].L,zxy[i].R,zxy[i].TYPE);
return ppl.count(q,q) == 1;
}
void solve(){
int L = 1, R = n, mid;
while(L != R){
mid = (L+R+1)>>1;
if(check(mid)) L = mid;
else R = mid-1;
}
printf("%d\n",L);
}
int main(){
init();
solve();
return 0;
}
思路贰
不断对序列的一部分进行排序,无论如何,你总觉得它是 努力朝着有序的方向进行。
更具体地说,序列中肯定会有很多个子区间,是已经有序的——我们的操作就是在创造这种子区间啊。每次操作,最多会让边缘的两个子区间被拦腰截断,中间的子区间却全部合并为一。所以子区间的数量变化是 O ( n + m ) \mathcal O(n+m) O(n+m) 的!
我们接下来只需要实现:有序数列合并(或者说是集合的合并)与分裂(将前 k k k 小提取出来)。
一个最 n a i v e \rm naive naive 的想法是,用平衡树维护。然而它的合并似乎与 s i z e size size 有关?复杂度会不会假啊?
请出我们的主角,线段树合并。显然权值线段树可以维护集合,也可以提取前 k k k 小。更重要的是,它的时间复杂度很容易证明!
经典的势函数——节点数量。合并时 O ( 1 ) \mathcal O(1) O(1) 的代价降低 1 1 1 的势能;一次分裂是 O ( log n ) \mathcal O(\log n) O(logn) 复杂度与 log n \log n logn 的势能增加,总复杂度 O ( n log n ) \mathcal O(n\log n) O(nlogn) 。
更恐怖的是,它甚至支持在线多次查询……听说外层套上平衡树,还可以支持查询区间信息……
代码贰
#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
#include <map>
using namespace std;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
typedef long long int_;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
inline void writeint(int x){
if(x > 9) writeint(x/10);
putchar((x-x/10*10)^48);
}
inline int ABS(const int &x){
return x < 0 ? -x : x;
}
const int MaxN = 200005;
int n;
namespace SgTree{
const int MaxM = MaxN*40;
int ch[MaxM][2], v[MaxM], cntNode;
void insert(int qid,int &o,int l=1,int r=n){
if(!o) o = ++ cntNode;
++ v[o]; if(l == r) return ;
if(qid <= ((l+r)>>1))
insert(qid,ch[o][0],l,(l+r)>>1);
else insert(qid,ch[o][1],(l+r)/2+1,r);
}
void merge(int &o,const int &fr,int l=1,int r=n){
if(!o || !fr) return void(o ^= fr);
v[o] += v[fr]; if(l == r) return ;
merge(ch[o][0],ch[fr][0],l,(l+r)>>1);
merge(ch[o][1],ch[fr][1],(l+r)/2+1,r);
}
void split(int o,int &x,int &y,int k,int l=1,int r=n){
if(!o) return void(x = y = 0);
y = ++ cntNode, x = o;
v[y] = v[o]-k; v[x] = k; // goal
if(k > v[ch[o][0]]) k -= v[ch[o][0]],
split(ch[o][1],ch[x][1],ch[y][1],k);
else{
swap(ch[x][1],ch[y][1]);
if(k != v[ch[o][0]])
split(ch[o][0],ch[x][0],ch[y][0],k);
}
}
int kthElement(const int &o,int k,int l=1,int r=n){
if(l == r) return l;
if(k <= v[ch[o][0]])
return kthElement(ch[o][0],k,l,(l+r)/2);
else k -= v[ch[o][0]];
return kthElement(ch[o][1],k,(l+r)/2+1,r);
}
}
using namespace SgTree;
map<int,int> mp; // positive: increasing
map<int,int>::iterator posAt(int r){
auto it = mp.lower_bound(r+1);
int nxt = it->first; -- it;
if(it->first == r) return it;
int pre = it->first, &o = mp[pre];
if(o > 0) split(o,o,mp[r],r-pre);
else{
split(-o,mp[r],o,nxt-r);
o = -o, mp[r] = -mp[r];
}
return mp.find(r);
}
int main(){
n = readint();
int m = readint();
for(int i=1; i<=n; ++i)
insert(readint(),mp[i]);
mp[n+1] = 0; // avoid end()
for(int opt,l; m; --m){
opt = readint();
auto L = posAt(l = readint());
auto R = posAt(readint()+1);
int o = ABS(L->second);
for(auto i=L; (++i)!=R; )
merge(o,ABS(i->second));
mp.erase(++L,R); // (L,R)
mp[l] = (1-2*opt)*o;
}
int q = readint(); posAt(q+1);
auto ans = posAt(q);
int o = ans->second;
writeint(kthElement(ABS(o),1));
putchar('\n');
return 0;
}
后记
最近一直在想,为啥线段树合并就可做呢?本质上线段树不也是平衡树的一种吗?
一种解释是,线段树通过非叶子节点,把树的形态固定了下来,于是就可以快速合并。非叶子节点撑起了线段树的结构,缺点就是舍弃了 任意有偏序关系的元素的在线插入 能力。当然它也失去了一些复杂的区间操作能力,比如翻转。
也就是说整数域是线段树恒优咯?