大厂真题 / 科大讯飞

科大讯飞 2026-8-23 笔试真题 - 算法岗

本场考试概述

考试时间:2026 年 8 月 23 日

考试岗位:算法岗

难度评级:中等

考点分析

  • 第 1 题:计数、独立事件、几何分布期望(中等)
  • 第 2 题:哈希计数、互补数配对(中等)
  • 第 3 题:堆优化 Dijkstra、按行程模拟(中等偏难)

第 1 题:骰子好运的期望轮数

题目描述

有一个 $n$ 面骰子,每个面写有一个数字,且各个面朝上的概率相同。

每轮独立抛两次骰子。只有第一次朝上的数字为 $a$,并且第二次朝上的数字为 $b$,这一轮才算得到好运。不断进行游戏,直到第一次得到好运时停止,求所需轮数的期望。

输入描述

第一行输入三个整数 $n,a,b$,分别表示骰子面数和两个目标数字。

第二行输入 $n$ 个非负整数 $x_1,x_2,\ldots,x_n$,表示各个面上的数字。

题目保证 $a\ne b$,并且 $a,b$ 都至少出现一次。

输出描述

输出期望轮数,保留一位小数。

样例 1

输入

3 8 5
8 5 5

输出

4.5

解释

数字 $8$ 出现在 $1$ 个面,数字 $5$ 出现在 $2$ 个面。一轮成功的概率为

\[\frac{1}{3}\times\frac{2}{3}=\frac{2}{9},\]

因此期望轮数为 $9/2=4.5$。

样例 2

输入

4 0 1
0 0 1 1

输出

4.0

思路分析

设数字 $a$ 和 $b$ 分别出现在 $c_a$、$c_b$ 个面上。两次抛掷相互独立,所以一轮成功的概率为

\[p=\frac{c_a}{n}\cdot\frac{c_b}{n} =\frac{c_ac_b}{n^2}.\]

各轮也独立同分布,首次成功的轮数服从参数为 $p$ 的几何分布。

若其期望为 $E$,考察第一轮:成功时游戏立即结束;失败时已经用掉一轮,之后面对的状态与游戏开始时完全相同。因此也可以写成

\[E=1+(1-p)E.\]

移项可得

\[E=\frac{1}{p}=\frac{n^2}{c_ac_b}.\]

只需扫描骰子各面,统计两个目标数字的出现次数,再代入公式。

正确性证明

引理 1:算法计算的一轮成功概率为真实概率。

证明:第一次抛出 $a$ 的概率为 $c_a/n$,第二次抛出 $b$ 的概率为 $c_b/n$。两次抛掷独立,所以二者同时发生的概率是两者乘积,即 $c_ac_b/n^2$。

引理 2:若每轮成功概率为 $p$,首次成功所需轮数的期望为 $1/p$。

证明:无论第一轮结果如何都先消耗一轮;若第一轮失败,概率为 $1-p$,之后仍需期望 $E$ 轮。故 $E=1+(1-p)E$,解得 $E=1/p$。

定理:算法输出首次得到好运所需轮数的正确期望。

证明:由引理 1,算法得到正确的单轮成功概率;由引理 2,其倒数就是所求期望。代码输出 $n^2/(c_ac_b)$,因此答案正确。

ACM Python 代码

import sys


def solve():
    data = list(map(int, sys.stdin.buffer.read().split()))
    n, a, b = data[:3]
    faces = data[3:3 + n]

    count_a = sum(value == a for value in faces)
    count_b = sum(value == b for value in faces)

    expectation = n * n / (count_a * count_b)
    print(f"{expectation:.1f}")


if __name__ == "__main__":
    solve()

复杂度分析

时间复杂度:$O(n)$。

空间复杂度:$O(n)$(按当前代码一次读入全部输入);若逐个读取并计数,可降为 $O(1)$ 额外空间。

易错点

  • 成功概率必须按目标数字出现的面数计算,不能把两个目标数字直接视为等概率。
  • 结果要求保留一位小数,按期望公式直接计算,避免用随机模拟造成误差。

第 2 题:拆除炸弹的最少按钮次数

题目描述

炸弹控制器上有 $n$ 个按钮,每个按钮写有一个自然数。每个按钮有触发和非触发两种状态,按一次按钮会切换其状态。初始时所有按钮都处于触发状态。

倒计时结束时,如果存在两个不同的、仍处于触发状态的按钮,并且它们的数字之和恰好为 $k$,炸弹就会爆炸。

求至少需要按多少次按钮,才能保证炸弹不爆炸。

输入描述

第一行输入两个正整数 $n,k$。

第二行输入 $n$ 个自然数 $a_1,a_2,\ldots,a_n$,表示各按钮上的数字。

输出描述

输出防止炸弹爆炸所需的最少按键次数。

样例 1

输入

1 8
6

输出

0

样例 2

输入

6 12
4 8 5 7 4 1

输出

2

解释

数值 $4$ 与 $8$ 互补,关闭唯一的 $8$ 代价为 $1$;数值 $5$ 与 $7$ 互补,再关闭其中任意一个代价为 $1$。共按 $2$ 次。

样例 3

输入

4 14
7 7 7 3

输出

2

解释

任意两个数值为 $7$ 的按钮都会凑出 $14$,因此最多保留一个,必须关闭两个。

思路分析

按按钮上的数值分组,记 cnt[x] 为数值 $x$ 的按钮个数。数值 $x$ 只会与 $k-x$ 产生冲突,不同互补数对之间互不影响。

对于一个无序互补组,有两种情况:

  1. $x\ne k-x$:如果两组中都保留按钮,就一定能选出一对使数字和为 $k$。所以必须关闭其中完整的一组,最小代价为

    \[\min(\operatorname{cnt}[x],\operatorname{cnt}[k-x]).\]
  2. $x=k-x$:即 $2x=k$。同组中只要留下至少两个按钮就会爆炸,所以最多保留一个,最小代价为

    \[\operatorname{cnt}[x]-1.\]

若补数 $k-x$ 没有出现,则该组没有冲突,不需要操作。

遍历哈希表时,只在 $x<k-x$ 时处理普通互补对,防止同一个无序对被计算两次;自配对组单独处理。

同一个按钮按两次会回到触发状态,因此最优方案不会对任何按钮按超过一次,问题等价于最少关闭多少个按钮。

正确性证明

引理 1:对于 $x\ne k-x$ 的互补组,最少需要关闭 $\min(\operatorname{cnt}[x],\operatorname{cnt}[k-x])$ 个按钮。

证明:若两侧各留下至少一个按钮,二者之和就是 $k$,仍会爆炸,所以任意可行方案必须关空一侧。关空两侧的代价分别是两侧出现次数,选择较小者可行且最优。

引理 2:对于 $2x=k$ 的自配对组,最少需要关闭 $\operatorname{cnt}[x]-1$ 个按钮。

证明:留下两个或更多数值为 $x$ 的按钮时,可选出两个不同按钮且和为 $k$,所以至多留一个;关闭其余按钮即可消除全部冲突,代价恰为 $\operatorname{cnt}[x]-1$。

引理 3:不同无序互补组可以独立求解。

证明:一个数值 $x$ 的冲突对象唯一是 $k-x$,不会与其他数值组产生冲突。因此对某组按钮的选择不影响其他组的可行性。

定理:算法得到防止炸弹爆炸的最少按键次数。

证明:由引理 1 和引理 2,算法对每个互补组取到最小代价;由引理 3,各组最优代价可以直接相加。遍历条件保证每组恰好计算一次,所以总和即全局最优答案。

ACM Python 代码

import sys
from collections import Counter


def solve():
    data = list(map(int, sys.stdin.buffer.read().split()))
    n, target = data[:2]
    values = data[2:2 + n]
    count = Counter(values)

    answer = 0
    for value, frequency in count.items():
        other = target - value
        if other not in count:
            continue

        if value == other:
            answer += frequency - 1
        elif value < other:
            answer += min(frequency, count[other])

    print(answer)


if __name__ == "__main__":
    solve()

复杂度分析

设不同数字的数量为 $d$。

时间复杂度:期望 $O(n+d)$,即期望 $O(n)$。

空间复杂度:$O(d)$。

易错点

  • 当某个数字与自身互补时,只需将该数的炸弹全部关闭一次,不能重复计数。
  • 对不同的互补数对只处理一次,否则会把同一对的代价计算两遍。

第 3 题:快递配送的总耗时

题目描述

有 $n$ 个乡村,编号为 $1$ 到 $n$。快递站位于乡村 $s$。乡村之间有 $m$ 条单向道路和 $k$ 条双向道路,每条道路都有非负耗时。任意两个乡村均可相互到达,且可能有重边和自环。

快递员从快递站出发,按照给定顺序配送 $q$ 件快递,每一段都走耗时最短的路径。

到达第 $i$ 件快递的目的地后,先把本次行驶耗时计入总耗时:

  • 如果此时累计耗时为奇数,则通知收货人当面取件,额外耗时 $a$;
  • 如果此时累计耗时为偶数,则将快递放入快递柜,额外耗时 $b$。

注意,本件快递的投递耗时不参与本件投递方式的奇偶判断,但会影响之后的累计耗时。

全部配送完成后,还要从最后一个目的地按最短路径返回快递站。求最终总耗时。

输入描述

第一行输入四个整数 $n,m,k,s$,分别表示乡村数、单向道路数、双向道路数和快递站编号。

接下来 $m$ 行,每行三个整数 $u,v,w$,表示一条从 $u$ 到 $v$、耗时为 $w$ 的单向道路。

再接下来 $k$ 行,每行三个整数 $u,v,w$,表示一条连接 $u,v$、两个方向耗时均为 $w$ 的双向道路。

下一行输入三个整数 $a,b,q$,分别表示当面取件耗时、放入快递柜耗时和快递数量。

最后一行输入 $q$ 个整数 $d_1,d_2,\ldots,d_q$,表示各件快递的目的地顺序。

输出描述

输出配送全部快递并返回快递站后的总耗时。

样例 1

输入

3 0 3 1
1 2 3
3 2 6
1 3 9
6 3 3
1 2 3

输出

30

解释

起点与第一件目的地都是 $1$,行驶耗时为 $0$,当前累计耗时为偶数,加快递柜耗时 $3$。随后 $1\to2$ 最短耗时为 $3$,累计为 $6$,再加 $3$;$2\to3$ 最短耗时为 $6$,累计为 $15$,再加当面取件耗时 $6$;最后 $3\to1$ 最短耗时为 $9$,总计 $30$。

样例 2

输入

4 0 4 1
1 2 1
2 3 1
3 4 1
4 1 1
3 1 3
2 4 3

输出

11

样例 3

输入

3 0 3 2
1 2 2
2 3 2
3 1 2
5 4 2
3 3

输出

12

思路分析

把每条双向道路拆成两条方向相反、权值相同的有向边,得到统一的有向图。

完整行程为

\[s\to d_1\to d_2\to\cdots\to d_q\to s.\]

只需要这些相邻站点之间的最短距离。每一段的起点一定属于集合

\[\{s,d_1,d_2,\ldots,d_q\}.\]

因此,对这个集合中的每个不同节点运行一次堆优化 Dijkstra,并保存其到所有节点的最短距离即可。配送点数量很少时,这比 Floyd 全源最短路更合适。

随后按顺序模拟:

  1. 从当前位置到本件目的地,加上最短行驶耗时;
  2. 检查此刻累计耗时的奇偶性,奇数加 $a$,偶数加 $b$;
  3. 更新当前位置。

所有快递送完后,再加当前位置返回 $s$ 的最短距离。

目的地与当前位置相同时,行驶距离为 $0$,但仍然要完成一次投递并进行奇偶判断。自环和重边不需要特殊处理,Dijkstra 的松弛操作会自然取到最短路径。

正确性证明

引理 1:每次 Dijkstra 得到指定源点到所有乡村的正确最短耗时。

证明:图中边权非负,满足 Dijkstra 算法的适用条件。堆优化只改变取出当前最小距离节点的实现方式,不改变标准 Dijkstra 的松弛过程,故结果正确。

引理 2:预处理的距离覆盖完整行程所需的每一段。

证明:每段行程起点只能是快递站 $s$ 或某个已配送目的地 $d_i$,这些节点全部在预处理源点集合中;终点是下一个目的地或最后的快递站。因此每一段所需距离都能从相应源点的距离数组中取得。

引理 3:模拟过程中每完成一件快递后,累计耗时与题意一致。

证明:算法先加入当前位置到目的地的最短耗时,此时恰为题目规定的奇偶判断时刻。之后根据该值,奇数加入 $a$、偶数加入 $b$,再更新位置,顺序与题意完全一致。按配送顺序归纳,每件完成后的累计值均正确。

定理:算法输出配送全部快递并返回快递站的正确总耗时。

证明:由引理 1 和引理 2,模拟使用的每段行驶耗时均为正确最短距离;由引理 3,全部配送与投递耗时被按正确顺序累计。最后加入返回快递站的最短耗时,故输出即题目要求的总耗时。

ACM Python 代码

import heapq
import sys


INF = float("inf")


def dijkstra(graph, source):
    distance = [INF] * len(graph)
    distance[source] = 0
    heap = [(0, source)]

    while heap:
        current_distance, node = heapq.heappop(heap)
        if current_distance != distance[node]:
            continue

        for next_node, weight in graph[node]:
            new_distance = current_distance + weight
            if new_distance < distance[next_node]:
                distance[next_node] = new_distance
                heapq.heappush(heap, (new_distance, next_node))

    return distance


def solve():
    data = list(map(int, sys.stdin.buffer.read().split()))
    iterator = iter(data)

    n = next(iterator)
    directed_count = next(iterator)
    undirected_count = next(iterator)
    station = next(iterator)

    graph = [[] for _ in range(n + 1)]

    for _ in range(directed_count):
        u = next(iterator)
        v = next(iterator)
        weight = next(iterator)
        graph[u].append((v, weight))

    for _ in range(undirected_count):
        u = next(iterator)
        v = next(iterator)
        weight = next(iterator)
        graph[u].append((v, weight))
        graph[v].append((u, weight))

    hand_time = next(iterator)
    locker_time = next(iterator)
    delivery_count = next(iterator)
    destinations = [next(iterator) for _ in range(delivery_count)]

    useful_sources = set([station] + destinations)
    distances = {
        source: dijkstra(graph, source)
        for source in useful_sources
    }

    total_time = 0
    current = station

    for destination in destinations:
        total_time += distances[current][destination]
        if total_time % 2 == 1:
            total_time += hand_time
        else:
            total_time += locker_time
        current = destination

    total_time += distances[current][station]
    print(total_time)


if __name__ == "__main__":
    solve()

复杂度分析

设将双向边拆开后有向边总数为 $E=m+2k$,不同的有效源点数为

\[r=\left\lvert\{s,d_1,d_2,\ldots,d_q\}\right\rvert\le q+1.\]

时间复杂度:$O\bigl(r(n+E)\log n+q\bigr)$。

空间复杂度:$O(E+n+rn)$,分别用于邻接表、Dijkstra 工作数组和保存各有效源点的距离。

易错点

  • 必须按配送顺序逐段累加时间,并在到达目的地后依据当前累计时间的奇偶性选择交付耗时。
  • 最后一件快递交付后仍需返回快递站,不能漏掉返程最短路。
  • 仅需从快递站和各目的地这些有效源点运行 Dijkstra,无须计算全源最短路。