题解:P12569 [UOI 2023] An Array and Partial Sums
题意
给定一个长度为 \(n\) 的序列 \(a\)。你可以进行以下三种操作:
- 把 \(a\) 中所有元素取相反数。
- 选择 \(a\) 的一个子段,替换成其前缀和数组。
- 选择 \(a\) 的一个子段,替换成其后缀和数组。
求最少的操作次数使得 \(a\) 中所有元素非负,并给出一个合法的操作序列。\(1\leq n\leq 2\times 10^5\),\(-1\leq a_i\leq 1\)。
题解
分类讨论题。
显然答案不会很大。我们首先指出答案不超过 \(3\)。
证明:直接构造证明。设前缀和数组 \(pre_i=\sum\limits_{j=1}^ia_j\),令 \(x,y\) 分别表示 \(pre\) 的最靠前的最小值和最大值的位置。我们按 \(x,y\) 的大小关系讨论:
- \(x=y\):说明 \(pre\) 中所有元素相同,于是 \(a_1=pre_1\),\(a_2=a_3=\cdots=a_n=0\)。若 \(a_1\geq 0\) 则无需操作,否则执行 \(1\) 次操作 \(1\) 即可。
- \(x>y\):我们执行操作 \(1\),化为 \(x<y\) 的情况。
- \(x<y\):我们先对 \(a[x+1,n]\) 执行操作 \(2\),那么被操作的数 \(a_i\leftarrow pre_i-pre_x\),一定是非负的。然后我们发现,在操作后的新的 \(pre'\) 数组中,\(pre'_y\) 一定是前缀最大值。这是因为
\[\begin{align*} pre'_y&=pre_x+\sum_{i=x+1}^y(pre_i-pre_x)\\ &=pre_y+\sum_{i=x+1}^{y-1}(pre_i-pre_x)\\ &\geq pre_y \end{align*} \]因此对于 \(1\leq i\leq x\),\(pre'_i=pre_i\leq pre_y\leq pre'_y\)。而对于 \(x<i\leq y\),显然在这段区间内 \(pre'\) 单调不减,于是同样有 \(pre'_y\geq pre_i\)。那么我们只需再对 \([1,y]\) 执行操作 \(3\) 即可。被操作的数 \(a_i\leftarrow pre'_y-pre'_i\),必然是非负的。\(\Box\)
接下来我们对答案 \(ans\) 进行分类讨论。
\(ans=0\)
此时 \(a\) 中所有元素已经非负了。
\(ans=1\)
首先显然判掉 \(a\) 中所有元素非正的情况,此时只需执行 \(1\) 次操作 \(1\)。
否则我们一定是选择了一个区间 \([l,r]\),对 \(a[l,r]\) 执行操作 \(2/3\)。注意到合法的区间 \([l,r]\) 必然满足 \(a[1,l-1]\) 和 \(a[r+1,n]\) 中的元素都非负,而我们把这些元素包含到操作区间里必然不劣,所以我们一定是执行全局操作。那么判断 \(a\) 的前缀和/后缀和数组是否非负即可。
\(ans=2\)
我们有可能执行了 \(1\) 次操作 \(1\),这种 case 化成 \(ans=1\) 的情况即可。
接下来考虑最后一次操作,不失一般性地,我们设最后一次执行了操作 \(2\)。根据 \(ans=1\) 的 case 的分析,这次操作我们一定是执行全局操作。所以问题转化为,能否在选择一个子段 \(a[l,r]\) 执行操作 \(2/3\) 后,使得 \(a\) 的前缀和数组非负。
我们来分类讨论第一次操作的类型。
若第一次操作是操作 \(2\),和前面类似,可以发现我们必然是对一个前缀执行操作。因为合法的区间 \([l,r]\) 要求 \(pre[1,l-1]\) 非负,所以我们把这部分包含到操作区间里同样不劣。
枚举操作的前缀 \([1,i]\),用线段树维护前缀和数组,每次 \(i\leftarrow i+1\) 就是 \(a\) 上的单点修改,对应于线段树上的后缀加,然后查询全局最小值判断是否合法即可。
接下来是比较复杂的 case:第一次操作是操作 \(3\)。设 \(pre\) 的后缀和数组 \(S_i=\sum\limits_{j=i}^npre_j\),再设操作区间为 \([l,r]\)。那么被操作的数 \(a_i\leftarrow pre_r-pre_{i-1}\)。分段考察新的 \(pre'\) 的取值:
- \(1\leq i<l\):\(pre'_i=pre_i\)。
- \(l\leq i\leq r\):化一下式子:\[\begin{align*} pre'_i&=pre_{l-1}+\sum_{j=l}^i(pre_r-pre_{j-1})\\ &=(i-l+1)pre_r-\sum_{j=l+1}^{i}pre_{j-1}\\ &=(i-l+1)pre_r-S_l+S_i \end{align*} \]
- \(r<i\leq n\):利用 \(l\leq i\leq r\) 推的式子,不难得到\[\begin{align*} pre'_i&=(r-l+1)pre_r-S_l+S_r+pre_i-pre_r\\ &=(r-l+1)pre_r-S_l+S_{r+1}+pre_i \end{align*} \]
根据这些讨论刻画怎样的区间 \([l,r]\) 是合法的:
考虑枚举右端点 \(r\),尝试找到合法的 \(l\) 与之匹配。
对于第 \(1\) 个条件,我们找到最小的 \(p\),使得 \(pre_p<0\),限制 \(l\leq p\) 即可。
对于第 \(3\) 个条件,我们把每个可能的 \(l\) 视作一条直线 \(y=(l-1)x+S_l\),那么每次只需查询 \(x=pre_r\) 处 \(y\) 的最小值。显然可以用李超线段树维护。而注意到每次插入的直线斜率单调递增,实际上可以直接使用单调栈维护上凸壳,二分查询最小值。
难搞的是第 \(2\) 个条件,因为右边的 \(\min\) 和 \(l,r\) 都有关。我们先假设 \(l\leq p\leq r\),令 \(f(i)=ipre_r+S_i\),把第二个条件拆成
对于前者,一个显然的必要条件是 \(pre_r\geq 0\),然后不难看出取 \(l=\operatorname*{arg\,min}\limits_{i=1}^{p-1}f(i)\) 一定合法。注意这里的 \(l\) 和前面处理第 \(3\) 个条件时得到的 \(l\) 是一样的,不需要额外维护。
对于后者,不等式右侧只与 \(r\) 有关,因此此时同样取 \(l=\operatorname*{arg\,min}\limits_{i=1}^{p-1}f(i)\) 一定是最优的。因此同样使用单调栈维护上凸壳求出 \(\min\limits_{i=p}^rf(i)\),判断是否合法即可。
如果 \(r<p\),我们直接忽略拆出来的第二个条件即可。
这样我们就处理完了所有条件。
\(ans=3\)
用前文中的方法构造即可。
时间复杂度 \(\mathcal{O}(n\log{n})\)。
代码
这里只给出 \(ans=2\) 且两次执行相同操作部分的代码。其他部分是很好实现的。
struct Line {
int id;
ll k, b;
ll operator()(ll x) const { return k * x + b; }
};
inline bool check(Line a, Line b, Line c) { return (a.b - b.b) * (c.k - b.k) > (b.b - c.b) * (b.k - a.k); }
inline bool check(Line a, Line b, ll x) { return (b.k - a.k) * x <= a.b - b.b; }
struct Convex {
int top;
Line stk[N];
inline void init() { top = 0; }
inline void ins(Line l) {
while (top >= 2 && !check(stk[top - 1], stk[top], l)) --top;
stk[++top] = l;
}
inline Line query(ll x) {
int l = 1, r = top;
while (l < r) {
int mid = l + r + 1 >> 1;
if (mid == 1 || check(stk[mid - 1], stk[mid], x)) l = mid;
else r = mid - 1;
}
return stk[l];
}
} c1, c2;
inline bool solve2(bool flag) {
for (int i = n; i; --i) S[i] = S[i + 1] + pre[i];
suf[n + 1] = INF;
for (int i = n; i; --i) suf[i] = min(suf[i + 1], pre[i]);
int p = 0;
for (int i = 1; i <= n; ++i) if (pre[i] < 0) { p = i; break; }
c1.init(), c2.init();
for (int r = 1; r <= n; ++r) {
if (r <= p) c1.ins({r, r - 1, S[r]});
if (r >= p) c2.ins({r, r, S[r]});
if (pre[r] < 0) continue;
Line res = c1.query(pre[r]);
int id = res.id;
ll val = res(pre[r]), mn = (ll)r * pre[r] + S[r + 1] + suf[r + 1];
if (r >= p) chk_min(mn, c2.query(pre[r])(pre[r]));
if (val <= mn) {
if (!flag) op[++sz] = {3, id, r}, op[++sz] = {2, 1, n};
else op[++sz] = {2, n + 1 - r, n + 1 - id}, op[++sz] = {3, 1, n};
return print(), 1;
}
}
return 0;
}

浙公网安备 33010602011771号