P2985 [USACO10FEB] Chocolate Eating S 题解 二分答案
P2985 [USACO10FEB] Chocolate Eating S 题解
题目理解
首先,我们需要清楚地理解题目要求:
- 我们有\(N\)块巧克力,每块有一个特定的快乐值\(H_i\).
- 需要在\(D\)天内吃完这些巧克力
- 每天可以选择吃任意数量的巧克力(可以不吃,也可以全吃)
- 每天获得的快乐值是当天吃的所有巧克力的快乐值之和
- 需要最大化这\(D\)天中最小的每日快乐值(即最大化"最小日快乐")
解题思路
这个问题可以使用二分答案的方法来解决。具体思路如下:
-
二分搜索最小日快乐值:我们可以对可能的最小日快乐值进行二分搜索。对于每一个候选值,我们检查是否可以在\(D\)天内分配巧克力,使得每天的快乐值都不小于这个候选值。
-
检查可行性:对于给定的候选值\(mid\),我们需要判断是否存在一种分配方式满足条件。具体方法是贪心地分配巧克力,尽可能让每天的快乐值达到\(mid\),同时不超过D天。
算法步骤
- 确定二分范围:最小可能值是0,最大可能值是所有巧克力快乐值之和。
- 进行二分搜索:
- 取
mid = (left + right + 1) / 2 - 检查mid是否可行
- 如果可行,则尝试更大的值(
left = mid) - 否则尝试更小的值
(right = mid - 1)
- 取
- 检查可行性的方法:
- 从第一天开始,尽可能多吃巧克力直到当天快乐值\(≥mid\)
- 记录需要的天数
- 如果总天数\(≤D\),则可行
代码实现
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
int N, D;
vector<ll> H;
// 检查是否可以在D天内分配巧克力使得每天快乐值都不小于min_happy
bool check(ll min_happy)
{
ll cur = 0;
int days = 0;
for (int i = 0; i < N;)
{
while (i < N && cur < min_happy)
{
cur += H[i];
i++;
}
if (cur >= min_happy)
{
days++;
cur = 0;
}
if (days >= D)
return true;
}
return days >= D;
}
// 重建分配方案
vector<int> solve(ll min_happy)
{
vector<int> res(N, D); // 默认最后一天吃
ll cur = 0;
int day = 1;
for (int i = 0; i < N;)
{
int start = i;
while (i < N && cur < min_happy)
{
cur += H[i];
i++;
}
for (int j = start; j < i; j++)
{
res[j] = day;
}
if (cur >= min_happy)
{
day++;
cur = 0;
}
if (day > D)
break;
}
return res;
}
int main()
{
cin >> N >> D;
H.resize(N);
ll sum = 0;
for (int i = 0; i < N; i++)
{
cin >> H[i];
sum += H[i];
}
ll left = 0, right = sum;
ll best = 0;
// 二分查找最大可能的最小日快乐值
while (left <= right)
{
ll mid = (left + right) / 2;
if (check(mid))
{
best = mid;
left = mid + 1;
}
else
{
right = mid - 1;
}
}
cout << best << endl;
// 重建分配方案
vector<int> ans = solve(best);
for (int day : ans)
{
cout << day << endl;
}
return 0;
}
代码解释
-
check函数:检查给定的\(min\) \(happy\)是否可行。(
$min_happy$会变成\(min_happy\))通过贪心地分配巧克力,计算需要多少天才能满足每天快乐值\(≥min\) \(happy\)。 -
solve函数:在找到最大\(min\) \(happy\)后,重建具体的分配方案。记录每块巧克力在哪一天被吃掉。
-
main函数:
- 读取输入数据
- 进行二分搜索找到最大可能的最小日快乐值
- 输出结果和分配方案
复杂度分析
- 时间复杂度:\(O(N log(sum(H_i)))\),其中\(sum(H_i)\)是所有巧克力快乐值的和。二分搜索需要\(O(log(sum(H_i)))\)次迭代,每次check需要\(O(N)\)时间。
- 空间复杂度:\(O(N)\),用于存储巧克力快乐值和分配方案。
注意事项
- 二分搜索的边界条件要正确处理,避免无限循环。
- 在重建分配方案时,要注意处理最后几天可能不需要吃满的情况。
- 题目要求输出方案时,如果有多解,输出最早达到最小快乐值的方案,我们的贪心方法自然满足这一条件。
25-8-8 补
朴素做法(非二分答案方法)题解
对于P2985这道题,除了二分答案的优化方法外,我们也可以考虑一种更直观的朴素做法。虽然时间复杂度较高,但对于理解问题本质很有帮助。
朴素思路
- 直接枚举最小日快乐值:从可能的最大值开始向下枚举,直到找到第一个可行的值。
- 验证每个候选值:对于每个候选的最小日快乐值,模拟分配过程,检查是否能在D天内完成分配。
- 记录最优解:一旦找到可行的分配方案,立即记录并输出结果。
算法步骤
- 计算所有巧克力快乐值总和sum
- 从sum/D开始向下枚举可能的最小日快乐值H
- 对于每个H:
- 模拟分配过程:每天尽可能多吃巧克力直到当天快乐值≥H
- 如果能在D天内分配完所有巧克力且满足每日≥H,则H就是解
- 输出最大的满足条件的H及其分配方案
代码实现
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
int N, D;
vector<ll> H;
// 检查给定min_happy是否可行,并返回分配方案
pair<bool, vector<int>> check_slow(ll min_happy)
{
vector<int> res(N, D); // 默认最后一天吃
ll cur = 0;
int day = 1;
int i = 0;
while (i < N && day <= D)
{
int start = i;
while (i < N && cur < min_happy)
{
cur += H[i];
i++;
}
for (int j = start; j < i; j++)
{
res[j] = day;
}
if (cur >= min_happy)
{
day++;
cur = 0;
}
}
bool ok = (i == N) && (day <= D + 1);
return {ok, res};
}
int main()
{
cin >> N >> D;
H.resize(N);
ll sum = 0;
for (int i = 0; i < N; i++)
{
cin >> H[i];
sum += H[i];
}
ll max_possible = sum / D;
ll best_h = 0;
vector<int> best_schedule;
// 从大到小枚举可能的最小日快乐值
for (ll h = max_possible; h >= 0; h--)
{
auto [ok, res] = check_slow(h);
if (ok) {
best_h = h;
best_schedule = res;
break; // 找到最大的可行h即可停止
}
}
cout << best_h << endl;
for (int day : best_schedule)
{
cout << day << endl;
}
return 0;
}
代码解释
-
check_slow函数:
- 尝试按照给定的\(min_happy\)分配巧克力
- 返回是否可行以及具体的分配方案
- 使用贪心方法,每天尽可能多吃直到达到\(min_happy\)
-
主函数:
- 读取输入数据
- 计算所有巧克力快乐值总和
- 从可能的最大值\(sum/D\)开始向下枚举
- 对每个候选值检查可行性
- 找到第一个可行的值即为解
- 输出结果和分配方案
复杂度分析
- 时间复杂度:\(O(sum/D × N)\),其中\(sum\)是所有巧克力快乐值的和。最坏情况下需要枚举\(sum/D\)次,每次检查需要\(O(N)\)时间。
- 空间复杂度:\(O(N)\),用于存储巧克力快乐值和分配方案。
优缺点
优点:
- 思路直观,易于理解和实现
- 不需要理解二分搜索等高级算法
缺点:
- 当\(sum\)很大时,枚举次数会非常多,效率低下
- 对于大规模数据无法在合理时间内完成
优化方向
这个朴素方法虽然简单,但对于较大的数据集显然不够高效。可以在此基础上进行以下优化:
- 从合理范围开始枚举:不必从0开始,可以从\(sum/D\)开始向下枚举
- 提前终止:一旦找到可行解立即终止,因为是从大到小枚举
- 转化为二分搜索:这正是我们之前给出的优化方法,将时间复杂度从\(O(sum/D × N)\)降为\(O(N log(sum))\)
浙公网安备 33010602011771号