华为11.4机考三题整理:装包裹、统计登录、判断回文

1 阅读9分钟

恭喜发现宝藏!微信搜索公众号【TechGuide】,回复公司名获取笔经面经!

这次整理了华为 11 月 4 日机考的三道编程题,分别讲讲怎么想、怎么写。更多更全的笔试面试真题可以去真题过招练习,题库刚改版,也欢迎批评指正~

三题分别是快递柜装载、连续登录统计和回文串判断。第一题要留意第二个优化条件,后两题把统计口径和输入输出看清楚,就比较好下手了。

第一题:快递柜最优装载方案

题目描述

小华要往一个快递柜里装包裹。快递柜总容量为 capacity,每个包裹有一个正整数体积。每个包裹只能完整装入一次,不能拆开,所选包裹总体积不能超过容量。 先让装进去的包裹数量尽可能多;如果数量相同,再选择占用空间更大的方案。

输入描述

第一行输入正整数 capacity,1 ≤ capacity ≤ 10000。 第二行输入 n 个正整数,表示各包裹体积,1 ≤ n ≤ 1000。n 由第二行的数字个数确定,不单独输入。

输出描述

返回三个整数组成的数组:[装载数量, 已用空间, 剩余容量]。结果使用方括号展示,三个数字用空格分隔。

样例

输入:

100
20 20 50 40 5

输出:

[4 95 5]

选择体积为 20、20、50、5 的四个包裹,总体积 95,剩余容量 5。五个包裹总体积 135,无法全部装下。

思路

这题有两个要求,先比数量,再比体积。比如样例按体积从小到大装,能装下 5、20、20、40,共四个,占用 85;换成 5、20、20、50,仍然是四个,占用却能提高到 95。

所以只优先装小包裹,只完成了前半个要求。容量上限是 10000,可以用一维背包,把每一种可达到的总空间都算出来。

每个空间,记下最多能装几个

用 dp[s] 表示“恰好占用 s 空间,最多可以装几个包裹”。初始 dp[0] = 0,其他位置设为 -1,表示暂时凑不出来。

遇到一个体积为 v 的包裹,如果 s-v 原来能凑出来,再装这个包裹就能达到 s,包裹数量加一。将这个数量与 dp[s] 比较,留下较大的那个。

为什么空间要从大往小遍历

每个包裹只有一个。倒着更新时,用到的 s-v 还保留着处理当前包裹之前的结果,当前包裹在这一轮只会算一次。如果正着更新,刚放进去的包裹可能继续被拿来凑后面的空间,相当于同一件被反复使用。

最后按两个条件挑答案

遍历 0 到 capacity 的所有空间,先选 dp[s] 最大的;数量相等时选 s 更大的。这样就同时满足题目的两个要求。剩余容量用 capacity-s 计算。

体积大于柜子容量的包裹直接跳过。所有包裹都装不下时,答案是 [0 0 capacity]。

时间复杂度 O(n × capacity),额外空间 O(capacity)。按题面上限,最多约一千万次状态检查。

代码

Java版本

// vx公众号关注TechGuide 实时题库 闪电速递
import java.io.*;
import java.util.*;

public class Main {
    static int[] solveTechGuide(int capacity, int[] packages) {
        // dp[s]:恰好占用 s 空间时,最多能装多少个包裹。
        int[] dp = new int[capacity + 1];
        Arrays.fill(dp, -1);
        dp[0] = 0;

        for (int volume : packages) {
            if (volume > capacity) continue;
            for (int s = capacity; s >= volume; s--) {
                if (dp[s - volume] >= 0) {
                    dp[s] = Math.max(dp[s], dp[s - volume] + 1);
                }
            }
        }

        int count = 0, used = 0;
        for (int s = 0; s <= capacity; s++) {
            if (dp[s] > count || (dp[s] == count && s > used)) {
                count = dp[s];
                used = s;
            }
        }
        return new int[]{count, used, capacity - used};
    }

    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        int capacity = Integer.parseInt(in.readLine().trim());
        StringTokenizer tokens = new StringTokenizer(in.readLine());
        int[] packages = new int[tokens.countTokens()];
        for (int i = 0; i < packages.length; i++) {
            packages[i] = Integer.parseInt(tokens.nextToken());
        }
        int[] answer = solveTechGuide(capacity, packages);
        System.out.println("[" + answer[0] + " " + answer[1] + " " + answer[2] + "]");
    }
}

第二题:计算用户连续登录天数

题目描述

小华要根据操作日志判断哪些用户是活跃用户。每条日志包含用户名和操作日期,一条日志记作一次操作;日期使用距离 2025 年 1 月 1 日的天数表示,例如 2025 年 1 月 5 日记作 4。 如果一个用户存在连续至少 N 天,每天操作次数都不少于 K,就判为活跃用户。

输入描述

第一行输入日志数量 M。 接下来 M 行,每行输入用户名和整数日期,中间用空格分隔。 倒数第二行输入每天操作次数下限 K,K > 0。 最后一行输入连续天数下限 N,N > 0。 日志可能不按日期排序,同一用户同一天可以出现多条日志。

输出描述

按用户在日志中第一次出现的顺序,每个用户输出一行:用户名和判定结果,用空格分隔。活跃输出 1,否则输出 0。

样例

输入:

2
user1 3
user2 4
2
1

输出:

user1 0
user2 0

要求连续至少 1 天、每天至少操作 2 次。两个用户都只有一天的一次操作,因此都不达标。

整理样例

输入:

7
user2 5
user1 4
user2 4
user2 5
user1 3
user2 4
user1 3
2
2

输出:

user2 1
user1 0

user2 在第 4、5 天各操作 2 次,满足要求;user1 第 4 天只有 1 次,无法形成连续两天达标。输出顺序按 user2、user1 首次出现的顺序。

思路

这里要数的是“连续多少天达标”,先把同一个用户同一天的操作合并,后面就很好处理了。

先统计每个人每天操作了几次

用一个表按用户名分组,每个用户下面再放一张“日期 → 次数”的表。读到一条日志,就把这个用户当天的次数加一。

外层使用 LinkedHashMap,它会保留用户名首次插入的顺序。最后遍历它,输出顺序就与题目一致,不需要另外排序用户名。

只留下当天次数足够的日期

比如某个用户的记录是:第 3 天 2 次、第 4 天 1 次、第 5 天 3 次、第 6 天 2 次。假设 K=2,留下的日期就是 3、5、6。

把留下的日期从小到大排序,再看相邻两天是否相差 1。3 到 5 断了一天,连续天数重新从 1 开始;5 到 6 相邻,连续天数变成 2。

当天操作不足 K,或者那天完全没有日志,都会让连续记录断开。这里通过日期差就能看出来,不必把所有空白日期逐个补齐。

连续天数够了,就记录达标

连续天数达到 N,这个用户的结果就是 1;扫描结束仍没达到则是 0。同一天的多条日志在第一步已经合并,不会被当成多天。

设日志总数为 M,每个用户的不同日期数为 Dᵤ。平均统计耗时 O(M),排序总耗时 O(Σ Dᵤ log Dᵤ),整体最坏 O(M log M),空间 O(M)。

代码

Java版本

// vx公众号关注TechGuide 实时题库 闪电速递
import java.io.*;
import java.util.*;

public class Main {
    static void solveTechGuide() throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        int m = Integer.parseInt(in.readLine().trim());
        // 外层保留用户首次出现顺序,内层统计每天的操作次数。
        Map<String, Map<Long, Integer>> users = new LinkedHashMap<>();
        for (int i = 0; i < m; i++) {
            StringTokenizer row = new StringTokenizer(in.readLine());
            String user = row.nextToken();
            long day = Long.parseLong(row.nextToken());
            Map<Long, Integer> counts = users.computeIfAbsent(user, x -> new HashMap<>());
            counts.put(day, counts.getOrDefault(day, 0) + 1);
        }
        int minOperations = Integer.parseInt(in.readLine().trim());
        long minDays = Long.parseLong(in.readLine().trim());
        StringBuilder out = new StringBuilder();

        for (Map.Entry<String, Map<Long, Integer>> entry : users.entrySet()) {
            List<Long> days = new ArrayList<>();
            for (Map.Entry<Long, Integer> day : entry.getValue().entrySet()) {
                if (day.getValue() >= minOperations) days.add(day.getKey());
            }
            Collections.sort(days);

            long streak = 0, previous = 0;
            boolean active = false;
            for (long day : days) {
                streak = streak > 0 && day - previous == 1 ? streak + 1 : 1;
                previous = day;
                if (streak >= minDays) {
                    active = true;
                    break;
                }
            }
            out.append(entry.getKey()).append(' ')
               .append(active ? '1' : '0').append('\n');
        }
        System.out.print(out);
    }

    public static void main(String[] args) throws IOException {
        solveTechGuide();
    }
}

第三题:基础回文串判断

题目描述

小华要判断一个字符串是不是回文串。回文串从左向右读、从右向左读,字符顺序完全相同,例如 level、noon、1221。 请编写函数接收字符串并判断,同时给出可独立运行的输入输出程序。

输入描述

输入一行字符串,仅包含小写字母和阿拉伯数字字符。

输出描述

如果是回文串,输出 True,否则输出 False。首字母大写。

样例 1

输入:

level

输出:

True

样例 2

输入:

hello

输出:

False

思路

把字符串两头的字符放在一起比:第一个和最后一个比,第二个和倒数第二个比,一直往中间走。

用 left 指向开头,right 指向结尾。两个字符相同,就让 left 加一、right 减一;一旦遇到不同字符,直接返回 false。比如 hello 的 h 和 o 第一轮就对不上,后面的字符可以省下不看。

当两个指针相遇,或者已经交错,说明前面比较的字符全部一致,返回 true。像 level 这种奇数长度的字符串,中间那个字符与自己对应,留着不比较也没影响。

题目只给小写字母和数字,按原字符直接比较即可。只需要两个下标,也不必额外生成倒序字符串。

设字符串长度为 L,最坏时间复杂度 O(L),判断函数额外空间 O(1);读入字符串本身需要 O(L) 空间。Java 的 boolean 默认打印为小写,这里用条件表达式输出题目要求的 True、False。

代码

Java版本

// vx公众号关注TechGuide 实时题库 闪电速递
import java.io.*;

public class Main {
    static boolean isPalindromeTechGuide(String s) {
        int left = 0, right = s.length() - 1;
        while (left < right) {
            if (s.charAt(left) != s.charAt(right)) return false;
            left++;
            right--;
        }
        return true;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String s = in.readLine();
        if (s == null) return;
        System.out.println(isPalindromeTechGuide(s) ? "True" : "False");
    }
}

这三道题里,第一题多想一步:装的件数已经最多了,还要继续比较空间利用率。后两题则适合练输入处理,特别是同一天多条日志和输出大小写这些小细节。