https://codeforces.com/gym/102192/problem/J
题意
给定 n 个苹果所在的高度
h
[
]
h[]
h[]。
每次都从第一个位置开始,如果当前苹果是第一个或者比上一次拿的苹果高度大,就拿;
否则就不拿,看下一个位置。
现在有 m 次询问,每次询问给出 x,y,表示修改 x 位置的苹果高度为 y,问修改过之后能拿多少个苹果?(各询问独立)
1
≤
n
,
m
≤
1
0
5
,
1
≤
h
i
≤
1
0
9
1 ≤ n, m ≤ 10^5,\ 1 ≤ h_i ≤ 10^9
1 ≤ n, m ≤ 105, 1 ≤ hi ≤ 109
1
≤
x
≤
n
,
1
≤
y
≤
1
0
9
1 ≤ x ≤ n,\ 1 ≤ y ≤ 10^9
1 ≤ x ≤ n, 1 ≤ y ≤ 109
思路
每次只修改一个地方,所以考虑这个位置修改了对前后两段的影响。对前面一段没有任何影响,对后面一段的影响和修改之后的高度有关。
- 如果修改 x 位置之后的高度 y 大于区间 [1, x-1] 的最大值的话,那么 x 位置必拿,并且后面从第一个大于 y 的位置开始往后拿。
- 否则,x 位置不会拿,后面从第一个大于区间 [1, x-1] 最大值的地方开始拿。
(第二种情况一开始想的是,当前位置变小了就不会拿,答案是原数组的答案。但是少考虑了一种情况,就是原来这个位置的值比前面最大值大,那么原数组中该位置是拿的,但是现在修改之后该位置不拿了,所以就要重新计算答案。)
现在对于每次修改之后,修改位置之前的答案是不变的,所以可以提前维护出来前 i
个位置一共拿的个数 cntl[i]
:从前往后遍历所有位置,如果当前位置比前面最大值大的话,当前位置必拿,cntl[i] = cntl[i-1] + 1
;否则 cntl[i] = cntl[i-1]
。
某个位置修改之后,这个位置后面第一个拿的位置可以确定,从这个位置如果直接遍历往后找求个数的话,时间复杂度就很高。所以也可以提前维护出来从某个位置开始走,到数组末位置一共拿的个数 cntr[i]
。
(场上这个地方维护的时候是仿照的维护前面的做法:从后往前走,如果当前位置是最小值的话,
cntr[i] = cntr[i+1] + 1
;否则cntr[i] = cntr[i+1]
。但这种思路是错的,维护前面的时候可以继承上一位置的状态,但是维护后面的就不能继承后一位置的状态,因为是从前往后拿的,当前位置拿了后面就要找第一个大于当前值的位置,从这个位置来转移。)
所以需要从后往前维护单调递减的栈,每次找后面第一个大于当前值的位置,从该位置的答案 + 1 来转移。(也可以从后往前维护递减的单调队列,然后到每个位置队列的长度就是从该位置开始到末尾能拿的个数)
然后就要处理 m 个询问,关键在于询问每个位置往后走第一个大于 y 的位置。
有两种处理方式:
方式 1:离线 + 二分单调栈
把对于每个位置的询问修改后的值以及询问编号都挂到每个位置下面,从后往前维护单调递减栈,每次询问后面所有位置中第一个大于 y 的值,就可以二分单调栈中的所有位置,找到第一个大于 y 的值所在位置。(单调栈里放位置,且只能放原数组的位置,对于外来的数只能二分找第一个大于其的位置)
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define int long long
const int N = 200010, mod = 1e9+7;
int T, n, m;
int a[N];
int cntl[N], cntr[N], maxl[N];
vector<PII> v[N];
int Ans[N];
int stk[N], idx;
void init()
{
for(int i=1;i<=n;i++){
v[i].clear();
stk[i] = 0;
}
}
int find(int x)
{
int l = 0, r = idx;
while(l < r)
{
int mid = l + r + 1 >> 1;
if(a[stk[mid]] > x) l = mid;
else r = mid - 1;
}
if(a[stk[l]] > x) return stk[l];
return 0;
}
signed main(){
Ios;
cin >> T;
while(T--)
{
cin >> n >> m;
init();
for(int i=1;i<=n;i++)
{
cin >> a[i];
cntl[i] = cntl[i-1];
if(a[i] > maxl[i-1]) cntl[i] ++;
maxl[i] = max(maxl[i-1], a[i]);
}
idx = 0; //单调栈维护从i位置到最后面的答案
for(int i=n;i>=1;i--)
{
while(idx && a[stk[idx]] <= a[i]) idx --;
if(!idx) cntr[i] = 1;
else cntr[i] = cntr[stk[idx]] + 1;
stk[++idx] = i;
}
for(int i=1;i<=m;i++) //离线
{
int x, y; cin >> x >> y;
v[x].push_back({y, i});
}
idx = 0;
for(int i=n;i>=1;i--)
{
for(PII it : v[i])
{
int x = it.fi, id = it.se;
int ans = 0;
if(x > maxl[i-1])
{
ans = cntl[i-1] + 1;
int p = find(x); //二分找单调栈里第一个大于x的值所在位置
if(p) ans += cntr[p];
}
else
{
ans = cntl[i-1];
int p = find(maxl[i - 1]); //二分找单调栈里第一个大于前面最大值的值所在位置
if(p) ans += cntr[p];
}
Ans[id] = ans;
}
while(idx && a[stk[idx]] <= a[i]) idx --; //用当前位置更新单调栈
stk[++idx] = i;
}
for(int i=1;i<=m;i++) cout << Ans[i] << "\n";
}
return 0;
}
方式2:RMQ + 二分
要询问每个位置 x
往后走遇到的第一个大于 y 的数的位置,可以二分该位置 p,如果区间 [x + 1, p]
的区间最大值大于 y 了,就把区间缩小,这样就能找到第一个大于的位置。
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define int long long
#define PII pair<int,int>
#define fi first
#define se second
#define endl '\n'
map<int, int> mp;
/**/
const int N = 200010, mod = 1e9+7;
int T, n, m;
int a[N];
int f[N][20], k;
int maxl[N];
int cntl[N], cntr[N];
void RMQ()
{
for(int i=1;i<=n;i++) f[i][0] = a[i];
k = log(n)/log(2);
for(int j=1;j<=k;j++)
{
for(int i=1;i<=n-(1<<j)+1;i++)
{
f[i][j] = max(f[i][j-1], f[i+(1<<j-1)][j-1]); //注意更新方式,j都是从j-1来更新
}
}
}
int query(int l, int r)
{
if(l > r) return 0;
int k = log(r-l+1)/log(2);
return max(f[l][k], f[r-(1<<k)+1][k]);
}
int find(int L, int x)
{
int l = L, r = n;
while(l < r)
{
int mid = l + r >> 1;
if(query(L, mid) > x) r = mid;
else l = mid + 1;
}
if(query(L, l) > x) return l;
return 0;
}
signed main(){
Ios;
cin >> T;
while(T--)
{
cin >> n >> m;
for(int i=1;i<=n;i++){
cin >> a[i];
maxl[i] = maxl[i-1], cntl[i] = cntl[i-1];
if(a[i] > maxl[i-1]){
cntl[i] ++;
maxl[i] = a[i];
}
}
RMQ();
stack<int> stk;
for(int i=n;i>=1;i--)
{
while(stk.size() && a[stk.top()] <= a[i]) stk.pop();
if(!stk.size()) cntr[i] = 1;
else cntr[i] = cntr[stk.top()] + 1;
stk.push(i);
}
while(m--)
{
int x, y; cin >> x >> y;
int maxa = query(1, x-1), ans = 0;
if(y > maxa)
{
ans = cntl[x-1] + 1;
int p = find(x + 1, y);
if(p) ans += cntr[p];
}
else
{
ans = cntl[x - 1];
int p = find(x + 1, maxl[x - 1]);
if(p) ans += cntr[p];
}
cout << ans << endl;
}
}
return 0;
}
场上这道题写了三个钟头没写出来,有两个地方出错了。
第一个就是维护从某个位置往后能拿的个数,直接仿照维护前面的做法写的,也没多想,后面也没输出来看一下。。
第二个地方就是找每个位置往后走第一次遇到比它大的数所在位置。场上直接实现错了,实现的是每个位置后面所有数中第一个比它大的数所在位置,把后面所有数放到集合里查第一个比它大的数了。。
这确实属于重大失误了,两个人看都没发现这两个错误!!
不知道哪里错了就多试几组样例,试试就试出来了。或者写一个暴力,拍拍小数据。
实在找不出来最后一个半小时就换个人重写一遍,两个人的代码再拍一下。
吸取教训吧。