P8868 比赛 题解

传送门

题意简述:给定序列 \(a,b\)\(m\) 次查询,每次询问 \(\displaystyle\sum_{i=l_i}^{r_i}\sum_{j=i}^{r_i}(\max_{k=i}^j a_k\times \max_{k=i}^jb_k)\)

\(n,m\le 2.5\times 10^5\)

【8pts:暴力】

暴力枚举。

【20pts:关注贡献集合的变化,O(1)更新】

离线,右端点排序,假设此时循环到右端点为 \(r\)

定义:\(x_i=\max\limits_{j=i}^r\{a_j\}\)\(y_i\) 类似定义。\(f_i=x_i\times y_i\)

在右端点为 \(r\) 时,我们发现,\(f_i\) 就是当 \(p=i,q=r\) 时的答案。

于是在右端点为 \(r\) 时,\(\displaystyle\sum_{i=l}^rf_i\) 就是当 \(p=l,l+1,\dots,r,q=r\) 时的答案。

再进一步,在右端点为 \(l,l+1,\dots,r\) 的时候,所有 \(\displaystyle\sum_{i=l}^r f_i\) 的和就是询问 \((l,r)\) 的答案。

但是如果我们在当前求 \(f_i\) 时,要找出所有包含 \(f_i\) 的询问累加,实在是太麻烦,太慢了。

因此可以转而在 \(f_i\) 上累加贡献:当右端点为 \(r\) 时,令 \(F_i\) 代表 \(p=i,q=i\sim r\) 时的答案和。(注意,这里变的是右端点,左端点不变

因为左端点不变,所以 \(r\leftarrow r+1\) 时, \(F_i\)\(\sum [i,i\sim r]\) 的部分不会变,我们只需加入 \([i,r+1]\) 的贡献,也就是 \(\displaystyle\max_{j=i}^{r+1}a_j\times \max_{j=i}^{r+1}b_j=x_i\times y_i\),可以 \(O(1)\) 求得。

\(r\) 右移时,更新是:

x[i] = max(x[i], x[r]);
y[i] = max(y[i], y[r]);
F[i] += x[i] * y[i]; //注意这里的 +=

答案就是:

for (int i = l; i <= r; i++)
    ans[id] += F[i];

因为 \(x,y,F\) 都要随着 \(i\) 的右移更新多次,所以 \(x,y,F\) 的更新是 \(O(n^2)\) 的;每次询问要循环,所以询问复杂度 \(O(n)\)。总复杂度 \(O(n^2+nq)\)

【小 Question】

为什么上面 \(F_i\) 定义为 \(p=i,q=i\sim r\) 的答案的和,而非 \(p=i\sim r,q=r\) 的答案的和?

因为这样在 \(r\) 右移的过程中,\(F_i\) 的定义中 \(p,q\) 都与 \(r\) 有关系,我们就需要补充 \([i\sim r+1,r+1]\) 的贡献。前一种定义只需要补充 \([i,r+1]\) 的贡献,因为 \(p\)\(r\) 无关。

【100pts】

一个很显眼的优化就是可以用线段树维护 \(x,y\):先用单调栈预处理每个数左边大于它的数,然后区间赋值。

接下来尝试用线段树维护 \(F_i\),这样对于询问 \([l,r]\),我们能 \(O(\log n)\) 的求出 \(\sum F_i\)

我们只有三种操作:区间赋值 \(x\),区间赋值 \(y\),区间加上这个区间内所有单个位置的 \(x\times y\)(简记为区间 \(+x\times y\))。

考虑我们要对一个节点保存什么信息:

  1. 答案 \(ans\),也就是这个区间的 \(F_i\) 和。

  2. 区间内每个位置 \(X\times Y\) 的和 \(sumxy\),这样方便执行区间 \(+x\times y\)

  3. 区间内 \(x\) 的和 \(sumx\),区间内 \(y\) 的和 \(sumy\)。这样在区间赋值 \(x,y\) 的时候,能快速更新 \(sumxy\)

然后考虑一个标记保存什么信息:

  1. \(setx,sety\) 显然需要,即区间赋值 \(x,y\) 的标记。

  2. \(addxy\),即这个区间应当加上 \(addxy\)\(x\times y\)

虽然上面两个标记已经包含了题目的信息,但是当两个标记碰在一起的时候,我们发现无法规定一个合适的优先级,使得两个标记能等价地合并成一个标记。

比如:当我们有一个 \(+1\;x\times y\) 的标记,但是遇到了 \(setx\) 标记。此时的 \(addxy\) 如果还是 \(1\),加出来的 \(x\times y\) 就是 \(set\) 之后的 \(x\times y\) 了,肯定不对;但是这一个 \(+1\;x\times y\) 的增加也不能丢掉,不然答案又少了。

这样就卡住了。

那怎么办呢?当你发现线段树的标记无法完美合并的时候,可以考虑一下多记录一些信息。

考虑我们上面遇到的问题:当 \(addxy\) 遇到 \(set\) 的时候,没有地方可以存原先的 \(addxy\) 了!

因此我们考虑设置一个暂存点:\(addC\),表示这个区间每一个位置的答案都要加上 \(addC\) 这么多数值,总答案也就是加上 \(addC\times len\) 的数值。

错误示范:如果 \(addxy\) 遇到 \(set\),我们可以调用这个标签所在的节点的 \(x\times y\) 值,加入 \(addC\) 中,同时 \(addxy\leftarrow 0\)

这是不对的!

线段树的 Tag*Tag,一定不能涉及到 Val 的属性!!!

因为当我们 pushdown 的时候,Val 的属性就会变化,Tag 也要跟着变,太麻烦了!

发现增加了一个 \(addC\) 还是不能合并,原因在于:如果 pushdown 了,\(addC\) 不能同时加到两个子区间上,因为这个 \(addC\) 是大区间的 \(addC\),小区间需要根据小区间自己的 \(sumx,sumy\) 增加。

既然小区间需要根据小区间的 \(sumx,sumy\) 打标记,我们自然想到在 Tag 上再额外记录两个信息:\(addx,addy\),表示这个区间需要 \(+addx\times sumx,+addy\times sumy\)

这样就对了,当 \(addxy\) 遇到 \(set\) 的时候,可以通过 \(set\) 的值,把增加的倍数加入 \(sumx,sumy\) 里面。

同时,我们规定:一个 Tag,加法操作的优先级高于赋值操作。比如 Tag:adx=3,covx=2,加的是原来的sumx,不是covx*len

【总结】

Val 包含的信息:

  1. ans,答案;

  2. sumX,区间内 X 的和;

  3. sumY,区间内 Y 的和;

  4. sumXY,区间内 X*Y 的和。

Tag 包含的信息(加法的优先级高于赋值):

  1. setx, sety:X,Y 的赋值信息;

  2. addXY:这个区间加了多少次 X*Y;

  3. addX:这个区间加了多少次 sumX;

  4. addY:这个区间加了多少次 sumY;

  5. addC:这个区间的答案要加上 addC*len。

所以一个区间的答案就是 \(ans+addXY\times sumXY+addX\times sumX+addY\times sumY+addC\times len.\)

Tag * Tag 怎么写?用标记队列的思想。

posted @ 2024-02-19 10:56  FLY_lai  阅读(4)  评论(0编辑  收藏  举报