过桥问题
NOIP某年的初赛题,嗯...
题面
在一个月黑风高的夜晚,有N个人要过桥,他们只有一盏灯,桥很窄,每次最多只能允许两个人过桥,现在告诉你每个人过桥需要的时间,问你最少需要多少时间能使得所有的人都过桥。
(如果两个人的过桥时间不同,则他们一起过桥的时间按照过桥慢的人的时间算。)
输入格式
第一行输入一个整数N
第二行输入N个整数,表示N个人过桥需要的时间
输出格式
输出一行,包含一个整数,表示所有人过桥需要的最少时间
约定
1 ≤ 所有的数 ≤ 1000
样例输入
5 1 3 6 8 12
样例输出
29
样例解释
1 3一起过桥,花费时间3
3回来,花费时间3
8 12一起过桥,花费时间12
1回来,花费时间1
1 6一起过桥,花费时间6
1 回来,花费时间1
1 3一起过桥,花费时间3
一共花费3 + 3 + 12 + 1 + 6 + 1 + 3 = 29分钟
分析
§ 1 搜索
我依然记得,当年那道程序填空写的是搜索,于是我二话不说暴搜就写出来了,代码如下。(还加了个剪枝)
#pragma GCC diagnostic error "-std=c++11" #include <cstdio> #include <algorithm> const int MAXN = 1005; enum class Stat { Left, Right }; enum class Dirc { Go, Back }; Stat pos[MAXN]; int N, T[MAXN], ans = 0x7f7f7f7f; void dfs(Dirc d, int rem, int tot); int main() { scanf("%d", &N); for (int i = 0; i < N; i++) { scanf("%d", &T[i]); pos[i] = Stat::Right; } dfs(Dirc::Go, N, 0); printf("%d\n", ans); return 0; } void dfs(Dirc d, int rem, int tot) { if (tot >= ans) return; int i, j; if (d == Dirc::Go) { if (rem <= 2) { int res = 0; for (i = 0; i < N; i++) if (pos[i] == Stat::Right) res = std::max(res, T[i]); ans = std::min(ans, tot + res); return; } for (i = N - 1; i > 0; i--) if (pos[i] == Stat::Right) { for (j = i - 1; j >= 0; j--) if (pos[j] == Stat::Right) { pos[i] = pos[j] = Stat::Left; dfs(Dirc::Back, rem - 2, tot + std::max(T[i], T[j])); pos[i] = pos[j] = Stat::Right; } } } else { for (i = 0; i < N; i++) if (pos[i] == Stat::Left) { pos[i] = Stat::Right; dfs(Dirc::Go, rem + 1, tot + T[i]); pos[i] = Stat::Left; } } }
这个搜索就很直白了,直接枚举往左和往右走的人。当然,考虑到此题的数据范围到了1000,因此对于搜索或许并不是那么友好。不出所料,上面的程序只能拿50分,剩下的点全部超时。
§ 2 问题性质?
于是我们不得不考虑其他算法。
当然,在这之前,我们不妨还是分析一下这个问题。怎么样使得总共的时间最少?首先,两个人过去了,若对岸仍然有人的话,那么这两个中必有一个要把灯送回去。因此,这个“浪费”的时间,必须使它尽可能少。
那么我们是不是一直用过桥时间最少的人送灯回去就一定好呢?答案是否定的,样例就是最好的反驳了。而且我们可以看到在“样例解释”中,时间最多的人居然是和时间第二多的人一起过河的。
那么我们还需要考虑一种情况,就是时间最多的人和时间第二多的人一起过河,当然了,我们绝对不会让这两者其中一个送灯回对岸,这是肯定不优秀的。因此,我们需要一个时间花费少的人先在对岸等候,再在这两个“慢羊羊”过河之后,把灯送回去;当然,这个人“送灯人”在对岸等候之前,肯定还需要一个过河时间少的人带着灯送他过河在把灯带回。对于这两个人,时间花费最少的两个人必然是最佳选择了。
参考程序
因此我们只需要一个贪心:
#include <cstdio> #include <algorithm> const int MAXN = 1005; int N, A[MAXN]; int main() { scanf("%d", &N); for (int i = 0; i < N; i++) scanf("%d", &A[i]); std::sort(A, A + N); int l = 0, r = N - 1, ans; // l,r表示当前还需处理的人区间的左端点和右端点(已排序) while (r - l >= 0) { if (r - l < 2) { ans += A[r]; break; } if (r - l < 3) { ans += A[l] + A[l + 1] + A[r]; break; } // 特判人少的两种情况 if (A[l + 1] * 2 > A[l] + A[r - 1]) ans += A[l] * 2 + A[r] + A[r - 1]; // 考虑上面说的两种情况,这个是时间最少的人带时间最多的两个过河 else ans += A[l + 1] * 2 + A[l] + A[r]; // 这种就是第二中时间最多的两个一起过河的 r -= 2; // 其实l端点在整个过程中都不会动,因此可以只用r端点 } printf("%d\n", ans); return 0; }
总结
我们考虑一些比较复杂,暴力做复杂度又比较高的题,它不一定需要一些高级算法或数据结构,有时候,解决它的,可能是一个很简单的方法,只是看你想不想到了。