题目
一支N个士兵的军队正在趁夜色逃亡,途中遇到一条湍急的大河。
敌军在T的时长后到达河面,没到过对岸的士兵都会被消灭。
现在军队只找到了一只小船,这船最多能同时坐上2个士兵。
1)当一个士兵划船过河,用时为a[i];0 <= i < N
2)当两个士兵坐船同时划船过河时,用时为 max(a[j], a[i]) 两士兵中用时最长的。
3)当两个士兵坐船一个士兵划船时,用时为 a[i] * 10 ; a[i] 为划船士兵用时。
4)如果士兵下河游泳,则会被湍急水流直接带走,算作死亡。
请帮忙给出一种解决方案,保证存活的士兵最多,且过河用时最短。
输入描述
第一行:N 表示士兵数(0 < N < 1,000,000)
第二行:T 表示敌军到达时长(0 < T < 100,000,000)
第三行:a[0] a[1] … a[i] … a[N - 1]
a[i] 表示每个士兵的过河时长。
(10 < a[i] < 100; 0 < i < N)
输出描述
第一行:“最多存活士兵数” “最短用时”
用例
| 输入 | 输出 | 说明 |
|---|---|---|
|
5 43 12 13 15 20 50 | 3 40 | 可以达到或小于43的一种方案: 第一步:a[0] a[1] 过河用时: 13 第二步:a[0] 返回用时: 12 第三步:a[0] a[2] 过河用时: 15 |
|
5 130 50 12 13 15 20 | 5 128 |
可以达到或小于130的一种方案: |
|
7 171 25 12 13 15 20 35 20 | 7 171 |
可以达到或小于171的一种方案: |
思考
题目要尽可能多的士兵过河,然后才是过河时间最短。看到题目中给出了很多计算过河的时间花费,猜测可能用到动态规划求解,比如只有一个士兵过河呢,2个士兵、N-1个士兵和N个士兵过河的解之间的关系。如果暴力求解需要把每一种过河方案都枚举一遍,每次选一个不同的士兵从余下士兵中递归枚举直到一个士兵,这其中可能出现许多重复的求解过程,而动态规划比较适合缓存这些重复的计算。(1)先分析一个士兵过河情形,当前敌军到达时间 T > a[0],则士兵能过河,存活数1,最短时间就是 a[0],否则不能过河,存活数0,最短时间 0 ;(2)假如有 2 个士兵,先计算两个人过河最短时长t1 = Min(Max(a[0], a[1]), a[0]*10, a[1]*10),如果t1 < T,则 2 个人都能过河,结果是存活 2,用时最短 t1,如果 t1 >= T,则 2 个人不能都过河,回到 (1) 的计算,这时候尝试让 a[0] 和 a[1] 过河时长最短的士兵过河;(3)假如有 3 个士兵,题目中说明一条船一次只能坐 2 个人,我到底让哪个 2 个先过河呢?我是不是应该先将 3 个士兵的过河时长排个序?我先从小到大排序,a[0] <= a[1] <= a[2]。因为只有一只船,两个人过河后还得让一个人把船划到对岸去,余下的士兵才能继续过河。也就是 3 个人以上过河,除了第一次和最后一次过河,中间每次过河都需要有人从对岸把船划过来,应该尽可能选择过河最快人划船?根据直觉最快的人每次单独划船耗时最短,这样会有效减少回程的累计时间。每次过河怎么配对人员,是最快的和次快的组合还是最快的和最慢的组合,不会最快的和随机选一个组合吧?最慢的过河时间就是最慢的那个人,最慢的过河了就不能让他回程。每次让最快搬运次快的和让最快的搬运最慢的感觉不到差异,至少 3 个士兵看不出来时间差异,先按最快带余下的次快过河这个思路来处理 3 个士兵过河的问题。假如三个士兵过河时间为 [12 13 15],最短时间过河步骤:
- 12 和 13 过河 花费时间 13;
- 12 返程花费时间 12;
- 12 和 15 过河花费时间 15;
因此总的 3 个人过河时长是 13 + 12 + 15 = 40,这时候有人可能说如果敌军到达时长T < 40,这个最短时长就没意义。是这样,但这个计算目的是让 3 个士兵以最短的时间过河,如果这个时间都无法在敌军到达前过河,那么证明 3 个士兵无法一起过河,只能考虑 2个士兵过河情况,把问题规划缩小了。因此,这题解法应该是从1 个士兵一直到 N 个士兵过河最短时间自底向上计算,直到敌军时长无法满足 k 个士兵过河,此时答案就是 k-1 个士兵的能存活,过河时间就是 k-1 个士兵过河最短时间。
为了验证前面用划船时间的最短的士兵返程方案是否是最佳方案,用测试用例2来验证下,5个士兵过河,过河时长分别为 50 12 13 15 20,从小到大排序为 12 13 15 20 50,过河方案:
- 12 和 13 过河, 耗时 13;-
- 12 返程, 耗时 12;-
- 12 带 15 过河, 耗时 15;-
- 12 返程, 耗时 12;-
- 12 带 20 过河, 耗时 20;
- 12 返程, 耗时 12;
- 12 带 50 过河,耗时 50。-
5 个士兵过河总时长是 13 + 12 + 15 + 12 + 20 + 12 + 50 = 134,用例答案是128,方案不对!按照题目用例说明的步骤执行下看看为什么最短时间是128不是134:
- 12 和 13 过河, 耗时 13;-
- 12 返程, 耗时 12;-
- 20 和 50 过河, 耗时 50;-
- 13 返回, 耗时 13;
- 12 和 13 过河, 耗时 13
- 12 返程, 耗时 12;-
- 12 和 15 过河,耗时 15. -
总的时长是 13 + 12 + 50 + 13 + 13 + 12 + 15 = 128。
和这个方案比较发现,并不是每次都让 12 返程,12 在第二次返程后让最慢的 50 和次慢的 20 过河了,为什么这么做?通过仔细比对两个方案发现,让最慢的和次慢的一次过河会更节省时间。观察发现第一个方案20没有和50一起过河导致他们两个耗时最长的家伙各自过河累计耗时70比一起过河慢了20,这个应该就是原因所在,这个134-128 = 6,这个差了6个时长实际上是20 + 12 - (13 + 13) = 6,很明显最快的12和次快的13差距很小,而次慢的20拉大了差距。查阅资料得知这个问题改自吊桥谜题,最优过河策略是尽量保证让最慢带次慢的过河,这样就能让整体过河时间最短。具体方案:1)如果剩下最慢的士兵没过河,需要河对岸最快的士兵返回带最慢的过河;2)剩下两个最慢士兵没过河,需要让河对岸最快士兵返回,让最慢带次慢的士兵过河,再让河对岸次快的士兵返回带最快的士兵过河,综合取这这两种情形中过河最短时间。
算法过程
1、对士兵过河时间按从小到大排序,a[0]<a[1]<... <a[n-1];
2、如果a[0] >T,则没有人能过河,返回"0 0";
3、如果只有一个士兵,返回“1 a[0] ";
4、初始化动态规划数据 dp,容量是N,dp[i] 表示前 i 个士兵过河最短时间,0个士兵过河时间dp[0] = 0,前1个士兵过河最短时间 dp[1] = a[0],前2个士兵过河时间 dp[2] = Min(a[0] * 10, a[1]);
5、如果dp[2] > T,前 2 个士兵过不了河,则让第一个更快的士兵过河,返回 “1 dp[1]";
6、从 i = 3 开始遍历,表示对 3 个以及更多的士兵过河最短时间进行更新,每轮循环根据前面分析的两种情形计算最短时间。
剩下一个最慢士兵没过河的最短时间为 :
dp[i-1]+a[0]+Min(a[0]*10, a[i-1]);
剩下一个最慢河一个次慢的士兵没过河的最短时间:
dp[i-2]+a[0]+Min(a[i-2]*10, a[i-1])+a[1]+Min(a[0]*10, a[1])
综合计算 dp[i] = Min(a1, a2);
7、如果发现 dp[i] > T,那么前 i 个士兵过不了河,前 i-1个士兵能过河,返回 ”i-1, dp[i-1]";
8、循环终止,返回所有士兵过河结果: N dp[N]。
参考代码
function solution(readlines) {
const lines = readlines.trim().split('\n');
const N = parseInt(lines[0]);
const T = parseInt(lines[1]);
const a = lines[2].trim().split(' ').map(Number);
// 对士兵过河时间进行排序
a.sort((x, y) => x - y);
// console.log(a);
if (a[0] > T) return '0 0';
if (N < 2) {
return `1 ${a[0]}`;
}
let dp = new Array(N+1);
dp[0] = 0;
dp[1] = a[0];
dp[2] = Math.min(a[0]*10, a[1]);
if (dp[2] > T) {
return "1 " + dp[1];
}
for (let i = 3; i <= N; i++) {
// 剩下一个最慢士兵没过河,需要河对岸最快士兵划船返回带最慢的过河
let a1 = dp[i-1]+a[0]+Math.min(a[0]*10, a[i-1]);
// 剩下两个最慢士兵没过河,需要河对岸最快士兵划船返回,让最慢的两个士兵先过河,再让次快的士兵返回带最快的士兵过河
let a2 = dp[i-2]+a[0]+Math.min(a[i-2]*10, a[i-1])+a[1]+Math.min(a[0]*10, a[1]);
dp[i] = Math.min(a1, a2); // 取两种方案中最小的时间
if (dp[i] > T) return i-1 + " " + dp[i-1];
}
return N + " " + dp[N];
}
const cases = [
`5
43
12 13 15 20 50`,
`5
130
50 12 13 15 20`,
`7
171
25 12 13 15 20 35 20`
];
cases.forEach(e => {
console.log(solution(e));
});
4898

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



