P4137 Rmq Problem / mex
知识点:线段树, 离线操作
原题面
题目要求 :
给定一长度为 \(n\) 的序列 \(a\) ,
给定 \(m\) 次询问 , 每次询问 给定一区间, 询问 "区间内最小的 未出现的 自然数"
分析题意 :
2019-12-20前 莫队可直接卡过
莫队太不优美 , 有一种 \(O((n + m)\log n)\) 的优美做法
-
对于区间 \([l, l] \sim [l, n]\) 中的答案 ,
左端点固定 , 随着右端点 右移 , 会有更多的数加入到 区间中
显然 , 区间 \([l, l] \sim [l, n]\) 中 答案单调不递减 -
假设已知 区间 \([l, r]\) 内的答案, 欲推得 \([l + 1, r]\) 区间内的答案 :
-
若 区间 \([l + 1, r]\) 内不存在 数 \(a[l]\) , 且原答案 \(> a[l]\)
则 区间 \([l + 1, r]\) 内答案可更新为 \(a[l]\) -
由上可得 ,
将 左端点右移 \(1\) 后, 会被影响的区间中 必然不存在 数 \(a[l]\)
显然, 被影响的区间为 : \([l + 1, l + 1] \sim [l + 1, l + next[l] - 1]\)- 其中, \(next[l]\) 代表 \(a[l]\) 在\([l + 1, n]\) 中第一个 出现的位置
-
若已知 区间 \([l, l] \sim [l, n]\) 中的答案,
且预处理出 上述的 \(next\) 数组,
即可 通过取最小值的方式 , 来获得 区间 \([l + 1, l + 1] \sim [l + 1, l + next[l] - 1]\) 中的答案
-
-
若按照上述方法 , 通过左端点右移, 来求得 不同区间答案的方法
需要满足 询问左端点单调不降 .
则可 离线处理询问 , 记录并排序后 , 再回答询问
算法实现:
由上 , 需要一种 支持查询 动态区间最小值 , 区间修改的数据结构
可使用 线段树进行维护
-
按照上述要求 , 则线段树的叶节点 存储的答案 , 表示以对应点作为右端点的答案.
即: [当前左端点 \(\sim\) 叶节点维护点] 的答案 , -
对于全局左端点 左移, 获得 区间 \([l + 1, l + 1] \sim [l + 1, l + next[l] - 1]\) 中的答案
有两种处理方法 :
1.由于 区间 \([l + 1, l] \sim [l + 1, n]\) 中 答案单调不递减
则可以进行线段树二分 , 求得第一个答案 \(> a[l]\) 的位置
并将之后的位置进行修改2.个人的 \(sb\) 做法:
对每一个区间, 维护其中 最大的答案 及 最小的答案- 若更新值 \(<\) 最小答案时, 将整个区间进行更新
- 若不满足\(1\) , 若更新值 \(>\) 最大值 , 则整个区间都不需要更新 , 直接 \(return;\)
- 若不满足 \(1,2\) , 继续向下深入
-
每次查询时 , 先将当前全局 左端点右移至查询左端点 ,
再直接查询 对应右端点储存的答案即可
单次左端点右移 与查询 复杂度都为 \(O(\log n)\), 则总复杂度为 \(O((n + m)\log n)\)
#include <cstdio>
#include <algorithm>
#include <map>
#include <ctype.h>
#define max(a,b) (a > b ? a : b)
#define min(a,b) (a < b ? a : b)
#define ls (now << 1)
#define rs (now << 1 | 1)
const int MARX = 2e5 + 10;
//=============================================================
struct node
{
int L, R;
int ans;//记录区间答案(若区间答案不一致则为-1
int minn, maxx;//记录区间内 最小 / 最大 的答案
}tree[MARX << 2];
struct Query
{
int l, r, data;//询问的左右端点, 及询问编号
}q[MARX];
int N, M, original[MARX], next[MARX], ans[MARX];
std :: map <int, int> last;//预处理使用, 记录某一个数 上一次出现的位置
std:: map <int,bool> vis; //预处理使用 , 记录一个数是否出现过
//=============================================================
inline int read()
{
int s = 1, w = 0; char ch = getchar();
for(; !isdigit(ch); ch = getchar()) if(ch == '-') s = -1;
for(; isdigit(ch); ch = getchar()) w = (w << 1) + (w << 3) + (ch ^ '0');
return s * w;
}
bool cmp(Query fir, Query sec)//对查询排序的 比较函数
{
if(fir.l == sec.l) return fir.r < sec.r;
return fir.l < sec.l;
}
void Pushdown(int now)//下传 懒标记
{
tree[ls].ans = tree[rs].ans = tree[now].ans;
tree[ls].minn = tree[rs].minn = tree[now].ans;
tree[ls].maxx = tree[rs].maxx = tree[now].ans;
tree[now].ans = -1;
}
void Pushup(int now)//更新 now点的信息
{
tree[now].minn = min(tree[ls].minn, tree[rs].minn);
tree[now].maxx = max(tree[ls].maxx, tree[rs].maxx);
tree[now].ans = tree[ls].ans == tree[rs].ans ? tree[ls].ans : -1;//更新懒标记
}
void Build(int now, int L, int R)//建树
{
tree[now].L = L, tree[now].R = R, tree[now].ans = -1;//赋初值
if(L == R) //递归到叶节点
{
tree[now].ans = tree[now].minn = tree[now].maxx = ans[L];
return ;
}
int mid = (L + R) >> 1;
Build(ls, L, mid), Build(rs, mid + 1, R);
Pushup(now);
}
void Change(int now, int L, int R, int key)//将 [L,R]内 答案 > key的区间 更新为key
{
if(L <= tree[now].L && tree[now].R <= R)//区间被完全包括
if(tree[now].minn >= key)//最小值大于key, 则区间需要全部被更新
{
tree[now].ans = tree[now].minn = tree[now].maxx = key;
return ;
}
else if(tree[now].maxx <= key) return ;//最大值 小于等于key, 则区间不需要被更新
if(tree[now].ans != -1) Pushdown(now);//下传懒标记
int mid = (tree[now].L + tree[now].R) >> 1;
if(L <= mid) Change(ls, L, R, key);//递归 修改左右区间
if(R > mid) Change(rs, L, R, key);
Pushup(now);//更新 now点的信息
}
int Query(int now, int pos)//单点查询 pos的答案
{
if(pos == tree[now].L && tree[now].R == pos) return tree[now].ans; //寻找到答案
if(tree[now].ans != -1) Pushdown(now);//下传懒标记
int mid = (tree[now].L + tree[now].R) >> 1;
if(pos <= mid) return Query(ls, pos);//查询 对应位置
else return Query(rs, pos);
}
void Prepare()//预处理
{
N = read(), M = read();
for (int i = 1, minn = 0; i <= N; i ++) //读入原始数列, 预处理next值
{
original[i] = read(), next[i] = N + 1;
next[last[original[i]]] = i, last[original[i]] = i; //更新 next, last值
vis[original[i]] = 1; //更新 此数的出现情况
while(vis[minn]) minn ++; //计算 [1,i] 的答案
ans[i] = minn;
}
Build(1, 1, N);
for(int i = 1; i <= M; i ++) q[i].l = read(), q[i].r = read(), q[i].data = i;
std :: sort(q + 1, q + M + 1, cmp);//将查询按照左端点 升序排序
}
//=============================================================
signed main()
{
Prepare();
for(int i = 1, nowl = 1; i <= M; i ++)//枚举 询问
{
//将左端点右移至 当前询问的左端点
for(;q[i].l > nowl; nowl ++) Change(1, nowl , next[nowl] - 1, original[nowl]);
ans[q[i].data] = Query(1, q[i].r);//回答询问
}
for(int i = 1; i <= M; i ++) printf("%d\n", ans[i]);
}
莫队:
查询区间未出现的 自然数
删除数时 直接更新出现次数, 并依照大小关系更新 答案
添加数时, 先更新出现次数, 之后暴力枚举查询 最小的未出现的自然数
#include <cstdio>
#include <cctype>
#include <cmath>
#include <algorithm>
#define ll long long
const int MARX = 2e5 + 10;
//===========================================================
struct Query
{
int L, R, ID;
} Q[MARX];
int N, M, Number[MARX], Belong[MARX];
int nowl = 1, nowr, nowans, use[MARX], Ans[MARX];
//===========================================================
inline int read()
{
int w = 0, f = 1; char ch = getchar();
for(; !isdigit(ch); ch = getchar()) if(ch == '-') f = -1;
for(; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
bool Sort_Compare(Query first, Query second)
{
if(Belong[first.L] == Belong[second.L]) return first.R < second.R;
return first.L < second.L;
}
void Prepare()
{
N = read(), M = read();
for(int i = 1; i <= N; i ++) Number[i] = read();
for(int i = 1; i <= M; i ++) Q[i].L = read(), Q[i].R = read(), Q[i].ID = i;
for(int i = 1, Block = sqrt(N + 1); i <= N; i ++) Belong[i] = (i - 1) / Block + 1;
std :: sort(Q + 1, Q + M + 1, Sort_Compare);
}
void add(int now)
{
use[Number[now]] ++;
if(Number[now] == nowans)
while(use[nowans]) nowans ++;
}
void del(int now)
{
use[Number[now]] --;
if(! use[Number[now]] && Number[now] < nowans)
nowans = Number[now];
}
//===========================================================
int main()
{
Prepare();
for(int i = 1; i <= M; i ++)
{
while(nowl < Q[i].L) del(nowl), nowl ++;
while(nowl > Q[i].L) nowl --, add(nowl);
while(nowr > Q[i].R) del(nowr), nowr --;
while(nowr < Q[i].R) nowr ++, add(nowr);
Ans[Q[i].ID] = nowans;
}
for(int i = 1; i <= M; i ++) printf("%d\n", Ans[i]);
return 0;
}