一道耗费大量精力的数据结构题目
原题目
题目分析
大家应该都能意识到,每一次都去找下标最小的最大值是非常浪费时间的。而对于一个位置而言,只要被操作一次就等于这个位置要被操作a[i]次,因为两边的值同时被-1,导致两边的值永远不会被取到。
因此,我们可以对一个选定的位置(i)一直操作,直到a[i]变成0,此时a[i-1]和a[i+1]必定都变成0。如此反复,我我们的任务就变成了维护这个序列,求在这个序列中被选出的若干数的和。
这样,我们最初的思路就产生了。vis[]用于判断某一个位置是否被覆盖,对于每一次修改,在修改完成后从大到小进行排序,每选出一个位置就把两边的位置的vis置为true,如此反复,直到把所有位置都取出,则取出这些数的和就是这一次询问的答案。
复杂度是
O
(
q
n
l
o
g
n
)
O(qnlogn)
O(qnlogn)。
这样我们就可以获得至少15分的好成绩啦
面对满眼的TLE,再看着出题人是lxl,我们就知道这个题目需要一些毒瘤的数据结构来维护啦。
题目再分析
由于需要取的数字是一系列较大的数,所以我们会考虑到一些单调的序列上的性质。我们发现,对于一个单调序列,我们取到的数字必定是第一大,第三大,第五大……在下标上的规律就是,取到的数下标的奇偶性必定是相同的。所以我们考虑使用树状数组(也许线段树也可以)分奇偶维护序列ai的前缀和,这样面对单调的序列就可以快速求出对应的数字和了。
那么下一个问题就是确定每一段单调序列的位置,这个问题也不难解决,我们只需要一个set来维护每一段序列的开头位置和结尾位置(就相当于是维护这个序列的极大值和极小值的位置)即可。
尽管这样,每一次修改对全部内容进行更新也产生了非常不理想的时间复杂度。所以我们考虑一次修改产生的最大的影响有多大的范围,只修改会产生影响的范围内的数据就可以减小需要耗费的时间了。
经过艰苦的找数据过程,我们发现最多会影响到两边不超过2个极值点的位置,如序列
(
11
,
10
,
9
,
2
,
6
,
8
)
(11,10,9,2,6,8)
(11,10,9,2,6,8),极值位置有
(
1
,
4
,
6
)
(1,4,6)
(1,4,6),将第3位的9改成1,会导致
[
4
,
6
]
[4,6]
[4,6]段变成
[
3
,
6
]
[3,6]
[3,6]段,影响到了右数第二个极值位置(6);而对于一个上凸的序列
(
2
,
5
,
8
,
6
,
3
)
(2,5,8,6,3)
(2,5,8,6,3),极值位置有
(
1
,
3
,
5
)
(1,3,5)
(1,3,5),修改第四位的6为9,影响到了
[
1
,
3
]
[1,3]
[1,3]段,变为了
[
1
,
4
]
[1,4]
[1,4]段,影响了向左数第二个极值点。
所以对于每一次修改,我们都把它两边的两个极值点之间的区间进行更新,就可以保证全部覆盖每一次修改的影响范围。而答案自然就是(上一次的答案-之前的区间的答案贡献 + 修改之后的答案贡献)。
在大框架完成之后,需要考虑的问题是极小值是否需要计算的问题。方法是先都不计算极小值的部分,需要计算的情况只有与左右两边的极大值之间距离均为偶数(下标奇偶性相同),所以在计算的时候加一步讨论就可以解决。同时计算的时候还需要分递增序列和递减序列进行考虑
其他还有许许多多的边界问题需要考虑和讨论,在这部分我出锅的主要原因是set中begin()是真实存在的,而end()是不存在的。统计答案的时候端点达到begin是可以的,但是达到end是不可以的。
在具体实现上,我们发现输入原始序列就相当于是对一个全0的序列进行若干次修改,所以也同样可以使用上面的方法,这样也减轻了代码的压力
XBB
菜鸡讨论了一下午,终于解决了需要考虑的所有问题,A掉了两年前加到题单里的题目,将近3KB的代码QAQ
代码已经不成人形了5555
#include <iostream>
#include <cstdio>
#include <cstring>
#include <set>
using namespace std;
const int MAXN = 1e5 + 100;
typedef long long ll;
typedef set<int>::iterator SIT;
#define DEBUG cerr << "OK";
int a[MAXN],n;
struct TreeArray{
ll s[MAXN];
int N;
inline int lowbit(int num)
{
return num & (-num);
}
void add(int pos, int val)
{
for (int i = pos; i <= N; i += lowbit(i))
s[i] += 1LL * val;
}
ll query(int pos)
{
int cnt = 0;
ll sum = 0;
for (int i = pos ; i; i -= lowbit(i),cnt++)
sum += s[i];
return sum;
}
}arr[2];
set<int> s;
ll ans;
inline int re()
{
int num = 0, minus = 1;
char ch = getchar();
while (ch > '9' || ch < '0')
{
if (ch == '-')
minus = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
{
num = (num << 1) + (num << 3) + ch - '0';
ch = getchar();
}
return num * minus;
}
// get the peak or valley sum
ll getSum(SIT l, SIT r)
{
ll ret = 0;
while (l != r)
{
SIT it = --r;
++r;
if(a[*it] < a[*r])
{
//递增
int curPos = *r;
ret += arr[curPos & 1].query(curPos) - arr[curPos & 1].query(*it);
}
else
{
//递减
int curPos = *it;
ret += arr[curPos & 1].query(*r - 1) - arr[curPos & 1].query(curPos);
//极小值是否需要被加
SIT moreRight = ++r;
--r;
SIT moreLeft = r;
if (moreLeft != s.begin())
moreLeft--;
if (moreRight == s.end())
moreRight --;
if (!((*moreRight - *r) & 1) && !((*r - *moreLeft) & 1))
ret += a[*r];
}
r--;
}
if (a[*l] >= a[*l + 1])
return ret;
//判断边界最小值是否需要加,跟上面完全一样
SIT moreRight = ++r;
--r;
SIT moreLeft = r;
//moreLeft --;
if (moreLeft != s.begin())
moreLeft--;
if (moreRight == s.end())
moreRight --;
if (!((*moreRight - *r) & 1) && !((*r - *moreLeft) & 1))
ret += a[*r];
return ret;
}
inline void checkValid(int pos)
{
if((a[pos] > a[pos - 1]) == (a[pos + 1] > a[pos]))
s.erase(pos);
else
s.insert(pos);
}
void update(int pos, int num)
{
//to get the influenced segment
SIT it = s.lower_bound(pos);
SIT r = it,l=it;
r++,l--;
if (r != s.end() && (*it) == pos)
r++;
if(r == s.end())
r--;
if (l != s.begin())
l--;
ans -= getSum(l, r);
//更新内容
arr[pos & 1].add(pos, num - a[pos]);
a[pos] = num;
checkValid(pos);
if(pos != 1)
checkValid(pos - 1);
if(pos != n)
checkValid(pos + 1);
ans += getSum(l, r);
}
int main()
{
n = re();
arr[0].N = arr[1].N =n + 1;
int tmp = 0;
s.insert(0);
s.insert(n + 1);
for (int i = 1; i <= n; ++i)
{
tmp = re();
update(i,tmp);
}
int q = re();
//for(SIT i = s.begin();i != s.end();++i)
// cerr << *i << ' ';
while(q--)
{
int pos = re(), val = re();
update(pos, val);
printf("%lld\n",ans);
}
return 0;
}