题解:P17241 [IOI 2026] 纪念碑 / monuments

· · 题解

题意简述

数轴上有 N 座古迹,其中一部分古迹不能移动。我们可以移动其余古迹,移动费用等于移动距离。

最终要求关于原点对称:对每个 x>0,坐标 x-x 上的古迹数量必须相等。坐标 0 上可以有任意多座古迹。

求最小总费用;如果无论如何都无法满足要求,返回 -1

本题是函数式交互题,需要实现:

long long get_cost(vector<int> X, vector<int> P);

思路

这道题的推导较长,可以先记住整条主线:

  1. 先抵消已经对称的固定古迹,找出必须补上的“左缺口”和“右缺口”。
  2. 先假设所有可移动古迹都经过原点,得到一个基准费用。
  3. 利用古迹的初始位置,可以少走一部分路。于是“求最小费用”变成“求最大节省量”。
  4. 节省量可以表示成一条四层链上的最大权匹配。
  5. 再把匹配改写成选子集问题,用贪心逐个加入边际收益最大的元素。
  6. 最后用三棵线段树快速维护所有边际收益。

下面逐步展开。

1. 固定古迹留下了哪些缺口

固定古迹不能移动,所以先单独处理它们。

对每个半径 d>0,比较固定古迹在 -dd 的数量。两边各拿出相同数量后,这些古迹已经互为镜像,可以直接忽略。

如果 d 处还多出若干座固定古迹,那么必须在 -d 处补上同样多的可移动古迹。反过来也一样。

为此定义四个多重集合:

例如,固定古迹在 -51 座,在 53 座,那么 5 这个半径会在 A 中出现两次,表示还需要在 -5 补两座古迹。

再记:

每个缺口至少需要一座可移动古迹,所以 D>K 时一定无解。

如果 D\le K,则一定有解:任选 D 座可移动古迹补齐所有缺口,其余古迹全部移到原点即可。因此,无解的判定恰好就是 D>K

初始位于原点的可移动古迹不放入 LR。它们虽然不能带来费用上的节省,但仍然计入 K,可以补缺口,也可以留在原点。

2. 先设计一个基准方案

直接优化移动方案很难。我们先设计一个简单但不一定最优的方案,再计算最多能省下多少钱。

基准方案如下:

  1. 所有可移动古迹先移到原点,费用为各自初始坐标的绝对值之和。
  2. A\cup B 中的每个缺口,再把一座古迹从原点移到目标位置,费用等于该缺口的半径。

因此基准费用为

C_0=\sum_{i\text{ 可移动}}|X_i|+\sum_{d\in A\cup B}d.

接下来不再直接计算最终费用,而是计算:利用古迹原来的位置,最多可以从 C_0 中省下多少。

同侧古迹直接补缺口

假设一座可移动古迹原来在 -x,现在要补左侧半径为 y 的缺口,即移动到 -y

在基准方案中,它先从 -x0,再从 0-y,费用为 x+y。如果直接从 -x 移到 -y,费用只有 |x-y|

所以节省量为 x+y-|x-y|=2\min(x,y)

正侧同理。也就是说:

左右两座可移动古迹互相配对

再考虑两座可移动古迹,它们原来分别位于 -xy

如果让它们最终成为一对互为相反数的古迹,最小移动费用是 |x-y|。基准方案会把两者都移到原点,费用为 x+y,所以这里同样可以节省 2\min(x,y)

至于其他配对方式:

因此,所有可能产生正节省的配对正好组成下面这条四层链:

A-L-R-B.

相邻两层之间可以配对。一对绝对值为 x,y 的元素,把匹配权值记为 min(x,y);真正省下的费用是匹配权值的两倍。

中间最多能配多少对

每条 L-R 边会占用两座可移动古迹。除此以外,D 个固定缺口还需要各占用一座可移动古迹。

若选了 zL-R 边,就必须满足 D+2z\le K,所以

没有通过同侧匹配获得节省的缺口,可以交给任意剩余古迹补上。因此,除了这个数量上限,不再有其他限制。 至此,原问题已经变成:在链 $A-L-R-B$ 上求最大权匹配,其中 $L-R$ 边最多选 $C$ 条。 ## 3. 两个集合之间如何取得最大匹配权值 后面会反复遇到同一个小问题:给定两个非负整数多重集合 $U,V$,从两边各选一个元素配对,一对 $x,y$ 的权值为 $min(x,y)$,最大总权值是多少? 记这个答案为 $Phi(U,V)$。 做法很简单:把两个集合都按从大到小排序,然后同位置配对。若排序后得到 $u_1\ge u_2\ge\cdots$ 和 $v_1\ge v_2\ge\cdots$,则 $\Phi(U,V)=\sum_i\min(u_i,v_i)$, 其中只计算前 $min(|U|,|V|)$ 对。 下面换一种角度解释这个结论,同时为后面的优化做准备。 对任意 $t\ge0$,令 $U_t$ 表示 $U$ 中不小于 $t$ 的元素个数,$V_t$ 同理。权值至少为 $t$ 的匹配边,最多只有 $min(U_t,V_t)$ 条。降序同位置配对会对每个 $t$ 同时达到这个上界,因此它的总权值最大。 把每条边的权值看成从高度 $0$ 累积到高度 $min(x,y)$,便得到 $$ \Phi(U,V)=\int_0^\infty\min(U_t,V_t)\,dt. $$ 读者不必把这里的积分理解得过于复杂。由于所有半径都是整数,它本质上只是把每一段高度区间的贡献相加。 ## 4. 只需决定哪些 $L$ 元素拿去配对 $R

A-L-R-B 的中间是 L-R。我们从 L 中选出一个多重子集 S,表示这些元素不再用于补左缺口,而是拿去与 R 配对。

一旦 S 确定,剩下的匹配就分成两个互不影响的问题:

因此固定 S 后,最大匹配权值为

F(S)=\Phi(L\setminus S,A)+\Phi(R,B\cup S).

这个表达式与原来的链匹配完全等价。

从链上的任意匹配出发,把所有 L-R 边的左端放进 S,就能得到上式。反过来,如果放进 S 的某个元素最后没有与 R 匹配,那么把它从 S 删除也不会变差。

所以我们只需在 |S|\le C 的条件下最大化 F(S)

S=\varnothing 时,匹配权值为

这部分可以直接排序计算。剩下的问题是:怎样选择至多 $C$ 个 $L$ 中的元素,使额外收益最大? ## 5. 选择一个元素能增加多少收益 继续使用“按高度统计数量”的想法。 对每个 $t\ge0$,定义: - $L_t,R_t,A_t,B_t$:相应集合中不小于 $t$ 的元素数量; - $s_t$:当前已经选入 $S$,且不小于 $t$ 的元素数量。 因为 $S$ 中的元素从 $L$ 移到了 $R$ 的匹配对象一侧,所以 $$ F(S)=\int_0^\infty \left( \min(L_t-s_t,A_t)+\min(R_t,B_t+s_t) \right)dt. $$ 现在考虑把一个绝对值为 $h$ 的元素加入 $S$。 它只会影响 $0\le t\le h$ 的部分,因为只有这些高度满足 $h\ge t$。在某个固定高度 $t$ 上,这次选择可能带来两种变化: - $R$ 一侧多了一个匹配对象,可能使收益增加 $1$; - $L$ 一侧少了一个匹配对象,可能使收益减少 $1$。 为了描述这两种情况,定义 $p_t=R_t-B_t-s_t$,$q_t=L_t-A_t-s_t$。 其中: - $p_t>0$ 表示在高度 $t$ 上,$R$ 还有多余元素,可以利用新加入的匹配对象,因此收益增加 $1$; - $q_t>0$ 表示在高度 $t$ 上,$L$ 还有富余,拿走一个元素不会损失收益;否则会损失 $1$。 于是,加入 $h$ 的边际收益为 $$ \Delta_S(h)=\int_0^h \left([p_t>0]+[q_t>0]-1\right)dt. $$ 方括号表示:条件成立时取 $1$,否则取 $0$。 因此,每一段高度对边际收益的贡献只可能是: - $1$:右侧需要它,左侧又有富余; - $0$:一边获益,另一边损失,正好抵消; - $-1$:右侧用不上它,左侧还会因此受损。 这就是后面线段树真正需要维护的量。 ## 6. 为什么“每次选当前最赚的”是正确的 先看直观原因。 随着选入 $S$ 的元素越来越多,每个高度上的 $p_t,q_t$ 都只会减小。因此,这个高度对下一次选择的贡献只会从 $1$ 降到 $0$,再降到 $-1$,不会重新变大。 也就是说,同一段高度越早使用越划算。这种“边际收益只会下降”的结构,使得最优解可以按大小一层一层扩展:先得到选 $1$ 个元素的最优解,再从它扩展到选 $2$ 个元素的最优解,以此类推。 下面给出严格的交换论证。不关心证明细节的读者可以直接跳到下一节。 把固定高度 $t$ 上对 $F$ 的贡献写成关于 $s_t$ 的函数 $f_t(s_t)$。由上面的分析,$s_t$ 每增加 $1$ 时,边际收益只会下降,所以 $f_t$ 是离散凹函数。 取两个选择集合 $S,T$,满足 $|S|<|T|$。删去它们共有的元素后,在 $T\setminus S$ 中选择绝对值最小的元素 $h$。 对每个 $0\le t\le h$,集合 $T\setminus S$ 中的所有元素都不小于 $t$。又因为 $|T\setminus S|>|S\setminus T|$,所以 $T$ 在这个高度选入的元素数严格多于 $S$,即 $s_T(t)>s_S(t)$。 离散凹性说明:在较小的计数 $s_S(t)$ 上再增加 $1$,得到的收益不会小于从较大的计数 $s_T(t)$ 中删除最后一个元素所损失的收益。因此 $$ f_t(s_S(t)+1)-f_t(s_S(t)) \ge f_t(s_T(t))-f_t(s_T(t)-1). $$ 当 $t>h$ 时,加入或删除 $h$ 都不会改变计数。把所有高度的贡献相加,得到交换不等式 $$ F(S\cup\{h\})+F(T\setminus\{h\})\ge F(S)+F(T). $$ 现在令 $S_k$ 是大小为 $k$ 的最优集合,$T$ 是大小为 $k+1$ 的最优集合。上式中的某个 $h\in T\setminus S_k$ 满足 $F(S_k\cup\{h\})+F(T\setminus\{h\})\ge F(S_k)+F(T)$。 因为 $S_k$ 已经是大小为 $k$ 的最优解,所以 $F(S_k)\ge F(T\setminus\{h\})$。代回上式可得 $F(S_k\cup\{h\})\ge F(T)$。而 $T$ 已经是大小为 $k+1$ 的最优解,因此 $S_k\cup\{h\}$ 也是大小为 $k+1$ 的最优解。 这证明了:存在一串互相包含的最优解。具体地说,如果当前集合已经是大小为 $k$ 的最优解,那么至少有一个元素能把它扩展为大小为 $k+1$ 的最优解。我们选择的元素具有最大的边际收益,它不会比这个可扩展元素差;同时又不可能超过大小为 $k+1$ 的最优值。因此,选择最大边际收益的元素也一定能得到大小为 $k+1$ 的最优解。 于是从空集开始,每次加入当前边际收益最大的元素,就能依次得到选择 $1,2,3,\ldots$ 个元素时的最优解。 我们最多选择 $C$ 个元素。如果当前最大边际收益已经不为正,那么继续选择只会让答案不变或变差,可以立刻停止。 ## 7. 离散化:把积分变成若干段长度之和 朴素计算所有候选的边际收益仍然太慢。接下来利用坐标离散化快速维护它们。 把 $L,R,A,B$ 中出现的所有正半径与 $0$ 一起排序去重: $0=v_0<v_1<\cdots<v_{Q-1}$。 对区间 $I_i=(v_i,v_{i+1}]$,所有“不小于 $t$ 的元素数量”都相同。记: - $w_i=v_{i+1}-v_i$,即这段区间的长度; - $p_i,q_i$ 为这段区间上的 $p_t,q_t$; - $g_i=[p_i>0]+[q_i>0]-1$。 那么,选择半径 $v_k$ 的边际收益就是它覆盖的所有区间贡献之和: $$ \Delta_S(v_k)=\sum_{i=0}^{k-1}g_iw_i. $$ 选择半径 $v_k$ 后,会发生什么? 由于这个元素不小于所有 $t\le v_k$,所以对所有 $i<k$,$s_i$ 都增加 $1$,从而 $p_i,q_i$ 都减少 $1$。 但我们不需要维护 $p_i,q_i$ 的完整数值,只关心它们是否仍然大于 $0$: - 若某个 $p_i$ 从 $1$ 变成 $0$,则 $[p_i>0]$ 从 $1$ 变成 $0$,所以 $g_i$ 减少 $1$; - $q_i$ 同理; - 一旦某个指示量变成 $0$,之后即使对应计数继续减小,也不会再次改变 $g_i$。 因此,每个区间在 $p$ 和 $q$ 两部分中各至多产生一次有效变化。 如果 $g_i$ 减少 $1$,所有半径至少为 $v_{i+1}$ 的候选都会覆盖区间 $I_i$,所以它们的边际收益都减少 $w_i$。在候选下标上,这正是对后缀 $[i+1,Q-1]$ 加上 $-w_i$。 ## 8. 三棵线段树分别维护什么 代码中的 `extra_matching_gain` 使用三棵线段树。 ### 前两棵:找出刚刚失效的区间 两棵 `PositiveCounterTree` 分别维护仍然为正的 $p_i$ 和 $q_i$。 选择半径下标 $k$ 后,需要把 $[0,k-1]$ 中的计数全部减一。线段树维护最小值,因此可以找出哪些叶子恰好降到了 $0$。 一个叶子第一次降到 $0$ 后就被永久删除,因为之后只关心“是否大于 $0$”,不再关心它会继续降到 $-1,-2$ 还是更小。 ### 第三棵:维护所有候选的边际收益 `MaximumTree` 维护每个离散半径当前的 $Delta_S$: - 根节点直接给出当前最大的边际收益及其半径下标; - 某个区间的 $g_i$ 减少 $1$ 时,对候选后缀 $[i+1,Q-1]$ 加上 $-w_i$; - 同一个半径可能对应多座可移动古迹,因此用 `available` 记录该半径还剩多少个候选;只有数量变为 $0$ 时,才禁用对应叶子。 每轮取出全局最大边际收益。若它不为正,或者已经选择了 $C$ 个元素,就结束。 设这些选择带来的额外匹配权值为 $G$。再加上不选中间边时已经得到的 $Phi(L,A)+\Phi(R,B)$,最终最大匹配权值为 $\Phi(L,A)+\Phi(R,B)+G$。 匹配权值的两倍才是真正节省的费用,所以答案为 $$ C_0-2\left(\Phi(L,A)+\Phi(R,B)+G\right). $$ ## 9. 完整算法 1. 标记所有固定古迹。 2. 按绝对值统计固定古迹在左右两侧的数量差,构造左缺口 $A$ 和右缺口 $B$。 3. 收集负侧、正侧可移动古迹的绝对值,得到 $L,R$;同时计算基准费用 $C_0$。 4. 若 $|A|+|B|>N-M$,返回 $-1$。 5. 将两边分别降序排序,计算基础匹配权值 $Phi(L,A)+\Phi(R,B)$。 6. 令 $C=\lfloor(N-M-|A|-|B|)/2\rfloor$,用三棵线段树贪心选择至多 $C$ 个 $L$ 中的元素,计算额外匹配权值 $G$。 7. 返回 $C_0-2(\Phi(L,A)+\Phi(R,B)+G)$。 # 复杂度分析 排序、离散化和建立线段树需要 $O(N\log N)$ 时间。 贪心最多选择 $O(N)$ 个元素。每个元素带来常数次线段树操作;每个离散区间在前两棵线段树中各至多失效一次。因此,所有维护操作的总时间也是 $O(N\log N)$。 总时间复杂度为 $O(N\log N)$,空间复杂度为 $O(N)$。 答案可能达到 $10^{15}$ 量级,所以费用、收益和边际收益都必须使用 `long long`。 # 参考代码 下面的代码实现题目要求的 `get_cost`,不包含 `main`。 ```cpp #include <bits/stdc++.h> using namespace std; namespace { constexpr int INF_INT = 1'000'000'000; constexpr long long NEG_INF = -(1LL << 60); struct PositiveCounterTree { int n = 0; vector<int> mn, lazy; PositiveCounterTree() = default; explicit PositiveCounterTree(const vector<int>& value) { init(value); } void init(const vector<int>& value) { n = (int)value.size(); if (n == 0) return; mn.assign(4 * n, INF_INT); lazy.assign(4 * n, 0); build(1, 0, n - 1, value); } void build(int p, int l, int r, const vector<int>& value) { if (l == r) { mn[p] = value[l] > 0 ? value[l] : INF_INT; return; } int m = (l + r) / 2; build(p * 2, l, m, value); build(p * 2 + 1, m + 1, r, value); pull(p); } void pull(int p) { mn[p] = min(mn[p * 2], mn[p * 2 + 1]); } void apply(int p, int delta) { if (mn[p] == INF_INT) return; mn[p] += delta; lazy[p] += delta; } void push(int p) { if (lazy[p] == 0) return; apply(p * 2, lazy[p]); apply(p * 2 + 1, lazy[p]); lazy[p] = 0; } void collect_zero(int p, int l, int r, vector<int>& crossed) { if (mn[p] != 0) return; if (l == r) { mn[p] = INF_INT; lazy[p] = 0; crossed.push_back(l); return; } push(p); int m = (l + r) / 2; collect_zero(p * 2, l, m, crossed); collect_zero(p * 2 + 1, m + 1, r, crossed); pull(p); } void decrement_prefix(int right, vector<int>& crossed) { if (n == 0 || right < 0) return; decrement(1, 0, n - 1, 0, min(right, n - 1), crossed); } void decrement(int p, int l, int r, int ql, int qr, vector<int>& crossed) { if (qr < l || r < ql || mn[p] == INF_INT) return; if (ql <= l && r <= qr) { apply(p, -1); collect_zero(p, l, r, crossed); return; } push(p); int m = (l + r) / 2; decrement(p * 2, l, m, ql, qr, crossed); decrement(p * 2 + 1, m + 1, r, ql, qr, crossed); pull(p); } }; struct MaximumTree { int n = 0; vector<long long> mx, lazy; vector<int> where; MaximumTree() = default; explicit MaximumTree(const vector<long long>& value) { init(value); } void init(const vector<long long>& value) { n = (int)value.size(); mx.assign(4 * n, NEG_INF); lazy.assign(4 * n, 0); where.assign(4 * n, -1); build(1, 0, n - 1, value); } void build(int p, int l, int r, const vector<long long>& value) { if (l == r) { mx[p] = value[l]; where[p] = l; return; } int m = (l + r) / 2; build(p * 2, l, m, value); build(p * 2 + 1, m + 1, r, value); pull(p); } void pull(int p) { if (mx[p * 2] >= mx[p * 2 + 1]) { mx[p] = mx[p * 2]; where[p] = where[p * 2]; } else { mx[p] = mx[p * 2 + 1]; where[p] = where[p * 2 + 1]; } } void apply(int p, long long delta) { mx[p] += delta; lazy[p] += delta; } void push(int p) { if (lazy[p] == 0) return; apply(p * 2, lazy[p]); apply(p * 2 + 1, lazy[p]); lazy[p] = 0; } void add_suffix(int left, long long delta) { if (left >= n) return; add(1, 0, n - 1, max(left, 0), n - 1, delta); } void add(int p, int l, int r, int ql, int qr, long long delta) { if (qr < l || r < ql) return; if (ql <= l && r <= qr) { apply(p, delta); return; } push(p); int m = (l + r) / 2; add(p * 2, l, m, ql, qr, delta); add(p * 2 + 1, m + 1, r, ql, qr, delta); pull(p); } void disable(int index) { set_value(1, 0, n - 1, index, NEG_INF); } void set_value(int p, int l, int r, int index, long long value) { if (l == r) { mx[p] = value; lazy[p] = 0; return; } push(p); int m = (l + r) / 2; if (index <= m) set_value(p * 2, l, m, index, value); else set_value(p * 2 + 1, m + 1, r, index, value); pull(p); } pair<long long, int> best() const { return {mx[1], where[1]}; } }; long long matched_value(vector<int> a, vector<int> b) { sort(a.begin(), a.end(), greater<int>()); sort(b.begin(), b.end(), greater<int>()); long long result = 0; for (int i = 0; i < (int)min(a.size(), b.size()); ++i) result += min(a[i], b[i]); return result; } long long extra_matching_gain(const vector<int>& left, const vector<int>& right, const vector<int>& target_left, const vector<int>& target_right, int limit) { limit = min<int>(limit, min(left.size(), right.size())); if (limit <= 0 || left.empty() || right.empty()) return 0; vector<int> coord = {0}; coord.insert(coord.end(), left.begin(), left.end()); coord.insert(coord.end(), right.begin(), right.end()); coord.insert(coord.end(), target_left.begin(), target_left.end()); coord.insert(coord.end(), target_right.begin(), target_right.end()); sort(coord.begin(), coord.end()); coord.erase(unique(coord.begin(), coord.end()), coord.end()); int q = (int)coord.size(); vector<int> freq_left(q), freq_right(q), freq_tl(q), freq_tr(q); auto add_frequencies = [&](const vector<int>& values, vector<int>& freq) { for (int x : values) { int at = lower_bound(coord.begin(), coord.end(), x) - coord.begin(); ++freq[at]; } }; add_frequencies(left, freq_left); add_frequencies(right, freq_right); add_frequencies(target_left, freq_tl); add_frequencies(target_right, freq_tr); vector<int> suffix_left(q + 1), suffix_right(q + 1); vector<int> suffix_tl(q + 1), suffix_tr(q + 1); for (int i = q - 1; i >= 0; --i) { suffix_left[i] = suffix_left[i + 1] + freq_left[i]; suffix_right[i] = suffix_right[i + 1] + freq_right[i]; suffix_tl[i] = suffix_tl[i + 1] + freq_tl[i]; suffix_tr[i] = suffix_tr[i + 1] + freq_tr[i]; } int intervals = q - 1; vector<int> need_right(intervals), surplus_left(intervals); vector<long long> candidate(q, NEG_INF); long long prefix_score = 0; candidate[0] = freq_left[0] ? 0 : NEG_INF; for (int i = 0; i < intervals; ++i) { need_right[i] = suffix_right[i + 1] - suffix_tr[i + 1]; surplus_left[i] = suffix_left[i + 1] - suffix_tl[i + 1]; int slope = (need_right[i] > 0) + (surplus_left[i] > 0) - 1; prefix_score += 1LL * slope * (coord[i + 1] - coord[i]); candidate[i + 1] = freq_left[i + 1] ? prefix_score : NEG_INF; } PositiveCounterTree need_tree(need_right), surplus_tree(surplus_left); MaximumTree candidate_tree(candidate); vector<int> available = freq_left; vector<int> crossed; crossed.reserve(64); long long gain = 0; for (int used = 0; used < limit; ++used) { auto [delta, at] = candidate_tree.best(); if (delta <= 0) break; gain += delta; --available[at]; crossed.clear(); need_tree.decrement_prefix(at - 1, crossed); for (int interval : crossed) { long long length = coord[interval + 1] - coord[interval]; candidate_tree.add_suffix(interval + 1, -length); } crossed.clear(); surplus_tree.decrement_prefix(at - 1, crossed); for (int interval : crossed) { long long length = coord[interval + 1] - coord[interval]; candidate_tree.add_suffix(interval + 1, -length); } if (available[at] == 0) candidate_tree.disable(at); } return gain; } } // namespace long long get_cost(vector<int> X, vector<int> P) { int n = (int)X.size(); vector<char> ancient(n, false); for (int index : P) ancient[index] = true; vector<pair<int, int>> fixed_events; fixed_events.reserve(P.size()); for (int index : P) { if (X[index] < 0) fixed_events.push_back({-X[index], -1}); else if (X[index] > 0) fixed_events.push_back({X[index], +1}); } sort(fixed_events.begin(), fixed_events.end()); vector<int> target_left, target_right; long long target_base_cost = 0; for (int i = 0; i < (int)fixed_events.size();) { int j = i; int balance = 0; while (j < (int)fixed_events.size() && fixed_events[j].first == fixed_events[i].first) { balance += fixed_events[j].second; ++j; } int radius = fixed_events[i].first; if (balance > 0) { target_left.insert(target_left.end(), balance, radius); target_base_cost += 1LL * balance * radius; } else if (balance < 0) { target_right.insert(target_right.end(), -balance, radius); target_base_cost += 1LL * (-balance) * radius; } i = j; } vector<int> left, right; long long move_to_zero_cost = 0; for (int i = 0; i < n; ++i) { if (ancient[i]) continue; move_to_zero_cost += llabs((long long)X[i]); if (X[i] < 0) left.push_back(-X[i]); else if (X[i] > 0) right.push_back(X[i]); } long long movable_count = n - (int)P.size(); long long required = target_left.size() + target_right.size(); if (required > movable_count) return -1; long long bonus = matched_value(left, target_left) + matched_value(right, target_right); int central_limit = (int)((movable_count - required) / 2); bonus += extra_matching_gain(left, right, target_left, target_right, central_limit); return move_to_zero_cost + target_base_cost - 2 * bonus; } ```