如题。
该题目需要在线段树上维护区间内最大值、最大值个数、严格次大值、严格次大值个数以及区间和。合并时采用归并。我采用如下实现:
using pint = pair <int, int>; // pint { 最大值, 计数 }
using ll = long long;
#define fst first
#define scd second
struct info { pint mx[2]; ll sum; } dta[N<<2];
info merge (const info &lft, const info &rht) const {
static pint mx[4];
const pint *lmx = lft.mx, *rmx = rht.mx,
*p = lmx, *q = rmx, *act; pint *pt = mx-1;
memset (mx, 0, sizeof mx);
while (p < lmx+2 || q < rmx+2) {
if (q >= rmx+2 || (p < lmx+2 && p->fst >= q->fst))
act = p++;
else act = q++;
if (pt >= mx && pt->fst == act->fst)
pt->scd += act->scd;
else *++pt = *act;
}
return {{ mx[0], mx[1] }, lft.sum + rht.sum };
}
我们保证,任何子节点均定义有严格次大值(不存在即设置为 -INF),故而函数主体执行完后必然有 pt >= mx + 1 为真,return 语句始终访问在上文中被赋值过的节点。从而得出,上文中 memset (mx, 0, sizeof mx) 是完全没有必要写的;但写了亦不影响最终结果。
然而,在 MinGW GCC 4.8.2 和 MinGW GCC 9.4.0 上,用编译参数 -std=c++14 -O2 -Wall 和 -std=c++14 -Wall 分别编译后,运行输出的结果不同。我已经排除了代码中其他任何部分的可能错误;通过 -fsanitize=address/leak/undefined 和静态查错均未发现 undefined behavior 和内存泄漏。我承认上文中 pint *pt = mx-1 的写法相当危险,但可以看到下文中有 pt >= mx 的检查。 有四点可以佐证:
一、容易发现下列写法是完全等价的:
info merge (const info &lft, const info &rht) const {
static pint mx[4];
const pint *lmx = lft.mx, *rmx = rht.mx,
*p = lmx, *q = rmx, *act; int idx = -1;
memset (mx, 0, sizeof mx);
while (p < lmx+2 || q < rmx+2) {
if (q >= rmx+2 || (p < lmx+2 && p->fst >= q->fst))
act = p++;
else act = q++;
if (~idx && mx[idx].fst == act->fst)
mx[idx] += act->scd;
else mx[++idx] = *act;
}
return {{ mx[0], mx[1] }, lft.sum + rht.sum };
}
仅仅将指针换作下标而已。但这时我们开启 -O2,会发现它得到正确答案。
二、Ubuntu 20.04 上使用 clang-10 以相同参数编译第一份代码,输出正确答案。
三、我们将 memset (mx, 0, sizeof mx) 替换为
memset (mx, 0, sizeof mx);
fprintf (stderr, "%d", mx[0].fst);
我们相信它对答案没有任何影响,对吧?只是向标准错误输出了些无用的“0”。但一旦添加这句毫无关联的语句,用 gcc 编译也能得到正确的输出。
四、直接把 memset 删了,输出正确答案。
我使用 Compiler Explorer 查看二者的汇编代码,发现 memset 对应的部分在不开启 -O2 时会有传参以及 call memset 的操作;但开启 -O2 后只有 movaps。我并不理解这几句汇编的意思。
提出我的猜想:由于我使用的指针 pt 在一开始并未指向 mx 数组的任何元素,且下文中也没有明确调用 mx,gcc 帮我“优化”掉了和 mx 相关的所有操作;而使用下标 idx(第二份代码),则明确地指出需要访问 mx 中元素,故而 gcc 老实地作 memset。
但问题在于:
如果您有兴趣查看完整的代码和题面,可以私信我。