java实现 n人过桥问题

【问题描述】

n个人要晚上过桥,在任何时候最多两个人一组过桥,每组要有一只手电筒。在这n个人中只有一个手电筒能用,求这些人过桥所用的最短时间。

【输入】

输入的第一行给出n,接下来的n行给出每个人的过桥时间
例如: 5 1 2 3 4 5

【输出】

输出的第一行给出所有n个人过桥的总的秒数,接下来的若干行给出实现策略。每行包括一个或两个整数,表示组成一组过桥的一个或两个人,以所用的时间标识。
5
1 2 3 4 5
去: 1, 2
回: 1
去: 4, 5
回: 2
去: 1, 3
回: 1
去: 1, 2
最短时间: 16

【问题分析】

当看到求最短、最长、最多、最少等字样的时候,大概率会是动态规划或者贪心算法的问题。n人过桥问题是比较典型的贪心算法,由于一次过桥最多两人且手电筒需要往返传递,因此以两个成员过桥为一个分析单位,计算过桥时间。首先按过桥时间顺序从小到大排序,nums[1]为最小,nums[n]为最大。

我们可以这么思考贪心算法的问题:将贪心问题分为多个子问题,这些子问题相互之间没有关联(动态规划的每一个子问题都会相互关联,这是与贪心算法的主要区别),且每个子问题的状态都是独立的。那么我们可以这样思考n人过桥问题:每个子问题所求的都是过去两人所花费的最短时间,且手电要回到起始点。贪心就贪在每次我都想让两个最费时的人过去。

基于上述思想,问题简化为如何求得最费时的两个人过去所花费的最短时间呢?这里有点拗口哈。有两种方案:

方案一:用最快的成员nums[1]传递手电筒帮助最慢的nums[n]和nums[n-1]过桥,易知来回所用的时间为2*nums[1]+nums[n]+nums[n-1]。

方案二:用最快的成员nums[1]和次快的成员nums[2]传递手电筒帮助最慢的nums[n]和nums[n-1]过桥,具体方案如下:

第一步:nums[1]和nums[2]到对岸,所用时间为nums[2];

第二步:nums[1]返回,将手电筒给最慢的nums[n]和nums[n-1],并且nums[n]和nums[n-1]到对岸后将手电筒交给nums[2],所用时间为:nums[1]+nums[n];

第三步:nums[2]返回,所用时间为nums[2];

综合起来方案二所用的总时间为2*nums[2]+nums[n]+nums[1]。

最后,不论是动态规划还是贪心算法,都需要考虑边界条件

显然,这里的边界条件是: 最后尚未过桥的人可能有3人或者2人,如果是3人,则最少过桥时间为nums[1]+nums[2]+nums[3],如果是2人,则是nums[1]+nums[2]。下面是算法实现:

public class Test {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt(); // 读取人数
        int[] nums = new int[n+1];
        Arrays.sort(nums);
        for (int i = 1; i <= n; i++) {
            nums[i] = sc.nextInt();
        }
        int result = 0;
        int stay = n;
        while(stay > 3){
            if(nums[1] + nums[stay-1] > 2*nums[2]) {
                // 第二种方案
                System.out.println("去: " + nums[1] + ", " + nums[2]);
                System.out.println("回: " + nums[1]);
                System.out.println("去: " + nums[stay-1] + ", " + nums[stay]);
                System.out.println("回: " + nums[2]);
                result += 2*nums[2] + nums[stay] + nums[1];
            } else {
                // 第一种方案
                System.out.println("去: " + nums[1] + ", " + nums[stay]);
                System.out.println("回: " + nums[1]);
                System.out.println("去: " + nums[1] + ", " + nums[stay-1]);
                System.out.println("去: " + nums[1]);
                result += 2*nums[1] + nums[stay] + nums[stay-1];
            }
            stay -= 2;
        }

        if (stay == 3){
            System.out.println("去: " + nums[1] + ", " + nums[3]);
            System.out.println("回: " + nums[1]);
            System.out.println("去: " + nums[1] + ", " + nums[2]);
            result += (nums[1] + nums[2] + nums[3]);
        } else {
            System.out.println("去:" + nums[1] + ", " + nums[2]);
            result += (nums[2]);
        }
        System.out.println("最短时间: " + result);
    }
}

当然,如果不需要显示来回的过程,只需要把打印语句去掉即可,这样使代码变得简便:

public class Test {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt(); // 读取人数
        int[] nums = new int[n+1];
        Arrays.sort(nums);
        for (int i = 1; i <= n; i++) {
            nums[i] = sc.nextInt();
        }
        int result = 0;
        int stay = n;
        while(stay > 3){
            if(nums[1] + nums[stay-1] > 2*nums[2]) {
                // 第二种方案
                result += 2*nums[2] + nums[stay] + nums[1];
            } else {
                // 第一种方案
                result += 2*nums[1] + nums[stay] + nums[stay-1];
            }
            stay -= 2;
        }
        if (stay == 3){
            result += (nums[1] + nums[2] + nums[3]);
        } else {
            result += (nums[2]);
        }
        System.out.println("最短时间: " + result);
    }
}