大厂真题 / 华为

华为 9.4 笔试真题 - 研发岗

本场考试概述

考试时间:2026 年 9 月 4 日

考试岗位:研发岗

题型构成:3 道编程题。


第 1 题:麻将胡牌规则

题目描述

给定当前的 13 张麻将牌,判断再摸哪一张牌可以胡牌。

牌只有三门,每门各有 1~9 共 9 种牌面:

  • 万:19
  • 筒:ai
  • 条:AI

同一种牌面最多有 4 张。摸牌后共有 14 张牌,胡牌必须同时满足:

  1. 14 张牌能拆成 4 组面子和 1 对将牌;
  2. 每组面子是同一门中的顺子(连续三个点数)或刻子(三张相同牌);
  3. 将牌是两张相同牌;
  4. 必须“缺一门”,即 14 张牌中至多出现两门花色。

枚举所有实际还能摸到且摸入后能胡的牌面,按 ASCII 码升序输出。

输入描述

输入一行长度为 13 的字符串,表示当前手牌。字符均为上述 27 种合法牌面,顺序任意;任一种牌面出现不超过 4 次。

输出描述

若存在可胡的摸牌,将所有这样的牌面按 ASCII 码升序拼成一个字符串输出;否则输出 -1

约束

  • 手牌张数固定为 13;
  • 候选牌面共 27 种;
  • 每种牌面最多 4 张;
  • 牌面 ASCII 顺序为 19AIai

样例 1

输入

111222333abcA

输出

-1

说明:当前三门牌都已出现,再摸任何牌都不能满足缺一门。

样例 2

输入

1234567abcdef

输出

147

说明:摸入 147 都能将万拆成两组面子和一对将,筒拆成 abcdef 两组顺子。

样例 3

输入

ABCABCABCABC1

输出

1

说明:摸入 1 后可以胡牌;ABC 在手中均已有 4 张,不能继续摸入。

算法

先把 27 种牌压成计数数组。按 ASCII 顺序枚举摸入的牌;若手中已有 4 张则跳过。摸入后先统计非空花色数,超过 2 直接判失败。

随后枚举 27 种牌面作为将牌。取走两张将牌后,三个花色互不影响,只需分别判断每门剩余牌能否全部拆成面子。

判断单个花色时,找到点数最小的剩余牌。它不可能作为某个更小点数顺子的中间或末尾,因为更小的牌已经不存在,所以它只有两种可能:

  • 与另外两张同牌组成刻子;
  • 与后面连续两个点数组成顺子。

递归尝试这两种选择,并用该花色的 9 元计数元组记忆化。只要存在一种拆法即可。

正确性证明

引理 1:单花色递归返回真,当且仅当该花色的全部牌能拆成若干面子。

取当前点数最小的牌。任何合法拆分中,包含它的面子只能是同牌刻子,或者以它为首张的顺子;递归恰好枚举这两种可能。取走一个面子后,对剩余牌应用同样结论。空集返回真,因此由剩余牌数归纳,递归既不遗漏合法拆分,也不会接受非法拆分。

引理 2:固定将牌后,算法判定成功,当且仅当余下 12 张牌能组成 4 组面子。

面子不能跨花色,所以余牌能整体拆分,当且仅当每个花色都能独立拆完。由引理 1,算法对三个花色的判断恰好等价于这一条件。

定理:算法输出的字符恰好是所有可摸入并胡牌的牌面,且顺序正确。

算法跳过已有 4 张的牌,故只枚举实际可摸的牌。对每个候选,先严格检查缺一门,再穷举所有可能的将牌;由引理 2,接受当且仅当余牌可组成 4 个面子。因此每个输出候选都能胡,所有能胡候选也一定被找到。候选按 ASCII 顺序枚举,所以输出顺序符合要求。

复杂度分析

时间复杂度:设 S 为单花色计数状态数。外层至多枚举 27 张摸牌和 27 种将牌,每次检查三个花色,复杂度为 O(27 × 27 × 3 × S);由于每门只有 9 种牌且每种至多 4 张,S 是很小的常数。

空间复杂度O(S),用于记忆化状态与递归栈。

Python ACM 题解

import sys
from functools import lru_cache


ASCII_TILES = "123456789ABCDEFGHIabcdefghi"


def tile_index(ch):
    if "1" <= ch <= "9":
        return ord(ch) - ord("1")
    if "a" <= ch <= "i":
        return 9 + ord(ch) - ord("a")
    return 18 + ord(ch) - ord("A")


@lru_cache(maxsize=None)
def can_form_melds(state):
    total = sum(state)
    if total == 0:
        return True
    if total % 3 != 0:
        return False

    first = next(i for i, count in enumerate(state) if count)
    counts = list(state)

    if counts[first] >= 3:
        counts[first] -= 3
        if can_form_melds(tuple(counts)):
            return True
        counts[first] += 3

    if first + 2 < 9 and counts[first + 1] and counts[first + 2]:
        counts[first] -= 1
        counts[first + 1] -= 1
        counts[first + 2] -= 1
        if can_form_melds(tuple(counts)):
            return True

    return False


def is_winning(counts):
    used_suits = sum(
        any(counts[start:start + 9]) for start in (0, 9, 18)
    )
    if used_suits > 2:
        return False

    for pair in range(27):
        if counts[pair] < 2:
            continue
        counts[pair] -= 2
        valid = all(
            can_form_melds(tuple(counts[start:start + 9]))
            for start in (0, 9, 18)
        )
        counts[pair] += 2
        if valid:
            return True
    return False


def solve():
    hand = sys.stdin.buffer.readline().strip().decode()
    counts = [0] * 27
    for ch in hand:
        counts[tile_index(ch)] += 1

    answer = []
    for ch in ASCII_TILES:
        index = tile_index(ch)
        if counts[index] == 4:
            continue
        counts[index] += 1
        if is_winning(counts):
            answer.append(ch)
        counts[index] -= 1

    print("".join(answer) if answer else -1)


if __name__ == "__main__":
    solve()

注意事项

  1. 万、筒、条的内部点数分别连续,但不同花色之间绝不能组成顺子。
  2. 必须先排除手中已有 4 张的摸牌候选。
  3. “缺一门”允许只出现一门,也允许恰好出现两门,但不允许三门齐全。
  4. 输出按字符的 ASCII 顺序,不是按代码内部的花色存储顺序。
  5. 同一副牌可能有多种拆法,因此不能使用只尝试一种组合的贪心;递归必须覆盖刻子和顺子两种分支。

第 2 题:软件包版本依赖安装

题目描述

系统中有 m 个软件包,每个包有唯一编号、已确定的三段式版本号,并可能依赖若干其他包。依赖有五种形式:

  • x>=a.b.c:包 x 的实际版本不低于 a.b.c
  • x<=a.b.c:包 x 的实际版本不高于 a.b.c
  • x>a.b.c:包 x 的实际版本严格高于 a.b.c
  • x<a.b.c:包 x 的实际版本严格低于 a.b.c
  • x:依赖包 x,但不限制版本。

被依赖的软件包必须先安装。需要先检查所有版本约束,再判断依赖图是否有环;若均无问题,输出编号序最小的安装顺序。

输入描述

第一行输入整数 m,表示软件包数量。

接下来 m 行,每行格式为:

软件包编号 版本号 依赖1 依赖2 ...

依赖列表可以为空。各行出现顺序不保证按编号排列;同一行依赖的软件包编号互不重复。软件包编号为 0m-1 且各不相同,版本号格式固定为 a.b.c

输出描述

  • 若任意版本约束不满足,输出 -1
  • 否则若存在循环依赖(包括自己依赖自己),输出 -2
  • 否则输出一种合法安装顺序,编号间以一个空格分隔;存在多种顺序时,输出字典序最小的一种。

若版本冲突和循环依赖同时存在,版本冲突优先,输出 -1

约束

  • 1 ≤ m ≤ 100
  • 版本号每一段均满足 0 ≤ a,b,c ≤ 999
  • 依赖仅使用 >=<=>< 或无版本限制这五种形式;
  • 同一软件包的依赖列表中,目标包编号不重复。

样例 1

输入

2
0 1.0.0 1>3.0.0
1 3.0.0

输出

-1

说明:包 1 的实际版本等于 3.0.0,不满足严格大于 3.0.0

样例 2

输入

2
0 1.0.0 1
1 1.0.0 0

输出

-2

说明:包 0 与包 1 互相依赖,形成环。

样例 3

输入

3
2 1.0.0 0 1
0 1.0.0
1 1.0.0

输出

0 1 2

说明:包 0、1 都要先于包 2;两者当前均可安装时优先选择编号较小的 0。

算法

把版本号解析成整数三元组,Python 元组比较会依次比较主版本、次版本和修订版本,正好符合版本规则。依赖 token 用正则表达式一次拆出目标编号、可选比较符与可选版本,避免把 >= 错拆成 >

读完全部软件包后,先逐条校验被依赖包的实际版本。只要有一条不满足就立即输出 -1,从而保证版本冲突的优先级高于环。

若版本全部满足,则对依赖关系建图:若包 u 依赖包 v,添加有向边 v -> u,并增加 u 的入度。使用 Kahn 拓扑排序,将所有入度为 0 的包放入小根堆。每次取出当前最小编号,安装后删除它的出边,并把新产生的零入度包加入堆。

若最终取出的包不足 m 个,图中有环,输出 -2;否则所得序列就是答案。

正确性证明

引理 1:版本校验阶段输出 -1,当且仅当存在不满足的版本约束。

每条带比较符的依赖都按照目标包实际版本与要求版本的三元组大小关系进行对应的 >=<=>< 判断;无比较符的依赖恒满足版本要求。算法检查所有依赖,所以结论成立。

引理 2:拓扑阶段取出的序列始终是合法安装顺序的前缀。

只有入度为 0 的包才会进入堆,此时它的所有依赖都已经被取出并安装。因此每次追加的包都满足安装前置条件,归纳可知整个已取出序列始终合法。

引理 3:若依赖图无环,算法得到字典序最小的拓扑序。

在任意一步,所有能接在当前前缀后的包恰好是当前入度为 0 的包。小根堆选择其中编号最小者。若另一合法序列在第一个不同位置选择了更大的编号,把算法选择的较小编号放到该位置仍不会违反依赖关系,并会得到更小序列。因此逐步选择最小可用编号得到全局字典序最小序列。

定理:算法按题意输出正确结果。

由引理 1,所有版本冲突都会且只会输出 -1,并且该检查先于判环。无版本冲突时,Kahn 算法取不满 m 个顶点当且仅当图中存在环,此时输出 -2;否则由引理 2、3,输出的是合法且字典序最小的安装顺序。

复杂度分析

时间复杂度:设依赖总数为 E。解析与版本检查为 O(m+E);拓扑排序中每个包至多进出堆一次,每条边处理一次,总复杂度为 O((m+E) log m)

空间复杂度O(m+E),用于邻接表、依赖表、版本表和入度数组。

Python ACM 题解

import heapq
import re
import sys


DEPENDENCY = re.compile(r"^(\d+)(?:(>=|<=|>|<)(\d+)\.(\d+)\.(\d+))?$")


def parse_version(text):
    return tuple(map(int, text.split(".")))


def version_satisfies(actual, operator, required):
    if operator == ">=":
        return actual >= required
    if operator == "<=":
        return actual <= required
    if operator == ">":
        return actual > required
    if operator == "<":
        return actual < required
    return True


def solve():
    input = sys.stdin.buffer.readline
    m = int(input())
    versions = [None] * m
    dependencies = [[] for _ in range(m)]

    for _ in range(m):
        parts = input().split()
        package = int(parts[0])
        versions[package] = parse_version(parts[1].decode())
        for raw in parts[2:]:
            match = DEPENDENCY.fullmatch(raw.decode())
            target = int(match.group(1))
            operator = match.group(2) or ""
            required = None
            if operator:
                required = tuple(int(match.group(i)) for i in (3, 4, 5))
            dependencies[package].append((target, operator, required))

    for package in range(m):
        for target, operator, required in dependencies[package]:
            if not version_satisfies(versions[target], operator, required):
                print(-1)
                return

    graph = [[] for _ in range(m)]
    indegree = [0] * m
    for package in range(m):
        for target, _, _ in dependencies[package]:
            graph[target].append(package)
            indegree[package] += 1

    ready = [package for package in range(m) if indegree[package] == 0]
    heapq.heapify(ready)
    order = []

    while ready:
        package = heapq.heappop(ready)
        order.append(package)
        for dependent in graph[package]:
            indegree[dependent] -= 1
            if indegree[dependent] == 0:
                heapq.heappush(ready, dependent)

    if len(order) != m:
        print(-2)
    else:
        print(*order)


if __name__ == "__main__":
    solve()

注意事项

  1. 三段版本必须按整数比较;字符串比较会错误地认为 10.0.0 < 9.0.0
  2. >=<= 是完整的双字符操作符,不能先按单字符 >< 切分。
  3. 输入行顺序不等于软件包编号顺序,必须按行首编号存储。
  4. u 依赖 v 时拓扑边方向是 v -> u,不能建反。
  5. 必须先检查全部版本,再判断环;两个问题同时存在时输出 -1
  6. 普通队列只能得到某个拓扑序;要得到字典序最小序列,必须用小根堆维护当前所有零入度节点。

第 3 题:双机器人同步盘点

题目描述

给定一个 h × w 的储物矩阵,每个格子有一个非负货物价值。两个机器人同时出发并同步移动:

  • 机器人 A 从左上角 (0,0) 前往右下角 (h-1,w-1),每步只能向下或向右;
  • 机器人 B 从右上角 (0,w-1) 前往左下角 (h-1,0),每步只能向下或向左。

两台机器人每个时刻都各自位于一个格子,所在格子的价值计入总和。任意同一时刻,两台机器人不能位于同一个格子;但它们可以在不同时刻经过同一个格子,此时两次经过应分别计值。求两条同步路径能获得的最大价值总和。

坐标仅用于说明,输入中不提供坐标。

输入描述

第一行输入两个整数 h,w,表示矩阵行数和列数。

接下来 h 行,每行输入 w 个整数,第 i 行第 j 个数表示格子 (i,j) 的货物价值。

输出描述

输出一个整数,表示两台机器人盘点价值之和的最大值。

约束

  • 3 ≤ h,w ≤ 100
  • 每个格子的价值满足 0 ≤ value ≤ 100000

样例 1

输入

3 3
2 1 4
3 0 5
6 7 8

输出

50

说明:最优方案允许两台机器人在不同时间经过同一格;每次到达都应计入对应机器人的收益。

样例 2

输入

3 4
1 0 0 2
3 4 5 6
7 8 9 1

输出

66

样例 3

输入

4 4
1 2 3 4
5 0 9 1
2 8 0 3
4 5 6 7

输出

67

算法

两条路径都恰好移动 h+w-2 步。设已经同步移动 k 步:

  • 若 A 在第 a 行,则它一定在第 k-a 列;
  • 若 B 在第 b 行,则它一定在第 w-1-(k-b) 列。

因此同一层中只需记录两台机器人的行号。令 dp[a][b] 表示移动 k 步后,A 位于第 a 行、B 位于第 b 行时,截止当前时刻的最大累计价值。

初始时 dp[0][0] = value[0][0] + value[0][w-1]。从第 k-1 层转到第 k 层时,每台机器人本步可能横向移动(行号不变)或向下移动(行号加一),所以一个新状态最多有四个前驱:(a,b)(a-1,b)(a,b-1)(a-1,b-1)

算出两台机器人当前列号后,若 (a, colA) == (b, colB),该状态表示同一时刻相撞,必须丢弃;否则在最佳前驱上加两个当前格子的价值。即使两个位置对应同一个格子在别的层曾被另一台机器人走过,也不去重,因为那属于不同时刻的两次盘点,题意要求重复计值。

逐层滚动数组。最后两台机器人行号都为 h-1,答案是 dp[h-1][h-1]

正确性证明

引理 1:在同步移动步数 k 和机器人行号确定后,两台机器人的列号唯一确定。

A 的向下步数为 a,故向右步数为 k-a,列号为 k-a。B 的向下步数为 b,故向左步数为 k-b,列号为 w-1-(k-b)。因此状态没有丢失位置信息。

引理 2:第 k 层的转移枚举了到达每个状态的全部合法上一步状态。

每台机器人本步只有横向或向下两种选择。反推其上一时刻行号,分别只能是当前行或当前行减一;两台机器人的选择组合恰好形成转移检查的四个前驱,没有遗漏,也没有额外移动方式。

引理 3:每层 dp[a][b] 等于到达对应位置且此前从未同刻重合的所有同步路径对中的最大累计价值。

对步数归纳。第 0 层准确计入两个不同起点。假设上一层结论成立,算法由引理 2 取所有前驱的最大值,并加上本时刻两个落脚格的价值;若本时刻位置相同则直接丢弃。因此保留的路径对在所有时刻都不重合,且最优值没有遗漏。历史上不同时刻访问同一格不会触发丢弃,每次访问都在对应层加值,完全符合计分规则。归纳成立。

定理:算法输出两台机器人合法同步行走可获得的最大总价值。

移动 h+w-2 步后,两台机器人必分别到达各自终点,此时行号均为 h-1。由引理 3,最终状态保存所有合法完整路径对的最大累计价值,故输出正确。

复杂度分析

时间复杂度:共 h+w-2 次转移,每层至多枚举 个行号对,每个状态检查 4 个前驱,因此为 O((h+w)h²)

空间复杂度O(h²),使用两层滚动状态。在 h,w ≤ 100 时最大答案不超过 2(h+w-1)×100000,Python 整数可直接容纳。

Python ACM 题解

import sys


def solve():
    data = list(map(int, sys.stdin.buffer.read().split()))
    h, w = data[0], data[1]
    values = [
        data[2 + row * w:2 + (row + 1) * w]
        for row in range(h)
    ]

    negative_infinity = -10**30
    dp = [[negative_infinity] * h for _ in range(h)]
    dp[0][0] = values[0][0] + values[0][w - 1]

    for step in range(1, h + w - 1):
        next_dp = [[negative_infinity] * h for _ in range(h)]
        low_row = max(0, step - (w - 1))
        high_row = min(h - 1, step)

        for row_a in range(low_row, high_row + 1):
            col_a = step - row_a
            for row_b in range(low_row, high_row + 1):
                col_b = w - 1 - (step - row_b)

                if row_a == row_b and col_a == col_b:
                    continue

                best_previous = negative_infinity
                for down_a in (0, 1):
                    previous_a = row_a - down_a
                    if previous_a < 0:
                        continue
                    for down_b in (0, 1):
                        previous_b = row_b - down_b
                        if previous_b >= 0:
                            best_previous = max(
                                best_previous,
                                dp[previous_a][previous_b],
                            )

                if best_previous != negative_infinity:
                    next_dp[row_a][row_b] = (
                        best_previous
                        + values[row_a][col_a]
                        + values[row_b][col_b]
                    )

        dp = next_dp

    print(dp[h - 1][h - 1])


if __name__ == "__main__":
    solve()

注意事项

  1. 只禁止两台机器人在同一时刻位于同一格;不同时刻访问同一格合法,而且每次访问都要计值,不能使用全局“访问过”集合去重。
  2. 两台机器人若在一步中交换相邻位置但落脚点不同,题目只禁止同刻站在同一货架,因此不应额外禁止这种边上交叉。
  3. 不可达状态必须初始化为足够小的负数,不能初始化为 0,否则会产生没有合法前驱的虚假路径。
  4. 每层合法行号范围是 max(0, k-w+1)min(h-1, k);限制范围既避免列越界,也减少无效枚举。
  5. 起点和终点都要计值;由于 w ≥ 3,两台机器人的起点不同、终点也不同。