NOIP2025 模拟赛2
【P11267 王国边缘】题解报告
题目核心分析
本题要求我们模拟一个在无限循环字符串上的移动过程。核心的挑战在于所有给定的数值(位置 s,步数 k,视野 m)都可能极大,达到 \(10^{18}\) 的量级。这意味着任何与这些数值大小成正比的模拟算法都是不可行的。
解决这类问题的关键通常在于利用题目中的周期性或固定模式,将复杂的、大量的操作转化为几次高效的数学计算或预处理查询。
幸运的是,题目中的字符串 \(T\) 是由 \(S\) 无限循环构成的,这给了我们强烈的周期性暗示。智能体的移动规则虽然看起来复杂,但由于字符串的周期性,从某个相对位置(模 \(n\) 的余数)出发的“跳跃模式”也应该是固定的。这引导我们向倍增(Binary Lifting)或矩阵快速幂这类对数时间复杂度的算法思考。
部分分算法解析
Subtask 1-2 (\(k=0\))
- 约束条件:\(k=0\)。
- 思路:移动次数为0,异客原地不动。
- 算法:对于每个询问,直接输出起点 \(s\) 对 \(10^9+7\) 取模后的结果。
- 代码实现:
#include <iostream> int main() { long long n, m, s, k; int q; std::string str_s; std::cin >> n >> m >> q >> str_s; while (q--) { std::cin >> s >> k; std::cout << s % 1000000007 << std::endl; } return 0; }
Subtask 3-5 (\(S\) 为全 0 或全 1)
这是非常重要的特殊情况,能帮助我们理解跳跃的本质。
-
情况A:\(S\) 为全
0- 思路:视野所及之处
T_{i+1...i+m}将永远是全0。根据规则,异客会移动到下一个字符 \(T_{i+1}\)。这意味着每一步都向前移动1个位置。 - 算法:从起点 \(s\) 移动 \(k\) 次,最终位置就是 \(s+k\)。
- 结果:\((s \pmod{MOD} + k \pmod{MOD}) \pmod{MOD}\)。
- 思路:视野所及之处
-
情况B:\(S\) 为全
1- 思路:视野所及之处
T_{i+1...i+m}永远有1。根据规则,异客会移动到其中最远的一个1上,也就是位置 \(i+m\)。这意味着每一步都固定向前移动 \(m\) 个位置。 - 算法:从起点 \(s\) 移动 \(k\) 次,最终位置就是 \(s + k \times m\)。
- 结果:\((s \pmod{MOD} + (k \pmod{MOD}) \times (m \pmod{MOD}) \pmod{MOD}) \pmod{MOD}\)。
- 思路:视野所及之处
-
启发:这两个特例告诉我们,在某些情况下,每一步的位移量是固定的。这启发我们思考:在一般情况下,位移量是否也存在某种规律?
Subtask 6-10 (暴力模拟, \(n, m, q, s, k\) 均较小)
- 约束条件:所有数值都在 \(3 \times 10^3\) 以内。
- 思路:数据范围很小,可以直接模拟。对于每次询问,循环 \(k\) 次,每次计算下一步的位置。
- 算法:
- 为了方便处理环形字符串,可以将字符串
S复制几倍(例如,复制到 \(n+m\) 的长度就足够一次查询)。 - 写一个函数
getNextPos(current_pos):- 从
current_pos + m向current_pos + 1倒序扫描。 - 遇到的第一个
'1'就是目标位置。 - 如果整个区间没有
'1',目标位置就是current_pos + 1。
- 从
- 主循环中调用这个函数
k次。
- 为了方便处理环形字符串,可以将字符串
- 代码实现:
// 伪代码,演示核心逻辑 long long getNextPos(long long current_pos, long long m, int n, const std::string& S) { for (long long i = current_pos + m; i > current_pos; --i) { // T_i 对应 S 的下标是 (i-1) % n if (S[(i - 1) % n] == '1') { return i; } } return current_pos + 1; } void solve_brute_force(long long s, long long k, long long m, int n, const std::string& S) { long long current_pos = s; for (int i = 0; i < k; ++i) { current_pos = getNextPos(current_pos, m, n, S); } std::cout << current_pos % 1000000007 << std::endl; } - 启发:暴力模拟帮助我们厘清了单步移动的具体逻辑,是实现正解中“单步跳转”计算部分的基础。
Subtask 11-14 (\(k=1\))
- 约束条件:\(k=1\),但 \(s, m\) 很大。
- 思路:只需计算一步。这强迫我们思考如何在 \(s\) 和 \(m\) 很大的情况下,高效地完成
getNextPos函数的功能。 - 算法:
- 设当前位置为 \(i\)。要找的目标区间是 \([i+1, i+m]\)。
- 要找这个区间里最远的 '1',等价于在整个无限字符串 \(T\) 中,寻找位置 \(\le i+m\) 的最远的 '1'。
- 我们可以预处理字符串
S,得到一个数组last_one[j],表示S的前缀S[0...j]中最后一个 '1' 的位置。 - 设目标位置上界为
limit = i + m。它在S循环体中的位置是第block = (limit - 1) / n个块,和块内偏移rem = (limit - 1) % n。 - 最远的 '1' 可能在
(block, rem)这个位置,也可能在它前面。利用last_one数组可以快速定位。- 如果
S[0...rem]中有 '1'(即last_one[rem]有效),那么最远的 '1' 就在第block个块的last_one[rem]位置。绝对位置是block * n + last_one[rem] + 1。 - 否则,最远的 '1' 在前一个块的末尾,也就是
(block - 1) * n + (S中最后一个'1'的位置) + 1。
- 如果
- 如果算出来的位置 \(\le i\),说明视野内没有 '1',则移动到 \(i+1\)。
- 启发:这个部分分解决了单步跳转的核心计算问题,是正解预处理步骤的关键。它告诉我们,无论 \(s,m\) 多大,单步跳转的计算量可以做到与 \(n\) 相关,而与 \(s,m\) 本身大小无关。
正解算法:周期性 + 倍增
结合以上所有启发,我们来设计正解。
1. 状态定义与转移
-
状态:一个位置 \(i\) 的关键信息是它在循环节
S中的相对位置,即p = (i - 1) % n。我们称p为当前的状态。 -
跳转分解:对于一个超大的视野 \(m\),我们可以将其分解为
m = q_m * n + r_m,其中q_m = m / n,r_m = m % n。 -
位移分解:从位置 \(i\)(状态为
p)跳一次,位移Δ可以分解为两部分:- 基础位移:由
q_m个完整的S循环贡献。 - 额外位移:由状态
p、视野零头r_m和S本身的1分布共同决定。
设从状态
p出发,通过一次跳转,位置从 \(i\) 变为 \(i_{next}\)。
\(i_{next}\) 是 \([i+1, i+m]\) 中最远的 '1'。
\(i_{next} - i = \Delta\) (总位移)。
我们可以定义一个extra_jump[p],使得 \(i_{next} = i + q_m \times n + \text{extra\_jump}[p]\)。
这个extra_jump[p]可以通过 Subtask 11-14 的方法预处理出来。 - 基础位移:由
-
状态转移:从状态
p跳一次后,新的状态p_next是:
p_next = (i_next - 1) % n = (i - 1 + Δ) % n = (p + Δ) % n
p_next = (p + q_m * n + extra_jump[p]) % n = (p + extra_jump[p]) % n
关键结论:状态的转移只和extra_jump[p]有关,与巨大的q_m无关!
2. 倍增预处理
我们现在有一个包含 \(n\) 个状态的系统,每次转移的规则也已确定。为了快速计算 \(k\) 步的移动,我们使用倍增。
建立两个表:
next_p[p][j]:从状态p开始,连续移动 \(2^j\) 次后,达到的新状态。extra_jump_sum[p][j]:从状态p开始,连续移动 \(2^j\) 次,所有extra_jump的总和(模 \(MOD\))。
递推关系:
- Base Case (j=0):
next_p[p][0] = (p + extra_jump[p]) % nextra_jump_sum[p][0] = extra_jump[p] % MOD
- Recursive Step (j>0):移动 \(2^j\) 步 = 先移动 \(2^{j-1}\) 步,再从新位置移动 \(2^{j-1}\) 步。
mid_p = next_p[p][j-1]next_p[p][j] = next_p[mid_p][j-1]extra_jump_sum[p][j] = (extra_jump_sum[p][j-1] + extra_jump_sum[mid_p][j-1]) % MOD
预处理的时间复杂度为 \(O(n \log k)\)。
3. 查询处理
对于每个查询 \((s, k)\):
- 计算总的基础位移:总共移动了 \(k\) 步,每步的基础位移是 \(q_m \times n\)。总基础位移就是 \(k \times q_m \times n\)。
- 计算总的额外位移:
- 初始状态
current_p = (s - 1) % n。 - 总额外位移
total_extra = 0。 - 将 \(k\) 进行二进制分解。从高位到低位遍历
j(从约60到0)。 - 如果 \(k\) 的第
j位是1(即(k >> j) & 1),说明需要进行一次 \(2^j\) 步的“大跳”。total_extra = (total_extra + extra_jump_sum[current_p][j]) % MODcurrent_p = next_p[current_p][j]
- 初始状态
- 合并结果:
final_pos = (s + k * q_m * n + total_extra) % MOD
注意所有计算都要在模 \(MOD\) 意义下进行。
查询的时间复杂度为 \(O(\log k)\)。总时间复杂度为 \(O(n \log(\text{max_k}) + q \log(\text{max_k}))\),可以通过本题。
最终代码实现
#include <iostream>
#include <vector>
#include <string>
#include <numeric>
using namespace std;
using ll = long long;
const int MOD = 1e9 + 7;
const int MAXN = 200005;
// k, m 可以很大,log2(10^18) 约 59.79,所以 LOGK 取 62 足够
const int LOGK = 62;
ll n_ll, m, q;
int n;
string s;
// 倍增表
int next_p[MAXN][LOGK];
ll extra_jump_sum_mod[MAXN][LOGK];
// 预处理S串的信息
vector<int> last_one_in_prefix; // last_one_in_prefix[i] = S[0..i]中最后一个'1'的下标
int last_one_in_s; // S中最后一个'1'的下标
// 辅助函数:给定T中的一个位置pos(1-based),返回<=pos的最远的'1'的位置
// 如果不存在,返回0
ll get_farthest_one_pos(ll pos) {
if (last_one_in_s == -1 || pos <= 0) return 0;
ll block = (pos - 1) / n_ll;
int rem = (int)((pos - 1) % n_ll);
if (last_one_in_prefix[rem] != -1) {
// 在当前块的前缀部分找到了'1'
return block * n_ll + last_one_in_prefix[rem] + 1;
} else {
// 当前块的前缀部分没有'1',需要到前一个块去找
if (block == 0) return 0; // 第一个块的前面没有了
return (block - 1) * n_ll + last_one_in_s + 1;
}
}
// 处理S全为0的特殊情况
void solve_all_zeros() {
for (int i = 0; i < q; ++i) {
ll start, k;
cin >> start >> k;
ll ans = (start % MOD + k % MOD) % MOD;
cout << ans << "\n";
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
cin >> n_ll >> m >> q;
n = (int)n_ll;
cin >> s;
// 特判:S全为0
bool all_zeros = true;
for (char c : s) {
if (c == '1') {
all_zeros = false;
break;
}
}
if (all_zeros) {
solve_all_zeros();
return 0;
}
// 预处理 S
last_one_in_prefix.resize(n);
int current_last = -1;
for (int i = 0; i < n; ++i) {
if (s[i] == '1') {
current_last = i;
}
last_one_in_prefix[i] = current_last;
}
last_one_in_s = current_last;
// --- 预处理倍增表 ---
ll m_q = m / n_ll; // 基础位移中的整周期数
ll m_r = m % n_ll; // 视野的零头
vector<ll> extra_jump(n);
for (int p = 0; p < n; ++p) { // p是状态, 对应位置 i = (任何数 k*n + p + 1)
ll current_pos_example = p + 1; // 选一个代表位置
ll vision_end = current_pos_example + m;
ll farthest_one = get_farthest_one_pos(vision_end);
if (farthest_one <= current_pos_example) { // 视野内没有 '1'
// 移动到下一个位置
ll jump_dist = 1;
extra_jump[p] = jump_dist - m_q * n_ll;
} else {
ll jump_dist = farthest_one - current_pos_example;
extra_jump[p] = jump_dist - m_q * n_ll;
}
}
// 填充倍增表 j=0 的情况
for (int p = 0; p < n; ++p) {
extra_jump_sum_mod[p][0] = (extra_jump[p] % MOD + MOD) % MOD;
ll next_p_ll = (ll)p + extra_jump[p];
next_p[p][0] = (int)((next_p_ll % n_ll + n_ll) % n_ll);
}
// 递推填充整个倍增表
for (int j = 1; j < LOGK; ++j) {
for (int p = 0; p < n; ++p) {
int mid_p = next_p[p][j - 1];
next_p[p][j] = next_p[mid_p][j - 1];
extra_jump_sum_mod[p][j] = (extra_jump_sum_mod[p][j - 1] + extra_jump_sum_mod[mid_p][j - 1]) % MOD;
}
}
ll base_jump_per_step_mod = (m_q % MOD * (n_ll % MOD)) % MOD;
// --- 处理查询 ---
for (int i = 0; i < q; ++i) {
ll start, k;
cin >> start >> k;
if (k == 0) {
cout << start % MOD << "\n";
continue;
}
ll final_pos_mod = start % MOD;
ll k_mod = k % MOD;
// 1. 加上所有基础位移
ll total_base_jump_mod = (k_mod * base_jump_per_step_mod) % MOD;
final_pos_mod = (final_pos_mod + total_base_jump_mod) % MOD;
// 2. 加上所有额外位移
int current_p = (int)((start - 1) % n_ll);
ll total_extra_jump_mod = 0;
for (int j = LOGK - 1; j >= 0; --j) {
if ((k >> j) & 1) {
total_extra_jump_mod = (total_extra_jump_mod + extra_jump_sum_mod[current_p][j]) % MOD;
current_p = next_p[current_p][j];
}
}
final_pos_mod = (final_pos_mod + total_extra_jump_mod) % MOD;
cout << final_pos_mod << "\n";
}
return 0;
}
【P11268 买东西】题解
题目大意
有 \(n\) 个物品和 \(m\) 张优惠券。每个物品 \(i\) 有原价 \(a_i\) 和折扣价 \(b_i\)。每张优惠券 \(j\) 有一个使用门槛 \(w_j\) 和减免金额 \(v_j\)。
对于每个物品,有三种购买方式:
- 原价 \(a_i\) 购买。(由于 \(b_i \le a_i\),此选项永远不优于折扣价,可以忽略)。
- 折扣价 \(b_i\) 购买。
- 使用一张未使用过的优惠券 \(j\),条件是 \(a_i \ge w_j\),价格为 \(a_i - v_j\)。
目标是购买所有 \(n\) 个物品,求最小总花费。
思路分析与部分分
这是一个带有约束的资源分配问题:我们有 \(m\) 张优惠券(资源),需要分配给 \(n\) 个物品(任务),以实现总成本最小化。核心的困难在于,一张优惠券可能可以用于多个物品,而一个物品也可能适用多张优惠券。将哪张券给哪个物品,是一个需要精心决策的问题。
一个简单的贪心策略,比如“对于每个物品,找到能用的、面额最大的优惠券”,很容易被证明是错误的。可能一个物品为了一个很小的优惠,用掉了一张“神券”,而这张神券本可以给另一个物品带来巨大的优惠。
面对 \(N, M \le 10^6\) 的数据范围,我们需要一个高效的算法,通常是 \(O(N\log N)\) 或类似级别的复杂度。我们从分析部分分数据范围的特殊性质入手,逐步推导出正解。
Subtask 1: \(N, M \le 10\)
思路:暴力搜索 / 状态压缩DP
数据范围极小,允许指数级复杂度的算法。我们可以尝试所有可能的“物品-优惠券”匹配方案。
一个直接的思路是使用回溯法(backtracking)。我们定义一个函数 dfs(item_index, used_coupons_mask),表示正在为第 item_index 个物品决策,而已使用的优惠券集合由位掩码 used_coupons_mask 表示。
在函数中,我们 для第 item_index 个物品:
- 尝试以折扣价 \(b_i\) 购买,然后递归到
dfs(item_index + 1, used_coupons_mask)。 - 遍历所有未被使用的优惠券 \(j\)(通过检查
used_coupons_mask)。如果该券满足 \(a_i \ge w_j\),我们就尝试使用它,然后递归到dfs(item_index + 1, used_coupons_mask | (1 << j))。
所有递归路径的尽头(item_index == n)的花费的最小值就是答案。
复杂度:最坏情况下,每个物品都可以和多个优惠券匹配,状态数巨大,复杂度大致为 \(O(N!)\) 或更高,但足以通过 \(N, M \le 10\) 的数据。
代码实现 (伪代码)
long long min_cost = infinity;
vector<bool> coupon_used(m, false);
void solve(int item_idx, long long current_cost) {
if (item_idx == n) {
min_cost = min(min_cost, current_cost);
return;
}
// 选项1: 使用折扣价
solve(item_idx + 1, current_cost + items[item_idx].b);
// 选项2: 尝试使用每个可用的优惠券
for (int j = 0; j < m; ++j) {
if (!coupon_used[j] && items[item_idx].a >= coupons[j].w) {
coupon_used[j] = true;
solve(item_idx + 1, current_cost + items[item_idx].a - coupons[j].v);
coupon_used[j] = false; // 回溯
}
}
}
Subtasks 2 & 3: \(a_i = b_i\)
思路:贪心
这个特殊性质是一个巨大的突破口。当 \(a_i = b_i\) 时,使用折扣价购买的花费是 \(a_i\)。而使用优惠券 \(j\) 的花费是 \(a_i - v_j\)。显然,只要能用优惠券,就一定比用原价(即折扣价)划算,因为 \(v_j > 0\)。
此时,问题转化为:为尽可能多的物品,匹配上尽可能好的优惠券。
我们的目标是最大化所有使用的优惠券的总面额 \(\sum v_j\)。
一个物品 \(i\) 可以使用优惠券 \(j\) 的条件是 \(a_i \ge w_j\)。
一个看似正确的贪心是:将物品按 \(a_i\) 升序排序,将优惠券按 \(w_j\) 升序排序。然后遍历每个物品,将所有满足 \(w_j \le a_i\) 的可用优惠券收集起来,并为当前物品选择其中面额 \(v_j\) 最大的一个。
这个策略是正确的。我们用一个优先队列(大根堆)来维护当前所有可用的优惠券的面额。
算法流程:
- 将物品按 \(a_i\) 升序排序。
- 将优惠券按 \(w_j\) 升序排序。
- 初始化总花费
total_cost = sum(all a_i)。我们先假设所有物品都用原价购买。 - 初始化一个大根堆
pq和一个优惠券指针coupon_ptr = 0。 - 遍历排序后的物品 \(i=0 \to n-1\):
a. 将所有满足coupons[coupon_ptr].w <= items[i].a的优惠券的面额vpush进大根堆pq。同时移动coupon_ptr。
b. 如果pq非空,说明有可用的优惠券。我们取出堆顶(面额最大的券),用它来减免费用。即total_cost -= pq.top(),然后pq.pop()。 - 最终的
total_cost就是答案。
启发:这个子任务告诉我们,将物品和优惠券排序,并用一个数据结构(如优先队列)动态维护可用资源是一个非常有效的处理此类匹配问题的框架。
代码实现:
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
#include <numeric>
struct Item { long long id, a, b; };
struct Coupon { long long id, w, v; };
// ... sort comparators ...
void solve_subtask_a_equals_b() {
// ... read data ...
// ... sort items by a, coupons by w ...
long long total_cost = 0;
for(const auto& item : items) {
total_cost += item.a;
}
std::priority_queue<long long> pq;
int coupon_idx = 0;
for (int i = 0; i < n; ++i) {
while (coupon_idx < m && coupons[coupon_idx].w <= items[i].a) {
pq.push(coupons[coupon_idx].v);
coupon_idx++;
}
if (!pq.empty()) {
total_cost -= pq.top();
pq.pop();
}
}
std::cout << total_cost << std::endl;
}
Subtasks 5 & 6: \(\max_j w_j \le \min_i a_i\)
思路:贪心
这个性质意味着任何一张优惠券都可以用于任何一个物品。物品和优惠券之间的使用门槛限制消失了。
对于每个物品 \(i\),我们可以在 \(b_i\) 和 \(a_i - v_j\) 之间选择。使用优惠券 \(j\) 带来的节省(相比于折扣价)是 b_i - (a_i - v_j) = v_j - (a_i - b_i)。
我们希望总节省量最大。我们最多可以用掉 \(\min(n, m)\) 张优惠券。
我们可以把每张优惠券看作一个提供基础优惠 \(v_j\) 的物品,而每个物品 \(i\) 则会产生一个 a_i - b_i 的“附加成本”。为了最大化节省 v_j - (a_i - b_i),我们应该:
- 用面额大的 \(v_j\) 去匹配。
- 用在
a_i - b_i小的物品上。
算法流程:
- 计算出每个物品的“附加成本”
d_i = a_i - b_i。 - 将所有优惠券按面额 \(v_j\) 降序排序。
- 将所有物品按它们的“附加成本” \(d_i\) 升序排序。
- 初始总花费为所有物品的折扣价之和
sum(b_i)。 - 遍历前 \(\min(n, m)\) 个优惠券和物品。对于第 \(k\) 个配对(第 \(k\) 大的 \(v_j\) 和第 \(k\) 小的 \(d_i\)),计算节省量
saving = v_j - d_i。 - 如果
saving > 0,说明使用这张券是划算的。我们将这份节省从总花费中减去total_cost -= saving。
启发:这个子任务让我们关注到了使用优惠券相对于折扣价的净收益,即 v_j - (a_i - b_i)。这个思想对正解至关重要。
正解:反悔贪心 (Regretful Greedy)
结合前面子任务的启发,我们来解决完整问题。
- 从子任务2/3,我们学会了排序+优先队列的框架。
- 从子任务5/6,我们学会了比较
v_j和a_i - b_i的大小。
我们将这两者结合,得到一种称为反悔贪心的精妙解法。
核心思想:
我们依然采用“按 \(a_i\) 排序并遍历物品,用优先队列维护可用券”的框架。
对于每个物品 \(i\),我们做一次临时决策,然后提供一个反悔的机会。
算法流程:
- 将物品按 \(a_i\) 升序排序,优惠券按 \(w_j\) 升序排序。
- 初始化总花费
total_cost = 0。 - 初始化一个大根堆
pq。这个堆里存放的不再仅仅是优惠券面额,而是广义的“优惠机会”。 - 遍历每个物品 \(i\):
a. 更新资源池:将所有新近可用的优惠券(满足 \(w_j \le a_i\))的面额 \(v_j\) 加入pq。
b. 决策:现在需要为物品 \(i\) 付款。我们有两种选择:
- 花费 \(b_i\)。
- 花费 \(a_i - V\),其中 \(V\) 是pq中最大的优惠机会。
c. 贪心选择:我们比较这两种花费。
- 如果pq为空,或者b_i <= a_i - V(等价于V <= a_i - b_i),说明用当前最好的优惠券也不如直接用折扣价划算。我们选择支付 \(b_i\)。
- 否则(即V > a_i - b_i),使用优惠券更优。我们选择支付 \(a_i - V\)。
d. 反悔机制:这是算法的精髓。
- 如果我们选择了支付 \(b_i\):那么最好的优惠机会V仍然在pq中,可以留给未来的物品,这本身就是一种“反悔”的体现——我们放弃了眼前的机会,以图未来的更大利益。
- 如果我们选择了支付 \(a_i - V\):我们用掉了机会V。但万一这个V对未来的某个物品k来说是“神券”呢?我们必须提供一个反悔的途径。
这个途径就是:未来的物品k可以来“抢走”本属于物品i的机会V,但它必须付出代价,即把物品i的购买方式变回折扣价b_i。
这个过程对总花费的影响是:物品i的花费从a_i - V变为b_i,增加了b_i - (a_i - V) = V - (a_i - b_i)。物品k的花费则减少了。
为了在我们的模型中体现这一点,我们在从pq中拿出V之后,需要再将a_i - b_i这个值放回pq。
这相当于,物品i使用了优惠券V后,自身变成了一个新的“优惠机会”,其面额为a_i - b_i。如果未来的物品k选择了这个面额为a_i - b_i的机会,就等价于它抢走了V,并补偿了i,使其最终花费为b_i。
总结一下决策过程:
对于物品 i:
- 把能用的新券
v_j加入大根堆pq。 - 如果
pq不为空,取出堆顶V。 - 比较
V和a_i - b_i:- 若
V > a_i - b_i:说明用券划算。总花费增加a_i - V。从pq中弹出V,然后把a_i - b_i压入pq(留下反悔机会)。 - 若
V <= a_i - b_i:说明用折扣价划算。总花费增加b_i。把V再放回pq中(因为没用它)。
- 若
- 如果
pq为空,只能用折扣价,总花费增加b_i。
这个过程可以简化为:
- 把能用的新券
v_j加入大根堆pq。 - 将
a_i - b_i这个“使用折扣价”的选项也看作一个优惠机会,加入pq。 pq中现在包含了所有可用的优惠券,以及“使用折扣价”这个选项(其优惠力度为a_i-b_i)。- 我们为当前物品选择
pq中最好(最大)的一个优惠机会V。 - 总花费增加
a_i - V。V被消耗,从pq弹出。
这两种描述是等价的,第二种在实现上可能更简洁。但第一种更能体现“反悔”的逻辑。下面的代码采用第一种描述的逻辑,更清晰。
AC 代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
// 为提高可读性,定义物品和优惠券的结构体
struct Item {
long long a, b;
};
struct Coupon {
long long w, v;
};
// 自定义比较函数,用于对物品按原价a升序排序
bool compareItems(const Item& x, const Item& y) {
return x.a < y.a;
}
// 自定义比较函数,用于对优惠券按门槛w升序排序
bool compareCoupons(const Coupon& x, const Coupon& y) {
return x.w < y.w;
}
int main() {
// 使用快速I/O处理大规模数据
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int n, m;
std::cin >> n >> m;
std::vector<Item> items(n);
for (int i = 0; i < n; ++i) {
std::cin >> items[i].a >> items[i].b;
}
std::vector<Coupon> coupons(m);
for (int i = 0; i < m; ++i) {
std::cin >> coupons[i].w >> coupons[i].v;
}
// 排序是贪心算法的关键预处理步骤
std::sort(items.begin(), items.end(), compareItems);
std::sort(coupons.begin(), coupons.end(), compareCoupons);
long long total_cost = 0;
// 大根堆,用于存放所有当前可用的“优惠机会”
std::priority_queue<long long> pq;
int coupon_idx = 0; // 优惠券数组的指针
// 遍历每一个物品
for (int i = 0; i < n; ++i) {
long long current_a = items[i].a;
long long current_b = items[i].b;
// 将所有对于当前物品可用的新优惠券加入优先队列
while (coupon_idx < m && coupons[coupon_idx].w <= current_a) {
pq.push(coupons[coupon_idx].v);
coupon_idx++;
}
// 为当前物品做购买决策
if (pq.empty()) {
// 没有任何优惠券可用,只能用折扣价
total_cost += current_b;
} else {
// 获取当前最好的优惠机会
long long best_opportunity = pq.top();
// 比较:使用最好机会的成本 vs. 使用折扣价的成本
// a_i - v < b_i <=> v > a_i - b_i
if (best_opportunity > current_a - current_b) {
// 使用优惠机会更划算
total_cost += current_a - best_opportunity;
pq.pop(); // 消耗掉这个最好的机会
// 【反悔机制】
// 将“物品i反悔,改用折扣价”这个选项作为新的优惠机会放入堆中
// 这个新机会的“面额”是 a_i - b_i
pq.push(current_a - current_b);
} else {
// 使用折扣价更划算 (或一样)
total_cost += current_b;
// 最好的机会没有被使用,它仍然在堆里,留给后续更合适的物品
}
}
}
std::cout << total_cost << std::endl;
return 0;
}
浙公网安备 33010602011771号