题目描述
阿德尼亚首都周围有 nnn 个湖泊,每个湖泊初始都是满的。预计未来 mmm 天将有强降雨,每天的天气预报由一个序列 t1,t2,…,tmt_1, t_2, \ldots, t_mt1,t2,…,tm 表示:
- 如果 ti∈[1,n]t_i \in [1, n]ti∈[1,n],表示第 iii 天在湖泊 tit_iti 上有强降雨
- 如果 ti=0t_i = 0ti=0,表示第 iii 天没有雨,龙可以喝干一个湖泊的水
降雨规则:
- 如果湖泊是空的,下雨后会被填满
- 如果湖泊是满的,下雨会导致溢出,引发自然灾害
目标:确定龙在非雨天的喝水策略,防止任何湖泊溢出。
输入格式
- 第一行:测试用例数 Z≤40Z \leq 40Z≤40
- 每个测试用例:
- 第一行:n≤106n \leq 10^6n≤106 和 m≤106m \leq 10^6m≤106
- 第二行:mmm 个整数表示天气预报
输出格式
- 如果可以防止溢出:输出
YES,第二行输出每个非雨天喝水的湖泊编号(000 表示不喝水) - 否则:输出
NO
题目分析
关键观察
- 初始状态:所有湖泊都是满的
- 第一次下雨:由于湖泊初始是满的,第一次下雨时必须提前喝掉该湖泊的水
- 后续下雨:每次下雨时,如果湖泊是满的,必须在上次下雨后和这次下雨前喝掉水
- 喝水时机:龙只能在非雨天(ti=0t_i = 0ti=0)喝水
问题转化
将问题转化为区间匹配问题:
- 对于每个湖泊,生成一系列必须喝水的时间区间
- 每个区间需要匹配一个在区间内的非雨天
- 每个非雨天只能被使用一次
区间定义
对于湖泊 lakelakelake,设其下雨时间为:time1,time2,…,timektime_1, time_2, \ldots, time_ktime1,time2,…,timek
需要喝水的区间为:
- [1,time1−1][1, time_1 - 1][1,time1−1]:在第一次下雨前喝掉
- [time1+1,time2−1][time_1 + 1, time_2 - 1][time1+1,time2−1]:在第一次和第二次下雨之间喝掉
- …\ldots…
- [timek−1+1,timek−1][time_{k-1} + 1, time_k - 1][timek−1+1,timek−1]:在倒数第二次和最后一次下雨之间喝掉
重要:如果某个区间无效(左端点 > 右端点),说明两次下雨之间没有非雨天,必然溢出。
解题思路
算法选择:贪心算法
这是一个经典的区间点覆盖问题,使用贪心算法:
- 区间生成:为每个湖泊生成所有有效的喝水区间
- 区间排序:按右端点升序排序
- 贪心匹配:为每个区间分配最小的可用非雨天
贪心正确性证明
- 右端点最小的区间最"紧急"
- 为它选择最小的可用点,不会使后续区间失去覆盖机会
- 如果选择较大的点,可能使后续区间失去唯一的覆盖机会
算法步骤
-
预处理:
- 收集所有非雨天的位置
- 记录每个湖泊的所有下雨时间
-
区间生成:
- 对于每个湖泊,生成所有有效的喝水区间
- 检查区间有效性,发现无效区间立即返回
NO
-
贪心匹配:
- 区间按右端点排序
- 维护可用非雨天的有序集合
- 对于每个区间 [L,R][L, R][L,R]:
- 找到 ≥L\geq L≥L 的最小非雨天
- 如果该点 ≤R\leq R≤R,使用它并从集合中移除
- 否则返回
NO
-
结果构建:记录每个非雨天喝水的湖泊
复杂度分析
- 时间复杂度:O(mlogm)O(m \log m)O(mlogm)
- 预处理:O(m)O(m)O(m)
- 排序:O(klogk)O(k \log k)O(klogk),其中 k≤mk \leq mk≤m
- 贪心匹配:O(klogm)O(k \log m)O(klogm)
- 空间复杂度:O(n+m)O(n + m)O(n+m)
参考代码
// Enter The Dragon
// UVa ID: 1623
// Verdict: Accepted
// Submission Date: 2025-11-25
// UVa Run Time: 0.730s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
vector<int> solveTestCase(int n, int m, vector<int>& forecast) {
vector<int> drys, result;
vector<vector<int>> rains(n + 1);
// 收集非雨天和下雨信息
for (int i = 0; i < m; i++) {
if (forecast[i] == 0) drys.push_back(i + 1);
else rains[forecast[i]].push_back(i + 1);
}
result.resize(drys.size(), 0);
set<int> drySet(drys.begin(), drys.end());
vector<pair<int, pair<int, int>>> tasks;
// 为每个湖泊生成喝水任务
for (int lake = 1; lake <= n; lake++) {
if (rains[lake].empty()) continue;
for (int i = 0; i < rains[lake].size(); i++) {
// 计算区间左右端点
int left = (i == 0) ? 1 : rains[lake][i - 1] + 1;
int right = rains[lake][i] - 1;
// 检查区间有效性
if (left > right) return vector<int>();
tasks.push_back(make_pair(right, make_pair(left, lake)));
}
}
// 按右端点排序
sort(tasks.begin(), tasks.end());
// 贪心匹配
for (auto task : tasks) {
int right = task.first;
int left = task.second.first;
int lake = task.second.second;
// 找到在区间内的最小可用非雨天
auto it = drySet.lower_bound(left);
if (it == drySet.end() || *it > right) return vector<int>();
int dryDay = *it;
drySet.erase(it);
// 记录匹配结果
result[lower_bound(drys.begin(), drys.end(), dryDay) - drys.begin()] = lake;
}
return result;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int Z;
cin >> Z;
while (Z--) {
int n, m;
cin >> n >> m;
vector<int> forecast(m);
for (int i = 0; i < m; i++) cin >> forecast[i];
vector<int> result = solveTestCase(n, m, forecast);
if (result.empty()) cout << "NO\n";
else {
cout << "YES\n";
for (int i = 0; i < result.size(); i++) {
if (i > 0) cout << " ";
cout << result[i];
}
cout << "\n";
}
}
return 0;
}
总结
本题的关键在于将防止湖泊溢出的问题转化为区间匹配问题,并使用贪心算法高效解决。算法的时间复杂度和空间复杂度都能很好地处理题目给出的数据规模(n,m≤106n, m \leq 10^6n,m≤106)。
核心技巧:
- 正确识别必须喝水的区间
- 使用贪心算法进行区间点覆盖
- 利用
set和lower_bound进行高效查找
这种区间匹配的贪心思路在编程竞赛中非常常见,掌握后可以解决许多类似问题。
457

被折叠的 条评论
为什么被折叠?



