每日算法 — 使用java实现赛马问题:25匹马经典智力题的最优策略

引言

“25匹马找前3名”是算法面试中极具代表性的智力题。给定25匹马和5条赛道,每场比赛最多让5匹马同时赛跑(只能比较相对快慢,无法计时),最少需要多少场比赛才能确定跑得最快的3匹马?直观答案可能是7场或8场,但通过严谨的数学分析可以证明:最少仅需7场比赛。本文将用Java完整模拟这一经典问题,展示如何通过分治策略与淘汰法逐步缩小候选集,最终锁定全局前3名。

问题分析

初始分组策略

将25匹马分为5组(A、B、C、D、E),每组5匹,进行5场初赛:

  • 第1-5场:各组内部比赛,确定每组内部排名

关键淘汰赛

第6场:让每组第1名(A1, B1, C1, D1, E1)比赛,假设结果A1 > B1 > C1 > D1 > E1。

这一场比赛蕴含了丰富的淘汰信息:

  1. A1是全局第1名(击败了所有组第1名)
  2. D组、E组全部被排除(D1、E1连前5都进不了,其组员更不可能进前3)
  3. C2及以后被排除(C1最多第3,C2不可能进前3)
  4. B3及以后被排除(B1最多第2,B2最多第3)
  5. A4、A5被排除(A1是第1,A2、A3可能争第2、3,但A4不可能)

最终决赛

第7场:仅需比较剩余候选马匹:A2, A3, B1, B2, C1

这5匹马中的前2名(去掉全局第1的A1)即为全局第2、3名。

Java实现

import java.util.*;

/**
 * 25匹马找前3名经典问题求解器
 * 通过分组赛、淘汰赛与决赛三阶段确定全局前三
 */
public class HorseRacing {

    /**
     * 马匹类:持有唯一ID和真实速度(模拟用)
     */
    static class Horse implements Comparable<Horse> {
        final String id;
        final double speed; // 真实速度,比赛时不暴露

        public Horse(String id, double speed) {
            this.id = id;
            this.speed = speed;
        }

        @Override
        public int compareTo(Horse other) {
            // 按速度降序排列(快的在前)
            return Double.compare(other.speed, this.speed);
        }

        @Override
        public String toString() {
            return id + "(" + String.format("%.1f", speed) + ")";
        }
    }

    /**
     * 模拟一场比赛,返回按名次排序的马匹列表(快的在前)
     */
    public static List<Horse> race(List<Horse> horses) {
        List<Horse> result = new ArrayList<>(horses);
        result.sort(null); // 使用Horse的compareTo按速度排序
        return result;
    }

    /**
     * 打印比赛结果
     */
    public static void printRace(String name, List<Horse> result) {
        System.out.print(name + ": ");
        for (int i = 0; i < result.size(); i++) {
            System.out.print((i + 1) + "." + result.get(i).id + " ");
        }
        System.out.println();
    }

    /**
     * 生成25匹速度各不相同的马
     */
    public static List<Horse> generateHorses() {
        List<Horse> horses = new ArrayList<>();
        Random rand = new Random(42);
        Set<Double> speeds = new HashSet<>();

        // 生成25个唯一速度值
        while (speeds.size() < 25) {
            speeds.add(rand.nextDouble() * 100);
        }

        Double[] speedArray = speeds.toArray(new Double[0]);
        char group = 'A';
        for (int i = 0; i < 25; i++) {
            if (i > 0 && i % 5 == 0) group++;
            String id = "" + group + (i % 5 + 1);
            horses.add(new Horse(id, speedArray[i]));
        }

        return horses;
    }

    /**
     * 核心算法:7场比赛找前3名
     * @param horses 25匹马
     * @return 全局前3名
     */
    public static List<Horse> findTop3(List<Horse> horses) {
        if (horses.size() != 25) {
            throw new IllegalArgumentException("需要恰好25匹马");
        }

        int raceCount = 0;

        // === 第一阶段:5场初赛 ===
        Map<Character, List<Horse>> groups = new HashMap<>();
        List<List<Horse>> groupRaces = new ArrayList<>();

        for (int g = 0; g < 5; g++) {
            List<Horse> group = new ArrayList<>(horses.subList(g * 5, (g + 1) * 5));
            List<Horse> result = race(group);
            groups.put((char) ('A' + g), result);
            groupRaces.add(result);
            raceCount++;
            printRace("初赛 " + (char) ('A' + g) + "组", result);
        }

        // === 第二阶段:第6场,各组第1名对决 ===
        List<Horse> groupWinners = new ArrayList<>();
        for (int g = 0; g < 5; g++) {
            groupWinners.add(groupRaces.get(g).get(0)); // 每组第1名
        }
        List<Horse> winnerRace = race(groupWinners);
        raceCount++;
        printRace("淘汰赛(各组冠军)", winnerRace);

        // 确定各组在冠军赛中的排名
        // winnerRace按速度排序,winnerRace.get(0)是全局最快的组的第1名
        Map<Horse, Integer> winnerRank = new HashMap<>();
        for (int i = 0; i < 5; i++) {
            winnerRank.put(winnerRace.get(i), i);
        }

        // 确定每组编号
        Map<Horse, Character> horseToGroup = new HashMap<>();
        for (int g = 0; g < 5; g++) {
            char groupName = (char) ('A' + g);
            for (Horse h : groups.get(groupName)) {
                horseToGroup.put(h, groupName);
            }
        }

        // === 淘汰分析 ===
        // 冠军赛第1名所在组 -> 该组的第1名是全局第1
        // 冠军赛第4、5名所在组 -> 整组淘汰(不可能进前3)
        // 冠军赛第3名所在组 -> 仅保留第1名(最多第3)
        // 冠军赛第2名所在组 -> 保留前2名(第2名可能争第2或第3)
        // 冠军赛第1名所在组 -> 保留前3名(第2、3名可能争第2、3)

        System.out.println("\n=== 淘汰分析 ===");

        List<Horse> candidates = new ArrayList<>();

        for (int g = 0; g < 5; g++) {
            char groupName = (char) ('A' + g);
            List<Horse> group = groups.get(groupName);
            Horse groupFirst = group.get(0);
            int rankInWinnerRace = winnerRank.get(groupFirst);

            int keepCount = 0;
            if (rankInWinnerRace == 0) keepCount = 3;      // 冠军组保留前3
            else if (rankInWinnerRace == 1) keepCount = 2; // 亚军组保留前2
            else if (rankInWinnerRace == 2) keepCount = 1; // 季军组保留第1
            else keepCount = 0;                            // 第4、5名组全部淘汰

            System.out.printf("%c组(冠军赛排名%d): 保留前%d名 -> ", 
                    groupName, rankInWinnerRace + 1, keepCount);
            for (int i = 0; i < keepCount; i++) {
                candidates.add(group.get(i));
                System.out.print(group.get(i).id + " ");
            }
            System.out.println();
        }

        // === 第三阶段:第7场,决赛 ===
        // 注意:全局第1已经确定(冠军赛的冠军),决赛是找第2、3名
        // 所以决赛的5匹马中的前2名(排除已知的第1)就是全局第2、3

        // 但实际上我们已经知道A1(假设A组赢了冠军赛)是第1
        // 决赛5匹:A2, A3, B1, B2, C1(假设A>B>C>D>E)
        // 这5匹中的前2名就是全局第2、3

        System.out.println("\n=== 决赛(确定第2、3名) ===");

        // 从候选中移除已确定的全局第1(冠军赛的冠军)
        Horse globalFirst = winnerRace.get(0);
        candidates.remove(globalFirst);

        if (candidates.size() > 5) {
            // 正常情况下应该是5匹,如果更多需要截取(理论上不会发生)
            candidates = candidates.subList(0, 5);
        }

        List<Horse> finalRace = race(candidates);
        raceCount++;
        printRace("决赛", finalRace);

        // 组装最终前3名
        List<Horse> top3 = new ArrayList<>();
        top3.add(globalFirst);
        top3.add(finalRace.get(0)); // 决赛第1名是全局第2
        top3.add(finalRace.get(1)); // 决赛第2名是全局第3

        System.out.println("\n=== 最终结果 ===");
        System.out.println("比赛总场次: " + raceCount);
        System.out.println("全局第1名: " + top3.get(0));
        System.out.println("全局第2名: " + top3.get(1));
        System.out.println("全局第3名: " + top3.get(2));

        return top3;
    }

    /**
     * 验证算法正确性:与直接排序结果对比
     */
    public static void verify(List<Horse> horses, List<Horse> top3) {
        List<Horse> sorted = new ArrayList<>(horses);
        sorted.sort(null);

        System.out.println("\n=== 正确性验证 ===");
        System.out.println("直接排序前3名: " + sorted.get(0).id + ", " + 
                sorted.get(1).id + ", " + sorted.get(2).id);
        System.out.println("算法结果前3名: " + top3.get(0).id + ", " + 
                top3.get(1).id + ", " + top3.get(2).id);

        boolean correct = top3.get(0).id.equals(sorted.get(0).id) &&
                         top3.get(1).id.equals(sorted.get(1).id) &&
                         top3.get(2).id.equals(sorted.get(2).id);
        System.out.println("结果正确: " + correct);
    }

    /**
     * 通用化:N匹马、M条赛道、找前K名的最少比赛场数
     * 本问题特化:N=25, M=5, K=3 -> 答案为7
     */
    public static void analyzeGeneralCase() {
        System.out.println("\n=== 问题泛化分析 ===");
        System.out.println("25匹马、5条赛道、找前3名:");
        System.out.println("  - 初赛: ceil(25/5) = 5场");
        System.out.println("  - 各组冠军赛: 1场");
        System.out.println("  - 决赛: 1场(剩余候选不超过5匹)");
        System.out.println("  - 总计: 7场(理论下界)");
        System.out.println("\n关键洞察:");
        System.out.println("  1. 冠军赛的第4、5名组可直接淘汰整组");
        System.out.println("  2. 冠军赛的第3名组仅第1名可能进前3");
        System.out.println("  3. 冠军赛的第2名组仅前2名可能进前3");
        System.out.println("  4. 冠军赛的第1名组前3名都可能进前3");
        System.out.println("  5. 最终候选集恰好5匹,刚好一场决赛");
    }

    public static void main(String[] args) {
        List<Horse> horses = generateHorses();
        System.out.println("25匹马初始状态(速度随机):");
        for (Horse h : horses) {
            System.out.print(h.id + "=" + String.format("%.1f", h.speed) + " ");
            if (h.id.endsWith("5")) System.out.println();
        }
        System.out.println();

        List<Horse> top3 = findTop3(horses);
        verify(horses, top3);
        analyzeGeneralCase();
    }
}

复杂度分析

  • 比赛场次:严格7场,这是理论下界
  • 每场比赛:O(M log M) 时间用于排序(M=5,实际为常数)
  • 总时间:O(N) 对于固定赛道数M,其中N=25
  • 空间复杂度:O(N) 存储马匹信息

最少7场的严谨证明

下界证明:

  1. 每匹马至少参加1场比赛才有排名依据 → 至少5场初赛
  2. 5个组的冠军必须互相比较,否则无法确定全局第1 → 至少1场冠军赛
  3. 经过淘汰后,仍有至少2匹马可能竞争第2、3名 → 至少1场决赛
  4. 总计下界:5 + 1 + 1 = 7场

本算法达到此下界,因此是最优解。该问题完美展示了信息论下界与分治淘汰的结合:通过每一场比赛最大化排除不可能项,将指数级搜索空间压缩到常数轮次。