1. 问题分析与算法设计
1.1 问题重述与理解
这是一个经典的过桥问题变种。题目描述如下:一群旅行者需要在夜间过桥,他们只有一支火把。每次最多只能有两个人同时过桥,且必须携带火把。过桥时间以两人中较慢者的时间为准。我们需要计算所有旅行者全部过桥的最短总时间。
关键约束条件:
- 每次过桥人数:1-2人
- 必须携带火把
- 过桥时间取较慢者
- 初始所有人都在桥的同一侧
1.2 输入输出分析
输入格式:
- 第一行:旅行者人数n
- 接下来n行:每个旅行者的过桥时间(已排序)
输出格式:
- 一个整数:所有旅行者过桥的最短总时间
示例分析:
输入:
code复制4
6
7
10
15
最优解为42,对应的过桥顺序可能是:
- 6和7过桥(耗时7)
- 6带火把返回(耗时6)
- 10和15过桥(耗时15)
- 7带火把返回(耗时7)
- 6和7过桥(耗时7)
总时间:7+6+15+7+7=42
1.3 算法选择与思路
这个问题属于典型的贪心算法应用场景。我们需要考虑两种主要的过桥策略:
策略A(快带慢):
- 最快的两人先过桥(时间t2)
- 最快的人带火把返回(时间t1)
- 最慢的两人过桥(时间tn)
- 次快的人带火把返回(时间t2)
总时间:t2 + t1 + tn + t2
策略B(快带快):
- 最快和最慢的人过桥(时间tn)
- 最快的人带火把返回(时间t1)
- 最快和次慢的人过桥(时间tn-1)
- 最快的人带火把返回(时间t1)
总时间:tn + t1 + tn-1 + t1
在实际计算中,我们需要比较这两种策略的时间消耗,选择较小的那个。对于剩余人数≤3的情况,可以直接处理。
2. 代码实现详解
2.1 预处理与输入处理
cpp复制#include<bits/stdc++.h>
using namespace std;
long long n,a[1000005],sum; //数组开大点防止越界
int main()
{
scanf("%lld",&n); //使用scanf提高输入效率
for(int i = 1;i <= n;i++)
scanf("%lld",&a[i]);
sort(a + 1,a + n + 1); //确保时间有序
代码说明:
- 使用long long防止大数溢出
- 数组大小设为1e6+5,满足题目n≤1e5的要求
- 使用scanf而非cin提高输入效率(对于大数据量很重要)
- 对输入的时间进行排序,这是后续贪心策略的基础
2.2 特殊情况处理
cpp复制if(n <= 2) //特判
{
cout << a[n]; //1人输出a[1],2人输出较慢者
return 0;
}
处理边界情况:
- 1人:直接过桥,时间a[1]
- 2人:一起过桥,时间a[2]
2.3 主算法逻辑
cpp复制while(n > 3)
{
sum += min(a[1] * 2 + a[n] + a[n - 1],
a[1] + a[2] * 2 + a[n]);
n -= 2;
}
这是算法的核心部分,比较两种策略:
-
策略A:a[1]*2 + a[n] + a[n-1]
- 最快两人过去(a[2])
- 最快的人回来(a[1])
- 最慢两人过去(a[n])
- 次快的人回来(a[2])
- 总时间:a[2]+a[1]+a[n]+a[2] = a[1]+2*a[2]+a[n]
-
策略B:a[1] + a[2]*2 + a[n]
- 最快和最慢过去(a[n])
- 最快回来(a[1])
- 最快和次慢过去(a[n-1])
- 最快回来(a[1])
- 总时间:a[n]+a[1]+a[n-1]+a[1] = 2*a[1]+a[n-1]+a[n]
每次处理完最慢的两人后,n减2。
2.4 剩余人数处理
cpp复制if(n % 2 == 0)
sum += a[2];
else
sum += a[1] + a[2] + a[n];
处理剩余3人或2人的情况:
- 剩余2人:直接一起过桥(a[2])
- 剩余3人:
- 最快两人过去(a[2])
- 最快回来(a[1])
- 最快和最慢过去(a[3])
- 总时间:a[2]+a[1]+a[3]
2.5 输出结果
cpp复制printf("%lld",sum);
return 0;
使用printf输出long long类型的结果。
3. 算法正确性证明
3.1 贪心选择性质
我们需要证明每次选择两种策略中较优的那个,最终能得到全局最优解。
关键观察:
- 最慢的两人必须过桥,且他们一起过桥比分开过桥更优
- 火把必须被带回,由最快或次快的人带回效率最高
- 两种策略覆盖了最优解可能的情况
3.2 最优子结构
每次处理最慢的两人后,问题规模减小,且子问题的最优解能构成原问题的最优解。
3.3 数学归纳法证明
基础情况:n≤3时,算法给出明显最优解
归纳假设:假设对n=k成立,证明对n=k+2成立
通过比较两种策略的时间消耗,选择较小的那个保证了最优性
4. 复杂度分析与优化
4.1 时间复杂度
主要时间消耗:
- 排序:O(nlogn)
- 主循环:O(n)(每次处理2人)
总时间复杂度:O(nlogn)
4.2 空间复杂度
使用一个数组存储时间:O(n)
4.3 可能的优化
- 输入优化:使用更快的输入方式(如快速读入)
- 输出优化:使用快速输出
- 对于n特别大的情况,可以尝试并行化排序
5. 测试用例设计
5.1 常规测试用例
plaintext复制输入:
4
1
2
5
10
输出:
17
解释:
1和2过去(2)
1回来(1)
5和10过去(10)
2回来(2)
1和2过去(2)
总计:2+1+10+2+2=17
5.2 边界测试用例
plaintext复制输入:
1
5
输出:
5
plaintext复制输入:
2
3
7
输出:
7
5.3 大规模测试用例
plaintext复制输入:
100000
[1,1,...,1,1000000000]
输出:
1000000000 + 99999*1 ≈ 1000099999
6. 常见错误与调试技巧
6.1 常见错误
- 未对输入时间排序:导致贪心策略失效
- 数组大小不足:n最大1e5,数组至少1e5+5
- 整数溢出:未使用long long导致大数溢出
- 剩余人数处理错误:特别是n=3的情况
6.2 调试技巧
- 打印中间结果:在循环中打印sum和n的值
- 小规模测试:先验证小规模数据的正确性
- 边界测试:特别测试n=1,2,3,4的情况
- 对拍:与暴力解法比较结果
7. 算法扩展与变种
7.1 类似问题
- 不同过桥人数限制(如每次最多3人)
- 多支火把的情况
- 不同方向的过桥需求
7.2 更优算法
对于某些特殊分布的数据,可能存在更优的策略,但本解法在一般情况下已经是最优。
7.3 实际应用
这类问题可以应用于:
- 资源调度
- 任务分配
- 交通管制
8. 个人实现心得
在实际编程竞赛中,这类贪心问题需要注意以下几点:
- 排序是基础:确保数据有序才能应用贪心策略
- 边界情况:小规模数据往往需要特殊处理
- 策略比较:有时需要比较多种策略,不能想当然
- 数据类型:注意数据范围,防止溢出
在实现这个算法时,我最初忽略了n=3的特殊情况,导致了一些错误。通过仔细分析剩余人数处理逻辑,最终修正了代码。这也提醒我,在编写贪心算法时,必须全面考虑所有可能的剩余情况。