三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

ABC348G 题解

ABC348G 题解

原题链接

前言

这是一篇单调队列二分的题解,并补充了决策单调性的详细证明。为什么一篇单调队列二分的题解都没有呢。虽然代码没有分治好写,但是使用范围更广吧。正好题解通道还没关,写之。

思路

我们当然需要会求 \(k\) 为某一具体值时的答案。首先将 \((A_i,B_i)\)\(B_i\) 升序排序,接下来说的下标都是排序后的下标。枚举 \(B_i\),钦定它为所选 \(B\) 中最大的。接着要求出 \(A_{[1,i]}\) 中前 \(k\) 大之和作为此时的最大值。求某一前缀中前 \(k\) 大元素之和,在可持久化线段树上二分就可以。枚举 \(B_i\)\(O(n)\) 的,线段树上二分是 \(O(\log V)\) 的,总复杂度 \(O(n \log V)\)

接着我们考虑如何求出所有 \(k\) 的答案 \(ans_k\)。我们称使得 \(k\) 答案最大的 \(i\)\(k\) 的决策点(最优点),记为 \(p_k\)。接下来我们证明决策是有单调性的,即

\[p_k \le p_{k+1} \]

如果觉得这个可以感性理解的话,下面的证明可以跳。

\(f(i,k)\) 表示在前 \(i\)\(A\) 中最大的 \(k\)\(A_j\) 之和。再记 \(k\) 的最优点为 \(i\),老是写 \(p_k\) 太累了喵。因为 \(i\) 是最优的,所以有

\[\forall j<i,f(j,k)-b_j < f(i,k)-b_i \]

我们还有一个四边形不等式(具体解释在下面)。

\[\forall j<i,f(j,k+1)-f(j,k)\le f(i,k+1)-f(i,k) \]

不等式左边的值就是 \(A_{[1:j]}\) 中第 \(k+1\) 大的,同理右边是 \(A_{[1:i]}\) 中第 \(k+1\) 大的。\(A_{[1,i]}\) 包含了 \(A_{[1,j]}\) 中的所有数,所以第 \(k+1\) 大的值只可能更大,不可能更小。

把上面两个式子加起来,就得到了这样的式子。

\[\forall j<i,f(j,k+1)-b_j\le f(i,k+1)-b_i \]

这也就是说,为 \(k+1\) 选决策点时,只可能选 \(i\)\(i\) 之后的点,\(i\) 之前的点绝无可能。

这样我们就证明完成了。这是所有与决策单调性有关题目的一般套路,非常值得掌握。


以上是单调队列二分与分治的共同部分,之后是单调队列二分特有的。做之前可以先做模版题 P1912 [NOI2009] 诗人小G,再做这题可以加深理解喵。那道题题解里有关单调队列二分的解释已经很好了,当然看本文的也可以。

我们发现,\(k\) 能选的决策点是所有的 \(j\ge k\),因为小于 \(k\) 的连 \(k\)\(A\) 都选不满。那我们从 \(n\)\(1\) 扫一遍 \(k\),然后依次求出答案。这样的好处是 \(k\) 能选的决策点逐渐增多,方便我们维护。

由上述决策单调性知,对于两个决策点 \(i\)\(i+1\)\(k\) 小于某一个值之前选 \(i\) 更优,跨过这个值之后选 \(i+1\) 更优。所以能将 \(i\) 作为决策点的 \(k_1\),一定是一个区间 \([L_1,R_1]\),而将 \(i+1\) 作为决策点的 \(k_2\),一定是在这个区间右边,并与这个区间相临的 \([L_2,R_2]\),其中 \(R_1 + 1 = L_2\)。换句话说,如果把 \(k\) 的决策点看作一种颜色,\(k \in [1,n]\) 就形成了一个颜色段,且决策点从左到右是逐渐增大的。

具体的,我们在双端队列里维护三元组 \((l,r,i)\),表示此时 \(k \in [l,r]\) 的决策点是 \(i\),保证三元组在队列里按 \(l\) 升序排列。当我们要把一个新的决策点加入队列,因为决策点是从大到小加入的,它掌管的区间的左端点一定是 \(1\)。不断找出此时优先队列中最左的区间 \((l_2,r_2,i_2)\)。计算一下,如果 \(r_2\)\(i\) 作为决策点 比 \(i_2\) 更优,那 \(i_2\) 就被彻底打败了,弹出队列就可以。否则二分一个 \(mid\)\(mid\) 以前选 \(i\) 优,\(mid\) 之后选 \(i_2\) 优,让 \(l_2=mid+1\),再把三元组 \((1,mid,i)\) 加入就可以了。

查询简单,假设我们要查的数是 \(k\) ,查队列中最右边的组 \((l,r,i)\),如果 \(k < l\),那它以后也不会用到,弹掉就可以。如此我们就做完啦。

这部分的代码在下面。

int head = 1 , tail = 1;
que[1] = {1 , n , n};
go(k , n , 1){// query ans_kwhile(k < que[head].l) head++;ans[k] = cal(k , que[head].i);// insert k - 1 as a disicion pointwhile(head <= tail){if(cal(que[tail].r , que[tail].i) <= cal(que[tail].r , k - 1)) tail--; // cal(i,j) 代表 i 选 j 当决策点时的答案。else break;}if(head > tail){que[++tail] = {1 , k - 1 , k - 1};continue;}int l = que[tail].l - 1 , r = que[tail].r;while(l < r){int mid = (l + r + 1) / 2;if(cal(mid , que[tail].i) <= cal(mid , k - 1)) l = mid;else r = mid - 1;}if(l < 1) continue;que[tail].l = l + 1;tail++;que[tail] = {1 , l , k - 1}; 
}
fo(i , 1 , n) cout << ans[i] << "\n";

复杂度分析

显然每个 \(i\) 只会入队出队一次,是 \(O(n)\) 的,算上二分找 \(mid\)\(\log n\),以及函数 cal(i,j) 中线段树上二分的 \(\log V\),总复杂度是和分治一样的 \(O(n \log n \log V)\)。然而实际上我的代码常数很大,在 Atcoder 上会 TLE(呜呜呜最开始还以为写挂了导致死循环)。离散化后变成 \(O(n \log^2 n)\) 过了。

下面是提交链接,可以看完整代码。

不加离散化的分治做法

不加离散化的单调队列二分做法

加离散化的单调队列二分做法

最后

当决策有单调性时,单调数据结构和分治都是常用的优化手段。关键是证明出四边形不等式,之后就只用考虑 \(w(i,j)\) 怎么维护了喵,在本题是用可持久化线段树维护。其他部分是板的。

但是分治只能处理没有依赖性的问题,而单调数据结构可以做,比如动态规划问题,也就是形如 \(f_i = \min_{j<i}f_j+w(i,j)\) 的式子。当然这也不是说分治完全没用,比如这道 CF868F Yet Another Minimization Problem。因为 \(w(i,j)\) 没有办法快速计算,只能依靠类似莫队的指针维护。

← 返回列表