文远知行(07.27场)笔试复盘:三道编程题思路详解与踩坑点分析

Hello,各位牛友!

文远知行近期的笔试,趁热打铁,把三道编程题的思路和AC代码整理了一下分享给大家。整体感觉题目难度中等,都是大家比较熟悉的经典模型,但想做到完全Bug-Free还是需要注意一些细节。希望这篇复盘能帮到后续参加的同学,祝大家都能顺利上岸!

第一题:分发糖果

题目大意

有一排人,每个人都有一个评分值。你需要按照以下规则给他们分发糖果:

  • 每个人至少得到 1 颗糖果
  • 评分更高的邻居,得到的糖果也必须更多
  • 目标是计算出满足这些规则所需的最少糖果总数

考点分析

贪心算法。这道题的难点在于,每个人的糖果数同时受到左、右两个邻居的制约。只考虑单边约束很容易,但要同时满足两边,就需要巧妙的解法。正确的思路是通过两次贪心遍历来解耦这个双向约束。

样例输入与输出

样例 1:

[1,0,2]

输出: 5 (最优分配为 )

样例 2:

[1,2,2]

输出: 4 (最优分配为 )

解题思路

这道题是LeetCode上的原题,非常经典。核心思想是"两边夹击"。

第一次遍历(从左到右):

  • 初始化一个 candies 数组,所有人糖果都为1
  • 从左到右遍历评分数组。如果当前学生 i 的评分比他左边的学生 i-1 高 (ratings[i] > ratings[i-1]),那么他的糖果数应该至少是左边学生糖果数加一,即 candies[i] = candies[i-1] + 1
  • 这次遍历结束后,我们保证了所有"右边比左边评分高"的情况都得到了满足

第二次遍历(从右到左):

  • 现在从右到左遍历。如果当前学生 i 的评分比他右边的学生 i+1 高 (ratings[i] > ratings[i+1]),他的糖果数需要更新
  • 关键点:此时他的糖果数应该是 max(candies[i], candies[i+1] + 1)。为什么要取 max?因为第一次遍历的结果已经满足了左边的约束,我们不能破坏它
  • 例如,对于评分 ,左到右遍历后糖果是 ;右到左遍历时,candies 会更新为 candies+1=2candies 会更新为 candies+1=3,结果是 ``
  • 但对于评分 ,左到右遍历后是 ,右到左遍历时,虽然评分不满足 >,但我们不能把 `` 降下来。因此,取最大值可以同时兼顾两个方向的约束

汇总:将最终的 candies 数组求和,就是最少的糖果总数。

AC代码 (Python)

import json

def solve():
    """
    处理分发糖果问题
    """
    try:
        # 使用 json 库来安全、高效地解析输入字符串
        line = input()
        ratings = json.loads(line)
    except (json.JSONDecodeError, IndexError):
        print(0)
        return

    n = len(ratings)
    if n == 0:
        print(0)
        return

    # 1. 从左到右遍历,满足右边比左边评分高的约束
    left_to_right_candies = [1] * n
    for i in range(1, n):
        if ratings[i] > ratings[i - 1]:
            left_to_right_candies[i] = left_to_right_candies[i - 1] + 1

    # 2. 从右到左遍历,满足左边比右边评分高的约束
    # 直接在第一次结果上优化,并累加总数以节省空间
    total_candies = 0
    right_val = 1 # 模拟一个从右往左的糖果计数器
    for i in range(n - 1, -1, -1):
        # 如果当前学生评分比右边高,则右侧规则生效
        if i < n - 1 and ratings[i] > ratings[i + 1]:
            right_val += 1
        else:
            # 否则,重置为1
            right_val = 1
        
        # 核心:当前学生的糖果数是两种规则下的最大值
        final_candy = max(left_to_right_candies[i], right_val)
        total_candies += final_candy
        
    print(total_candies)

if __name__ == "__main__":
    solve()

第二题:课程价值最大化

题目大意

给定一组课程,每门课程都有开始时间、结束时间和价值。你需要从中选择一部分课程,要求所选课程之间时间上不能有重叠(一门课程的结束时间等于另一门的开始时间不算重叠),目标是让所选课程的总价值最大。

考点分析

动态规划 (DP) + 贪心 + 二分查找。这是一个经典的加权区间调度问题。

  • 贪心:按结束时间对课程进行排序是解决此类问题的标准预处理步骤
  • 动态规划dp[i] 通常定义为"考虑前 i 门课程所能获得的最大价值"
  • 二分查找:用于优化状态转移过程,将复杂度从 O(n²) 降低到 O(nlogn)

样例输入与输出

样例 1:

3
1 10 10
3 4 11
4 5 3

输出: 14

样例 2:

4
1 3 5
2 5 6
4 6 5
5 7 4

输出: 10

解题思路

  1. 排序:将所有课程按照结束时间升序排列。为什么要按结束时间?因为当我们考虑第 i 门课时,这个排序策略能保证所有可能与它搭配的、不冲突的课程都位于它的前面,方便我们做决策。
  2. 定义DP状态:我们定义 dp[i] 为"处理完排序后的前 i 门课程(从1到i),可以获得的最大价值"。
  3. 状态转移:对于第 i 门课程(设其开始时间为 start_i,结束时间为 end_i,价值为 value_i),我们有两种选择:
    • 不选择第 i 门课:那么最大价值就和处理前 i-1 门课时一样,即 dp[i-1]
    • 选择第 i 门课:那么我们就不能选择任何在 start_i 之后结束的课程。我们需要找到一个最大的索引 p,使得第 p 门课的结束时间 end_p <= start_i。那么选择第 i 门课的总价值就是 dp[p] + value_i

所以,状态转移方程为:

dp[i] = max(dp[i-1], dp[p] + value_i)
  1. 优化查找:暴力查找 p 会使时间复杂度达到 O(n²)。由于课程是按结束时间有序的,我们可以使用二分查找在 O(logn) 的时间内快速找到这个 p。

AC代码 (Python)

import sys
import bisect

def solve():
    """
    处理课程价值最大化问题
    """
    try:
        n = int(sys.stdin.readline())
        courses = []
        for _ in range(n):
            # 存储为 (结束时间, 开始时间, 价值) 以便排序
            s, e, v = map(int, sys.stdin.readline().split())
            courses.append((e, s, v))
    except (ValueError, IndexError):
        return

    # 1. 按课程结束时间升序排序
    courses.sort()

    # dp[i] 表示考虑前 i 门课程能获得的最大价值
    # dp 数组长度为 n+1,dp[0] = 0 作为哨兵
    dp = [0] * (n + 1)
    
    # 提取所有结束时间,用于二分查找
    end_times = [c[0] for c in courses]

    for i in range(1, n + 1):
        end_time, start_time, value = courses[i-1]
        
        # 决策1: 不选择当前第 i 门课程
        # 此时最大价值与考虑前 i-1 门课时相同
        option1_value = dp[i-1]
        
        # 决策2: 选择当前第 i 门课程
        # 使用二分查找 (bisect_right) 找到在当前课程开始前就已结束的课程的临界点
        # bisect_right 会找到 start_time 在 end_times 中的插入点
        # 这个索引 p 正好是前 p 门课程,它们的结束时间都 <= start_time
        p = bisect.bisect_right(end_times, start_time, hi=i-1)
        
        # dp[p] 就是这 p 门课程可以获得的最大价值
        # dp数组索引比课程索引大1,所以dp[p]对应的是前p门课的结果
        option2_value = (dp[p] if p > 0 else 0) + value
        
        # 状态转移:取两种决策中的最大值
        dp[i] = max(option1_value, option2_value)

    print(dp[n])

if __name__ == "__main__":
    solve()

第三题:奶牛编号递推

题目大意

奶牛的编号 f(n) 由一个线性递推关系式决定:

f(n) = 2×f(n−1) + 3×f(n−2) + f(n−3)

初始条件为 f(1)=1, f(2)=2, f(3)=31

需要计算 f(n) 对 123456789 取模的结果,其中 n 非常大(可达 10¹⁸)。

考点分析

线性代数 + 矩阵快速幂

这是一个非常明显的信号:

  • 固定的线性递推关系
  • 超大的 n

当遇到这种情况时,几乎可以肯定是考察矩阵快速幂。其思想是将递推过程转化为矩阵的幂运算,然后利用类似整数快速幂的分治思想,将计算 A^n 的时间复杂度从 O(n) 优化到 O(logn)。

样例输入与输出

样例:

5
3
6
9
12
15

输出:

31
700
7486
64651
527023

解题思路

  1. 构造状态向量:递推关系涉及到前三项,所以我们可以构造一个 3×1 的状态向量:
V_n = [f(n), f(n−1), f(n−2)]ᵀ
  1. 构建转移矩阵:我们的目标是找到一个 3×3 的矩阵 A,使得 V_n = A × V_{n−1}

根据递推公式 f(n) = 2f(n−1) + 3f(n−2) + 1f(n−3),我们可以构建出矩阵 A 的第一行。

另外两个关系是平凡的:f(n−1) = 1f(n−1) + 0f(n−2) + 0f(n−3)f(n−2) = 0f(n−1) + 1f(n−2) + 0f(n−3)

综合起来,转移矩阵 A 为:

A = [2  3  1]
    [1  0  0]
    [0  1  0]
  1. 建立递推关系
[f(n)  ]     [2  3  1] [f(n−1)]
[f(n−1)] =   [1  0  0] [f(n−2)]
[f(n−2)]     [0  1  0] [f(n−3)]

由此可得:V_n = A × V_{n−1} = A² × V_{n−2} = ⋯ = A^{n−3} × V_3

其中 V_3 是我们的初始状态向量:V_3 = [f(3), f(2), f(1)]ᵀ = ᵀ 4. 矩阵快速幂:现在的任务就是高效计算 A^{n−3}。我们可以实现一个 mat_pow(matrix, power) 函数,使用二分法原理来计算矩阵的幂。所有计算过程中都要对 MOD 取模。 5. 计算最终结果:得到 A^{n−3} 后,记为 Result_Matrix。最终的 f(n) 就是 Result_Matrix 的第一行与初始向量 V_3 的点积:

f(n) = Result_Matrix[0][0]×f(3) + Result_Matrix[0][1]×f(2) + Result_Matrix[0][2]×f(1)

AC代码 (Python)

import sys

MOD = 123456789

def mat_mul(A, B):
    """
    计算两个 3x3 矩阵的乘积 (A * B),结果对 MOD 取模。
    """
    C = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
    for i in range(3):
        for j in range(3):
            for k in range(3):
                C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD
    return C

def mat_pow(base_matrix, power):
    """
    计算矩阵的快速幂 (base_matrix ^ power)。
    """
    # 结果矩阵初始化为单位矩阵
    result_matrix = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
    for i in range(3):
        result_matrix[i][i] = 1
    
    temp_matrix = base_matrix
    
    while power > 0:
        # 如果幂是奇数,将结果乘上当前的基底矩阵
        if power & 1:
            result_matrix = mat_mul(result_matrix, temp_matrix)
        # 基底矩阵自乘
        temp_matrix = mat_mul(temp_matrix, temp_matrix)
        # 幂次减半
        power >>= 1
        
    return result_matrix

def solve():
    """
    主解决函数
    """
    try:
        t = int(sys.stdin.readline())
        for _ in range(t):
            n = int(sys.stdin.readline())
            
            if n <= 2:
                print(n)
                continue
            if n == 3:
                print(31)
                continue

            # 转移矩阵 A
            transition_matrix = [
                [2, 3, 1],
                [1, 0, 0],
                [0, 1, 0]
            ]
            
            # 初始状态向量 V3 = [f(3), f(2), f(1)]
            initial_vector = [31, 2, 1]
            
            # 我们需要计算 A^(n-3)
            power = n - 3
            result_mat = mat_pow(transition_matrix, power)
            
            # f(n) = Result_Matrix[0] · V3
            ans = 0
            for i in range(3):
                ans = (ans + result_mat[0][i] * initial_vector[i]) % MOD
            
            print(ans)

    except (ValueError, IndexError):
        return

if __name__ == "__main__":
    solve()
#笔试##秋招##算法#
全部评论

相关推荐

计算机劝退第一人:北✌🏻乱杀
你最希望上岸的公司是?
点赞 评论 收藏
分享
评论
2
2
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务