刷题打卡之—JAVA—《最佳人选》| 豆包MarsCode AI刷题

130 阅读7分钟

今天详解一下AI刷题中的一道简单题《最佳人选》

本文将给一个非常简单易懂的解题方法,先看一下题目描述:

问题描述

某特种部队采用了一套性格密码机制来筛选执行特定任务的最佳士兵,该机制的规则如下:

  1. 每个人的性格可以通过 M 个维度来描述,每个维度分为 A, B, C, D, E 五种类型。
  2. 同一维度内,字母距离越近,性格类型差异越小,匹配程度越高。比如,A 和 B 的差异为 1,A 和 D 的差异为 3
  3. 其中 AEBDCEBE 为不相容性格类型,差异值设为无穷大(无法匹配)。
  4. 如果某一维度存在不相容性格类型,则表示两个士兵性格完全不匹配。
  5. 对于符合匹配条件的士兵,差异值总和越小表示匹配程度越高。

现在,有一个重要的机密任务,要求找到最匹配该任务所需性格密码的士兵。你需要编写一个算法,帮助部队找到符合条件的最佳人选。

  • m 表示性格密码的维度。
  • n 表示备选特种兵的数量。
  • target 是代表任务的性格密码。
  • array 是一个包含 n 个元素的列表,每个元素为 M 位的性格密码。

测试样例

样例1:

输入:m = 6, n = 3, target = "ABCDEA", array = ["AAAAAA", "BBBBBB", "ABDDEB"]
输出:'ABDDEB'

样例2:

输入:m = 5, n = 4, target = "ABCED", array = ["ABCDE", "BCDEA", "ABDCE", "EDCBA"]
输出:'ABCDE'

样例3:

输入:m = 4, n = 4, target = "AEBC", array = ["ACDC", "BBDC", "EBCB", "BBBB"]
输出:'None'

思路解析

个人认为这道题还是比较麻烦的一道题,因为一遇上描述比较长的题目,就不得不发动阅读理解能力来提取关键信息了。本题说白了就是给一个字符串数组array,和一个字符串target,所有字符串只有 A,B,C,D,E 五种字符,每次在array中取出一个字符串和target按位进行对比,如果当前位的字符符合AE,BD,CE,BE其中一种,则当前士兵不符合直接pass,否则,每一字符取差值,最后加在一起得到一个“匹配值”,在array中所有士兵中选择最匹配的,也就是“匹配值最小”,即为答案。思路如下图:

image.png 由图可知,我们要做的其实就是比较字符串的每一位字符即可,所以很容易就会想到使用charArray来循环。接下来我们看看代码以及代码中的一些注意点。

代码解析

由于我们要比较字符,所以直接将target转为字符数组chars。再定义一个count来保存性格不相容的人数,因为我们如果没有一个合适人选,所有士兵都遍历了一遍但是所有士兵都不合适,即 count==n 就必须返回“None”。 然后接下来使用一个map来保存每个士兵对应的“匹配值”。

char[] chars = target.toCharArray();
int count = 0;//性格不相容人数

Map<String, Integer> counts = new HashMap<>();
HashMap<String, Integer> map = new LinkedHashMap<>();

这里使用LinkedHashMap的作用是因为普通的hashmap在插入新的键值对时会自动排序,比如先插入键为“B”,再插入键为“A”,最后取出时会打乱顺序,由于我们需要返回String类型,所以不能打乱顺序,否则就会出现这样的情况:

image.png

“AB”被插入到“BA”前面,导致答案错误。

// 外循环遍历特种兵
for (int i = 0; i < n; i++) {

    map.put(array[i], 0);
    counts.merge(array[i], 1, Integer::sum);

    char[] strs = array[i].toCharArray();
    // 内循环对具体士兵和target循环
    for (int j = 0; j < m; j++) {
        // 如果检测到不相容性格进行下一次循环
        if (!isFit(chars[j], strs[j])) {
            count++; // 性格不相容人数加1
            map.remove(array[i]);
            break;
        } else {
            // 更新匹配值
            map.put(array[i], map.get(array[i]) + Math.abs(chars[j] - strs[j]));
        }
    }
}

外层循环遍历array中的每个士兵,在进入内层循环之前我们将当前士兵加入map中,并初始化“匹配值”为0。可能有同学注意到我前面还定义了一个counts,这个的作用是什么呢?由于测试用例中,我们可能会碰到不同士兵的性格密码是相同的,比如array={"A","A","B","C","B"}的情况,所以counts用来记录性格密码相同的士兵的个数,否则,HashMap没办法保存相同键但是不同值的数据,会导致我们最后拼接结果的出现问题。 内层循环遍历当前士兵的每一位与target进行对比.isFit()代码如下:

public static boolean isFit(char a, char b) {
    switch (a) {
        case 'A':
            if (b == 'E') return false;
            break;
        case 'B':
            if (b == 'D' || b == 'E') return false;
            break;
        case 'C':
            if (b == 'E') return false;
            break;
        case 'D':
            if (b == 'B') return false;
            break;
        case 'E':
            if (b == 'A' || b == 'C' || b == 'B') return false;
            break;
    }
    return true;
}

继续看内层循环,if检测到不相容性格就进行下一位士兵的循环,并且性格不相容人数count+1,再把当前士兵从map中去除。else则给当前士兵的“匹配值”加上字符差的绝对值。

循环完毕,我们要开始拼装结果了。

// 没有合适人选
        if (count == n) return "None";

        int res = Integer.MAX_VALUE; // 保存最佳匹配值
        StringBuilder str = new StringBuilder(); // 保存最佳特种兵

        // 找出最佳匹配值
        for (Integer val : map.values()) {
            if (val < res) {
                res = val;
            }
        }

        // 找出所有的最佳特种兵
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            // 如果当前key为最佳特种兵拼入res
            String key = entry.getKey();
            if (entry.getValue().equals(res)) {
                if (counts.containsKey(key)) {
                    while (counts.get(key) > 0) {
                        str.append(key + " ");
                        counts.put(key, counts.get(key) - 1);
                    }
                } else {
                    str.append(key + " ");
                }
            }
        }
        // 删除空格
        str.deleteCharAt(str.length() - 1);
        return str.toString();

我们先遍历一下map.values(),获取最佳匹配值,然后再遍历map找出最佳人选,但是要注意,此时我们的counts就体现出作用了,如果counts中存在最佳人选,意味着最佳人选有不止一位并且性格密码相同,此时我们就要通过while循环取出所有人,即:

if (counts.containsKey(key)) {
    while (counts.get(key) > 0) {
        str.append(key + " ");
        counts.put(key, counts.get(key) - 1);
    }
} else {
    str.append(key + " ");
}

否则直接拼接即可。最后删除空格即可返回答案。

// 删除空格
str.deleteCharAt(str.length() - 1);
return str.toString();

总结以及知识点

本题逻辑上不难理解,但是最麻烦的是需要我们记录每一个符合条件的字符串,所以需要我们对Map的使用非常熟练才可以。例如HashMapmergegetput等方法,以及Map这个数据结构本身的特点,还有mapEntry遍历。本人最开始在写题的时候,就忽略了不同士兵的性格密码可能相等 和最后拼接答案的顺序错误的问题。这个题的细节注意点很多,所以选择写成文章分享给大家最后带大家复习一下Map

在Java中,Map 是一个非常重要的集合接口,它用于存储键值对(key-value pairs)。每个键唯一地映射到一个值,键和值都可以是对象,键不能重复,而值可以重复。常用的 Map 接口的实现类包括:

  1. HashMap:

    • 最常用的实现,基于哈希表。
    • 存储和检索速度非常快,平均时间复杂度为 O(1)。
    • 不保证元素的顺序,顺序是不固定的。
  2. LinkedHashMap:

    • 继承自 HashMap,保持插入的顺序。
    • 在迭代时,按照插入的顺序返回元素。
    • 较 HashMap 的性能稍低,但提供了顺序功能。
  3. TreeMap:

    • 基于红黑树实现。
    • 存储的键是有序的,按照自然顺序或构造时提供的比较器排序。
    • 时间复杂度为 O(log n) 来查找、插入和删除元素。
  4. Hashtable:

    • HashMap 类似,但它是同步的,线程安全。
    • 不推荐使用,通常建议使用 ConcurrentHashMap 作为替代。

常用方法

  • put(K key, V value): 将指定的键值对添加到 map 中。
  • get(Object key): 根据键获取对应的值。
  • remove(Object key): 删除指定键的键值对。
  • containsKey(Object key): 检查 map 中是否包含指定的键。
  • keySet(): 返回 map 中所有键的集合。
  • values(): 返回 map 中所有值的集合。
  • entrySet(): 返回 map 中所有键值对的集合。