<think>我们首先需要理解题解中的思路:
1. 用一个桶记录每个数字出现的次数。
2. 将每个数字x看成一段覆盖区间[x - buc[x] + 1, x](因为每个数字x可以覆盖从x-buc[x]+1到x的位置)。
3. 答案就是[1, n](实际上由于整体移动,所以是[st+1, st+n])中没有被覆盖的位置数。
由于有整体加减操作,我们用`sett`表示当前0的位置(初始`sett=150000+1`,以避免负数)。线段树维护整个数轴(范围是1到450000左右)上每个位置被覆盖的次数(通过区间加减实现)。然后,我们使用线段树维护区间内0的个数(即未被覆盖的位置数),每次查询[sett+1, sett+n]区间的0的个数即为答案。
然而,我的代码中存在一些问题。我们逐步分析:
### 问题1:线段树维护的信息
题解中线段树节点维护了最小值`mi`,最小值出现次数`cnt`,以及区间内0的个数`ans`(实际上就是当最小值为0时,最小值出现的次数,因为最小值为0的位置就是未被覆盖的位置)。还有懒标记`ad`。
在我的代码中,我维护了:
- `minn`: 区间最小值
- `cnt`: 区间内最小值出现的次数(注意:这个`cnt`并不是题解中的桶,而是线段树节点中最小值出现的次数,不要混淆)
- `cnt0`: 区间内0的个数(即未被覆盖的位置数)
- `lzyt`: 懒标记
题解中在`tag`函数中更新`ans`:`t[x].ans=(t[x].mi==0)?t[x].cnt:0;`,也就是如果最小值为0,那么0的个数就是最小值出现的次数,否则为0。
我的`upd`函数中更新`cnt0`为:`tr[p].cnt0 = (tr[p].minn == 0) ? tr[p].cnt : 0;`,这是正确的。
但是,在`down`函数中,我注释掉了对左右儿子`cnt0`的更新,这是不对的。因为下放懒标记后,儿子的最小值改变了,所以`cnt0`也要重新计算。但是,我们不能在`down`中直接计算,因为懒标记可能只是加减,我们可以在`down`后调用`upd`来更新,但是这里我们没有调用。所以,正确的做法是:在`down`函数中,我们只下放懒标记,然后在`upd`函数中统一更新`cnt0`。但是,在`down`后,子节点的最小值已经改变,所以我们需要在`down`后,对子节点重新计算`cnt0`(实际上,因为懒标记改变的是整个区间的值,所以改变后,子节点的`cnt0`要根据新的最小值重新计算,而子节点可能还没有更新,所以我们不能直接计算,必须通过`upd`来更新。然而,这里我们无法调用`upd`,因为当前节点可能不是叶子节点,所以我们需要在`down`时,对每个子节点重新设置`cnt0`(根据新的最小值)。
题解的处理是:在`tag`函数中,直接更新当前节点的`ans`(即我们的`cnt0`)为:`t[x].ans=(t[x].mi==0)?t[x].cnt:0;`。所以,我们在下放懒标记时,只需要调用`tag`函数即可。
因此,我修改代码,模仿题解,写一个`push_tag`函数(或者叫`tag`函数),在打标记时更新当前节点的`cnt0`,然后在`down`函数中,对左右儿子调用`tag`函数。
### 问题2:初始化
题解中,初始构建线段树时,将所有位置都初始化为0,并且每个位置都是1(即`cnt`为1,`ans`为1,因为最小值为0,所以0的个数就是1)。然后,再根据初始的桶,对每个桶对应的区间进行加1操作。
我的`build`函数中,对叶子节点的初始化是正确的:`tr[p].cnt = tr[p].cnt0 = 1; tr[p].minn = 0;`。
### 问题3:修改操作
对于单点修改,我们首先将原值从桶中去除,然后修改桶,再添加新值。
- 去除原值:如果原值`a[p]`在区间[sett+1, sett+n]内(即<=sett+n),那么我们就需要将原来这个值对应的覆盖区间减去1(即区间`[a[p]-cnt[a[p]]+1, a[p]]`减去1)。注意,在减去之前,`cnt[a[p]]`还没有减少,所以区间就是`[a[p]-cnt[a[p]]+1, a[p]]`。然后我们将`cnt[a[p]]`减1,并将新值加入桶(`cnt`加1),如果新值在区间内,则对新值的区间加1。
对于整体加减操作:
- 整体加1(即x=1):首先,在整体加1之前,我们需要将当前区间最右边的一列(即`sett+n`)的覆盖去掉(因为整体加1后,原来在`sett+n`的数会变成`sett+n+1`,超过了当前区间[sett+1, sett+n]的右端点,所以这些数不再覆盖区间)。具体操作:如果`cnt[sett+n]`不为0,那么将区间`[sett+n-cnt[sett+n]+1, sett+n]`减去1。然后`sett--`(注意:整体加1相当于0的位置向后移动了一格,所以sett要减1,因为sett记录的是0的位置,原来0的位置在sett,现在整体加1,那么0的位置就相当于前移了,即sett-1?这里需要明确)。
题解中的处理:
- 当x>0(整体加1)时,先处理当前`st+n`位置(因为整体加1后,这个位置上的数会超出区间,所以去掉覆盖),然后`st--`。
- 当x<0(整体减1)时,先`st++`,然后处理新的`st+n`位置(因为整体减1后,原来`st+n+1`的数会变成`st+n`,进入区间,所以加上覆盖)。
这里要注意:整体加1操作,相当于整个数列的每个数都加1,那么原来值为x的变成x+1。而我们的桶下标也要相应的移动?实际上,我们并不移动桶,而是移动`sett`(0的位置)。原来0的位置在`sett`,整体加1后,0的位置就变成了`sett-1`(因为每个数加1,那么相当于数轴向左移动了一格,所以0的位置就变成了原来的位置减1)。同样,整体减1后,0的位置变成`sett+1`。
因此,整体加1的操作步骤:
1. 将原来在`sett+n`位置上的桶的贡献去掉(因为整体加1后,这个位置上的数变成`sett+n+1`,不在区间[sett+1, sett+n]内,所以要去掉)。注意,这里去掉的是当前`sett+n`位置上的桶(在整体加1前,这个位置还在区间内,但是整体加1后就不在区间内了)。
2. `sett--`(0的位置左移)。
整体减1的操作步骤:
1. `sett++`(0的位置右移)。
2. 将新的`sett+n`位置上的桶的贡献加上(因为整体减1后,原来在`sett+n+1`位置上的数(整体减1后变成`sett+n`)现在进入了区间,所以要加上)。
### 问题4:线段树操作范围
题解中,线段树的范围是`lim=450000+5`(因为初始0位置在150000+1,数列整体加减可能移动150000次,所以总范围大约是450000)。我的代码中,`MAXN = 150005*3 = 450015`,所以线段树范围是`1`到`MAXN-1`(即450014),是足够的。
### 修改我的代码
1. 修改线段树的下放函数,采用题解中的`tag`函数方式,并在下放时调用这个`tag`函数。
2. 在修改操作中,注意整体加减时`sett`的变化。
具体修改步骤:
#### 增加一个`tag`函数
```cpp
void tag( int p, int v ){
tr[p].minn += v;
tr[p].lzyt += v;
tr[p].cnt0 = ( tr[p].minn == 0 ) ? tr[p].cnt : 0; // 更新当前节点的0的个数
}
```
#### 修改`down`函数
```cpp
void down( int p ){
if( !tr[p].lzyt ) return;
tag( p << 1, tr[p].lzyt );
tag( p << 1 | 1, tr[p].lzyt );
tr[p].lzyt = 0;
}
```
#### 修改`mdf`函数
在区间修改函数中,我们不再直接修改`cnt0`,而是通过`tag`函数来更新。所以在`mdf`函数中,当递归到子区间时,我们先`down`,然后递归,最后`upd`。但是,当我们对整个节点打标记时,我们调用`tag`函数。
修改`mdf`函数:
```cpp
void mdf( int p, int l, int r, int x, int y, int v ){
if( x <= l && r <= y ){
tag( p, v ); // 调用tag函数
return;
}
down( p );
int mid = ( l + r ) >> 1;
if( x <= mid ) mdf( p << 1, l, mid, x, y, v );
if( y > mid ) mdf( p << 1 | 1, mid + 1, r, x, y, v );
upd( p );
}
```
#### 修改`upd`函数
在`upd`函数中,我们更新`cnt0`(因为合并左右儿子后,当前节点的最小值可能变化,所以`cnt0`也要更新):
```cpp
void upd( int p ){
tr[p].minn = min( tr[p << 1].minn, tr[p << 1 | 1].minn );
tr[p].cnt = 0;
if( tr[p].minn == tr[p << 1].minn ) tr[p].cnt += tr[p << 1].cnt;
if( tr[p].minn == tr[p << 1 | 1].minn ) tr[p].cnt += tr[p << 1 | 1].cnt;
tr[p].cnt0 = ( tr[p].minn == 0 ) ? tr[p].cnt : 0; // 更新cnt0
}
```
#### 修改整体加减操作
在整体加1(x==1)时,我们先去掉原来`sett+N`位置的覆盖(如果这个位置有桶,则将其覆盖的区间减去1),然后`sett--`。
在整体减1(x==-1)时,我们先`sett++`,然后加上新的`sett+N`位置的覆盖。
注意:整体加减操作中,x的值可能是1或-1(根据题目描述,p=0时,x=-1或1)。
#### 注意:初始添加数字
在初始时,我们将每个数字加上`sett`,然后将其加入桶中,并且如果这个数字在区间[sett+1, sett+n]内(实际上初始时所有数字都在这个区间内),我们就将其覆盖的区间加上1。
### 代码修改后的完整代码
注意:由于我的代码中`cnt0`已经在`tag`和`upd`中正确维护,所以查询函数`query`可以直接查询区间[sett+1, sett+n]内的`cnt0`(即0的个数)。
### 提交注意事项
题目中m和n最大为150000,所以数组要开足够大(我们开3倍,即450015)。
由于整体加减操作,桶数组`cnt`的下标可能会达到450000左右,所以`cnt`数组也要开足够大(我们开的是`MAXN=150005*3`,所以足够)。
### 修改后的代码
下面是修改后的完整代码,我尽量保持原有结构,只修改必要的地方。
```cpp
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 150005 * 3 + 10; // 稍微开大一些
int sett = 150005;
int N, M;
int a[MAXN], p, x, cnt[MAXN]; // a数组和桶数组
struct Segtree {
int l, r;
int minn, cnt, cnt0, lzyt;
} tr[MAXN * 4]; // 线段树开4倍
void upd( int p ) {
tr[p].minn = min( tr[p<<1].minn, tr[p<<1|1].minn );
tr[p].cnt = 0;
if (tr[p].minn == tr[p<<1].minn) tr[p].cnt += tr[p<<1].cnt;
if (tr[p].minn == tr[p<<1|1].minn) tr[p].cnt += tr[p<<1|1].cnt;
tr[p].cnt0 = (tr[p].minn == 0) ? tr[p].cnt : 0;
}
void tag( int p, int v ) {
tr[p].minn += v;
tr[p].lzyt += v;
tr[p].cnt0 = (tr[p].minn == 0) ? tr[p].cnt : 0; // 更新cnt0
}
void down( int p ) {
if (!tr[p].lzyt) return;
tag(p<<1, tr[p].lzyt);
tag(p<<1|1, tr[p].lzyt);
tr[p].lzyt = 0;
}
void build( int p, int l, int r ) {
tr[p].l = l; tr[p].r = r;
tr[p].lzyt = 0;
if (l == r) {
tr[p].minn = 0;
tr[p].cnt = 1;
tr[p].cnt0 = 1;
return;
}
int mid = (l + r) >> 1;
build(p<<1, l, mid);
build(p<<1|1, mid+1, r);
upd(p);
}
void mdf( int p, int l, int r, int x, int y, int v ) {
if (x <= l && r <= y) {
tag(p, v);
return;
}
down(p);
int mid = (l + r) >> 1;
if (x <= mid) mdf(p<<1, l, mid, x, y, v);
if (y > mid) mdf(p<<1|1, mid+1, r, x, y, v);
upd(p);
}
int query( int p, int l, int r, int x, int y ) {
if (x <= l && r <= y) {
return tr[p].cnt0;
}
down(p);
int mid = (l + r) >> 1, res = 0;
if (x <= mid) res += query(p<<1, l, mid, x, y);
if (y > mid) res += query(p<<1|1, mid+1, r, x, y);
return res;
}
int main() {
cin >> N >> M;
build(1, 1, MAXN-1); // 注意:线段树区间从1到MAXN-1
sett = 150005; // 0的位置,初始0在150005+1? 实际上,我们定义sett为0的位置,那么数列中数字a[i]实际存储在a[i]+sett位置。
// 但是题解中初始st=150000+1,而我们这里sett初始为150005,那么初始0的位置就是150005。
// 注意:题解中st初始为150000+1,范围是450000。我们这里MAXN=150005*3=450015,所以线段树范围是1..450014。
// 读入初始数组
for (int i = 1; i <= N; i++) {
cin >> a[i];
a[i] += sett; // 加上偏移量
cnt[a[i]]++; // 桶计数
}
// 初始化线段树:对于每个在区间[sett+1, sett+N]内的数,将其覆盖区间加上1
for (int i = sett+1; i <= sett+N; i++) {
if (cnt[i] > 0) {
mdf(1, 1, MAXN-1, i - cnt[i] + 1, i, 1);
}
}
while (M--) {
cin >> p >> x;
if (p > 0) { // 单点修改
// 删去旧点
// 如果旧点在当前区间内(即<=sett+N),则需要将其覆盖的区间减去一次
if (a[p] <= sett + N) {
mdf(1, 1, MAXN-1, a[p] - cnt[a[p]] + 1, a[p], -1);
}
cnt[a[p]]--; // 桶中移除旧点
// 更新a[p]为新值
a[p] = sett + x; // 新值加上当前偏移
cnt[a[p]]++; // 加入桶
if (a[p] <= sett + N) {
mdf(1, 1, MAXN-1, a[p] - cnt[a[p]] + 1, a[p], 1);
}
} else { // p=0,整体加减
if (x == 1) { // 整体加1
// 首先,当前区间最右边的一列(即sett+N)将不再被覆盖(因为整体加1后,这些数变成sett+N+1,超过区间)
if (cnt[sett + N] > 0) {
mdf(1, 1, MAXN-1, (sett+N) - cnt[sett+N] + 1, sett+N, -1);
}
sett--; // 整体加1,0的位置左移(相当于sett减1)
} else if (x == -1) { // 整体减1
sett++; // 整体减1,0的位置右移(sett加1)
if (cnt[sett + N] > 0) {
mdf(1, 1, MAXN-1, (sett+N) - cnt[sett+N] + 1, sett+N, 1);
}
}
}
// 查询区间[sett+1, sett+N]中的0的个数
cout << query(1, 1, MAXN-1, sett+1, sett+N) << endl;
}
return 0;
}
```
### 注意:初始偏移量
在题解中,`st`初始为`150000+1`,而我们这里`sett`初始为`150005`。这并不影响,因为我们线段树的范围足够大。但是要注意,初始时每个数字加上`sett`后,桶的下标就是`a[i]+sett`。
### 测试样例
使用题目给的样例输入测试,观察输出是否与样例输出一致。
### 最后
由于题目复杂度为O(nlogn),应该可以通过。
希望这个修改后的代码能正确运行。如果还有问题,请再提出。
</think>我仔细分析了你的代码,发现主要问题出现在线段树的下推操作和标记处理上。关键问题在于:
1. `cnt0`(值为0的位置数量)在标记下推时没有正确更新
2. 整体加减操作中`sett`的更新逻辑需要调整
3. 线段树查询范围需要明确
以下是调试后的完整代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 150005 * 3;
int sett = 150005;
int N, M;
int a[MAXN], p, x, cnt[MAXN];
struct Segtree{
int l, r;
int minn, cnt, cnt0, lzyt;
}tr[MAXN << 2];
void tag(int p, int v) {
tr[p].minn += v;
tr[p].lzyt += v;
tr[p].cnt0 = (tr[p].minn == 0) ? tr[p].cnt : 0;
}
void down(int p) {
if(!tr[p].lzyt) return;
tag(p<<1, tr[p].lzyt);
tag(p<<1|1, tr[p].lzyt);
tr[p].lzyt = 0;
}
void upd(int p) {
tr[p].minn = min(tr[p<<1].minn, tr[p<<1|1].minn);
tr[p].cnt = 0;
if(tr[p].minn == tr[p<<1].minn) tr[p].cnt += tr[p<<1].cnt;
if(tr[p].minn == tr[p<<1|1].minn) tr[p].cnt += tr[p<<1|1].cnt;
tr[p].cnt0 = (tr[p].minn == 0) ? tr[p].cnt : 0;
}
void build(int p, int l, int r) {
tr[p].l = l; tr[p].r = r;
tr[p].lzyt = 0;
if(l == r) {
tr[p].minn = 0;
tr[p].cnt = 1;
tr[p].cnt0 = 1;
return;
}
int mid = (l + r) >> 1;
build(p<<1, l, mid);
build(p<<1|1, mid+1, r);
upd(p);
}
void mdf(int p, int l, int r, int x, int y, int v) {
if(x <= l && r <= y) {
tag(p, v);
return;
}
down(p);
int mid = (l + r) >> 1;
if(x <= mid) mdf(p<<1, l, mid, x, y, v);
if(y > mid) mdf(p<<1|1, mid+1, r, x, y, v);
upd(p);
}
int query(int p, int l, int r, int x, int y) {
if(x <= l && r <= y) return tr[p].cnt0;
down(p);
int mid = (l + r) >> 1, res = 0;
if(x <= mid) res += query(p<<1, l, mid, x, y);
if(y > mid) res += query(p<<1|1, mid+1, r, x, y);
return res;
}
int main() {
cin >> N >> M;
build(1, 1, MAXN-1);
for(int i = 1; i <= N; i++) {
cin >> a[i];
a[i] += sett;
cnt[a[i]]++;
}
for(int i = sett+1; i <= sett+N; i++) {
if(cnt[i]) {
mdf(1, 1, MAXN-1, i - cnt[i] + 1, i, 1);
}
}
while(M--) {
cin >> p >> x;
if(p > 0) {
if(a[p] <= sett+N) {
mdf(1, 1, MAXN-1, a[p]-cnt[a[p]]+1, a[p], -1);
}
cnt[a[p]]--;
a[p] = sett + x;
cnt[a[p]]++;
if(a[p] <= sett+N) {
mdf(1, 1, MAXN-1, a[p]-cnt[a[p]]+1, a[p], 1);
}
} else {
if(x == 1) {
if(cnt[sett+N]) {
mdf(1, 1, MAXN-1, (sett+N)-cnt[sett+N]+1, sett+N, -1);
}
sett--;
} else if(x == -1) {
sett++;
if(cnt[sett+N]) {
mdf(1, 1, MAXN-1, (sett+N)-cnt[sett+N]+1, sett+N, 1);
}
}
}
cout << query(1, 1, MAXN-1, sett+1, sett+N) << endl;
}
return 0;
}
```
### 关键修改点说明:
1. **标记处理函数`tag()`**:
- 添加专门的标记处理函数,确保标记下推时正确更新`minn`和`cnt0`
- 计算公式:`cnt0 = (minn == 0) ? cnt : 0`
2. **下推函数`down()`**:
- 使用`tag()`函数处理子节点标记
- 确保标记下推后重置父节点标记
3. **更新函数`upd()`**:
- 合并子节点信息后重新计算`cnt0`
- 确保节点信息一致性
4. **整体加减操作**:
- 整体加1 (`x=1`): 先移除右边界影响再`sett--`
- 整体减1 (`x=-1`): 先`sett++`再添加新右边界影响
- 严格维护`sett`表示当前0的位置
5. **线段树范围**:
- 使用`MAXN = 150005*3`确保足够覆盖整体移动的范围
- 线段树构建范围`1`到`MAXN-1` (450014)
这个实现严格遵循了题解中的算法思想,使用线段树维护桶的覆盖情况,通过查询区间内值为0的位置数量得到答案。