阅读这份题解,讲解这道题,脱离原题解,你讲解使用的不同种类符号数量尽可能少,为了代码可读性允许在正确...

Creato il: 9 ottobre 2026

Risposto usando GPT-6 Pro da Chat01

Domanda

阅读这份题解,讲解这道题,脱离原题解,你讲解使用的不同种类符号数量尽可能少,为了代码可读性允许在正确的前提下牺牲部分效率。

题解:https://www.luogu.com.cn/article/deunoa1q

Pensiero

Gathering resources

I’m collecting official analysis and solution links, focusing on primary sources and original contests to accurately reconstruct the content.

Evaluating alternatives
Comparing persistent segment trees and mutable multisets. Considering divide & conquer strategies and managed prefixed pointers.

Evaluating data structures
Weighing Fenwick trees for prefix sums. Analyzing mutable multisets for real-time updates. Seeking efficient k-th smallest queries in O(n log^2 n) time.

Choosing algorithm methods

I’m piecing together sorting and using an ordinary segment tree for prefix movement. This ensures efficient updates and queries, maintaining a manageable complexity.

Crafting the invariant

Ok, let me see. I’m crafting an invariant for efficient partitioning and updates in a balanced manner. The goal is to avoid unneeded resizes and loop til non-empty subarray for each recursion level, ensuring a balanced partition.

Formulating the solution

I’m developing a scoring formula to precisely identify the optimal cut for each candidate, considering constraints and feasibility without involving tricky variables like x.

Mapping out strategies

I'm laying out the options to mechanize the game post its predefined steps and constraints.

Piecing together segment tree

I’m pulling together regular segment tree elements, integrating specific functions for smallest sum and prefix movement, and simplifying code. Progressing smoothly.

Breaking down the solution

I'm examining the algorithm, considering tree traversal, recursion, and prefix movement. It's interesting to see how complexity unfolds!

Evaluating persistent structures

I'm analyzing the complexities of persistent data structures, balancing cognitive load, efficiency, and readability. Testing various tree models to optimize both.

Weighing tools

I'm thinking about wavelet trees and multisets, but they add complexity. Exploring simpler structures to find the best compromise between performance and clarity.

Mapping out strategies

I’m outlining budget and strategic updates now. Interesting point: working through extensive testing and benchmarking.

Constructing the solution

I’m organizing the problem by focusing on dominant strategies and score calculations to ensure optimal outcomes.

Assessing optimal cuts

I’m working through different cut strategies and their impacts on scores. Consider sorting and widow sums to pinpoint the best values.

Mapping optimal solutions

I’m figuring out the best division points tender for tasks and suffixes, with their solutions and sums, to maintain a progressive approach in each step.

Configuring the code

Aligning with the latest guidelines, I’m figuring out the optimal split right within the known range, ensuring efficient prefix handling and minimal reinitialization.

Organizing benchmarks

Thinking through organizing 200,000 benchmarks and avoiding hardware bias. Progress includes theorizing updates and ensuring compliance with mathematical constraints.

Blending techniques
I’m merging monotonicity concepts and segment trees for optimized queries. پکvraag results clarify the complexity.

Pulling together

Mapping out the solution involves clarifying constraints, coding efficiently, and ensuring the final method is optimal and concise. Combining examples and proofs helps in achieving a comprehensive understanding.

Assessing node balance

I’m considering if node counts maintain balance by examining split intervals and tree depth. Trees should handle node distribution and query complexity efficiently.

Worked for 7m 38s

Risposta

这道题的关键是:把“选哪些题回答”变成“选一个分界点”,再利用分界点的单调性减少枚举。

下面先推导这个过程,再给出不使用主席树、只使用普通线段树的完整实现。

一、先确定对手会怎么改答案

每道题答对得到 aa,答错得到 −b-b,不回答得到 00。

对于一次询问,我们必须回答至少 kk 道题,对手可以修改其中至多 kk 道,让我们尽可能少得分。注意“至少回答 kk 道”这个要求,因此答案可能是负数,不能简单地选择全部不答。题目中题数最多为 2×1052\times 10^5。(USACO)

一道题被改以后,得分从 aa 变成 −b-b,所以我们会少得

a+ba+b

分。

因此,无论我们回答了哪些题,对手都会修改其中 a+ba+b 最大的 kk 道。(USACO)

于是,先把所有题按照 a+ba+b 从大到小排序。之后提到的位置,都是排序后的位置。

这样,对于我们选出来的题:

前 kk 道被改,每道贡献 −b-b;剩下的不会被改,每道贡献 aa。

a+ba+b 相同的题可以任意排序,因为改它们造成的减分相同。

二、为什么只需要枚举一个分界点?

先考虑 k>0k>0。

假设我们已经选好了一些题,把最后一道被改的题的位置记作 xx。

那么,位置 xx 后面的题,即使我们全部回答,也不会影响前面那 kk 道被改的题。它们都能得到正的 aa 分。

所以:

最后一道被改的题之后,所有题都应该回答。

选题方案因此可以整理成:

  • 前 xx 道题里,选择恰好 kk 道,它们全部被改。
  • 后面的题全部回答,它们全部答对。

前面选出的题只贡献 −b-b,所以当然选 bb 最小的 kk 道。

于是,分界点固定之后,最优得分就是:

得分=x 后面所有 a 的和−前 x 道题中最小的 k 个 b 的和\boxed{ \text{得分} = x\text{ 后面所有 }a\text{ 的和} - \text{前 }x\text{ 道题中最小的 }k\text{ 个 }b\text{ 的和} }

枚举所有满足 k≤x≤nk\le x\le n 的分界点,取最大值即可。这个表达式也是题解中枚举分界点做法的核心。(Luogu)

这里有一个容易疑惑的细节

按照“前面选最小的 kk 个 bb”的规则,第 xx 道题不一定被选中。

这没有关系。现在的 xx 只是划分前后两段的位置,不再要求它恰好是最后一道被改的题。

这样做依然正确:

不会产生非法方案。 前面恰好回答 kk 道,后面全部回答。按照排序顺序,对手可以选择修改前面这 kk 道,而且这是最坏情况。

不会漏掉最优方案。 任意最优方案都可以补上最后一道被改的题之后的所有题;再把前面选出的题替换成 bb 最小的 kk 道,得分只会变好。

对于 k=0k=0,允许 x=0x=0,就表示所有题都回答、没有题被改。

举个例子

假设排序后是:

位置答对得分 aa答错扣分 bba+ba+b
18917
211011
3527
4314

考虑 k=1k=1。

选择 x=3x=3 时,前三道题里只回答 bb 最小的第三题,最后一道题也回答。

第三题被改,得到 −2-2;第四题没被改,得到 33。最终得分为 11。

四个分界点的得分依次为:

0, −1, 1, −10,\ -1,\ 1,\ -1

所以最优分界点是 33。

到这里,原来的选题问题已经变成了:

对每个 kk,找一个最好的 xx。

三、为什么 kk 越大,最优分界点不会向左移动?

这是整道题最重要的性质。

我们约定:如果多个分界点得分相同,选择最靠左的那个。

在这个约定下,随着 kk 增大,最优分界点只会保持不变或者向右移动。官方分析也使用了这一决策单调性。(USACO)

不需要复杂的交换论证,直接观察“多改一道题会多扣多少分”。

1. 固定分界点,多改一道会发生什么?

分界点固定时,后面所有 aa 的和不变。

前面原本选择最小的 kk 个 bb,现在需要选择最小的 k+1k+1 个 bb。

因此,得分会减少:

这个前缀中,第 k+1k+1 小的 bb。

2. 分界点越靠右,这次额外扣分不会越大

比较一个靠左的分界点和一个靠右的分界点。

靠右的前缀包含靠左前缀中的所有题,还多了一些题。

候选题变多以后,第 k+1k+1 小的 bb 只可能变小或不变,不可能变大。

所以:

当 kk 增加一时,靠右的分界点,得分减少得不会比靠左的更多。

3. 原来落后的左侧分界点,不可能反超

考虑当前 kk 的最优分界点。

因为同分时选择最靠左的,所以在它左侧的所有合法分界点,得分都严格更低。

当 kk 增加一:

  • 左侧分界点原本就落后;
  • 它们这次减少的得分,又不比当前分界点少。

因此,它们仍然落后,不可能成为新的最优分界点。

有一个边界情况:当前分界点是 x=kx=k,增加一次修改后,它不再合法。但这时新的分界点至少是 k+1k+1,自然也只能在它右侧。

单调性得证。

四、利用单调性分治,而不是逐个询问完整枚举

假设现在要处理一段连续的 kk,并且已经知道:这些 kk 的最优分界点,都在某个候选区间里。

取中间的 kk,枚举它的候选分界点,找到最优位置。

根据单调性:

更小的 kk,最优分界点不会超过这个位置。
更大的 kk,最优分界点不会小于这个位置。

于是可以把问题分成左右两部分,继续递归。

代码中用:

cpp
solve(firstK, lastK, firstCut, lastCut)

表示:

计算 firstK 到 lastK 的答案;它们的最优分界点都在 firstCut 到 lastCut 之间。

初始调用:

cpp
solve(0, n, 0, n);

这里直接预处理所有 kk 的答案,之后按输入顺序输出即可。这样不需要对询问排序,也不需要记录询问原来的编号。

注意,单调的是“最优分界点”,不是“得分随分界点单调变化”。

前面例子的得分是 0,−1,1,−10,-1,1,-1,所以不能从左往右走,看到得分下降就停下来,也不能直接三分。

五、不用主席树,普通线段树就够了

现在只剩一个需要高效计算的部分:

前 xx 道题中,最小的 kk 个 bb 的和。

普通线段树配合分界点的增删,也可以完成这个任务,不必保存所有前缀的历史版本。(USACO)

1. 线段树按 bb 的大小排列

先把题按 bb 从小到大编号。

线段树的叶子按照这个编号排列。因此,左边的叶子对应较小的 bb,右边对应较大的 bb。

线段树中的顺序是 bb 的大小顺序,不是前面按照 a+ba+b 排出的题目顺序。

每个节点保存两个信息:

cpp
count // 这个范围内,目前有多少道题 sum // 这些题的 b 的总和

当前分界点为 xx 时,线段树只放入前 xx 道题。

2. 怎么查询最小的 kk 个数之和?

看左子树里有多少个数。

如果左子树已经有至少 kk 个数,那么答案全部在左子树里,继续向左查询。

否则,左子树里的数全部取走,再去右子树补足剩下的数量。

每次只继续进入一棵子树,因此一次查询是 O(log⁡n)O(\log n)。

3. 怎么切换前缀?

维护当前已经放入了前多少道题。

要把分界点向右移动,就加入新经过的题;向左移动,就删除退出前缀的题。

cpp
movePrefix(x);

执行之后,线段树就恰好保存前 xx 道题。

为避免递归之间互相干扰,我们统一约定:

每次 solve 调用前和返回后,线段树都保存前 firstCut 道题。

代码里明确恢复状态,虽然会多做一些增删,但逻辑清楚,而且不改变最终的复杂度。

六、完整代码:C++17

cpp
#include <algorithm> #include <iostream> #include <limits> #include <vector> using namespace std; struct Question { long long a, b; int rank; // 按 b 从小到大排列时的编号 }; struct Node { int count = 0; long long sum = 0; }; int n; vector<Question> question; vector<long long> value; vector<long long> suffix; vector<long long> answer; vector<Node> tree; int currentPrefix = 0; // 加入或删除一道题。 // delta 为 1 表示加入,为 -1 表示删除。 void change(int node, int left, int right, int rank, int delta) { tree[node].count += delta; tree[node].sum += value[rank] * delta; if (left == right) return; int middle = (left + right) / 2; if (rank <= middle) { change(node * 2, left, middle, rank, delta); } else { change(node * 2 + 1, middle + 1, right, rank, delta); } } // 查询当前线段树中,最小的 k 个 b 的和。 long long smallestSum(int node, int left, int right, int k) { if (k == 0) return 0; // 整个节点里的数都要选,直接返回总和。 if (k == tree[node].count) { return tree[node].sum; } if (left == right) { return value[left] * k; } int middle = (left + right) / 2; int leftCount = tree[node * 2].count; if (k <= leftCount) { return smallestSum(node * 2, left, middle, k); } return tree[node * 2].sum + smallestSum( node * 2 + 1, middle + 1, right, k - leftCount ); } // 让线段树恰好保存排序后的前 x 道题。 void movePrefix(int x) { while (currentPrefix < x) { ++currentPrefix; change( 1, 1, n, question[currentPrefix].rank, 1 ); } while (currentPrefix > x) { change( 1, 1, n, question[currentPrefix].rank, -1 ); --currentPrefix; } } // 计算 firstK 到 lastK 的答案。 // 已知这些 k 的最优分界点在 firstCut 到 lastCut 之间。 // // 调用前、返回后: // 线段树都恰好保存前 firstCut 道题。 void solve(int firstK, int lastK, int firstCut, int lastCut) { if (firstK > lastK) return; int k = (firstK + lastK) / 2; int bestCut = max(firstCut, k); long long bestScore = numeric_limits<long long>::lowest(); // 前缀里至少需要有 k 道题,所以 x 不能小于 k。 for (int x = max(firstCut, k); x <= lastCut; ++x) { movePrefix(x); long long score = suffix[x + 1] - smallestSum(1, 1, n, k); // 从左往右枚举,只在严格更优时更新。 // 这样同分时保留最靠左的分界点。 if (score > bestScore) { bestScore = score; bestCut = x; } } answer[k] = bestScore; // 处理更小的 k。 movePrefix(firstCut); solve(firstK, k - 1, firstCut, bestCut); // 左侧递归返回后,前缀仍是 firstCut。 // 调整到右侧递归需要的初始状态。 movePrefix(bestCut); solve(k + 1, lastK, bestCut, lastCut); // 恢复本次调用开始时的状态。 movePrefix(firstCut); } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int q; cin >> n >> q; question.resize(n + 1); value.resize(n + 1); suffix.assign(n + 2, 0); answer.resize(n + 1); tree.resize(4 * n + 4); for (int i = 1; i <= n; ++i) { cin >> question[i].a >> question[i].b; } // 给每道题分配按 b 排列的编号。 // 相同的 b 也分配不同编号,不需要去重。 sort( question.begin() + 1, question.end(), [](const Question& x, const Question& y) { return x.b < y.b; } ); for (int i = 1; i <= n; ++i) { question[i].rank = i; value[i] = question[i].b; } // 正式的题目顺序:按被修改时的损失从大到小排列。 sort( question.begin() + 1, question.end(), [](const Question& x, const Question& y) { return x.a + x.b > y.a + y.b; } ); // suffix[i] 表示从第 i 道题到最后一道题的 a 之和。 for (int i = n; i >= 1; --i) { suffix[i] = suffix[i + 1] + question[i].a; } // 初始前缀为空,符合 firstCut = 0 的要求。 solve(0, n, 0, n); while (q--) { int k; cin >> k; cout << answer[k] << '\n'; } return 0; }

七、为什么这些来回增删不会太慢?

在一次分治调用中,除去递归内部,前缀来回移动的总距离,只是当前候选分界点区间长度的常数倍。

而在分治的同一层,各个候选区间只共享端点,总长度是 O(n)O(n)。分治共有 O(log⁡n)O(\log n) 层。

因此,总共只有 O(nlog⁡n)O(n\log n) 次增删,以及 O(nlog⁡n)O(n\log n) 次得分计算。每次线段树操作花费 O(log⁡n)O(\log n),得到:

时间复杂度 O(nlog⁡2n+q)\boxed{\text{时间复杂度 }O(n\log^2 n+q)}

只维护一棵普通线段树,其他数组也都是线性大小,因此:

空间复杂度 O(n)\boxed{\text{空间复杂度 }O(n)}

最后注意三个边界:k=0k=0 时答案是所有 aa 的和;k=nk=n 时答案是所有 bb 的和取负;分数和必须使用 long long。

上述实现已通过原题样例,以及 1006 组小规模数据的穷举对拍,对拍枚举了全部答题集合和所有 kk;也运行了题数为 2×1052\times10^5 的规模测试,但未实际提交在线评测。

Condividi questa Q&A