青训营学习收获2 | 豆包MarsCode AI 刷题

107 阅读12分钟

青训营学习收获2

(1)AI 刷题:黑产行为序列识别(难度中)

问题描述

小S 和小M 正在研究一种黑产行为序列识别技术。网络黑色产业链是指使用互联网技术进行非法活动,例如网络攻击、窃取信息、诈骗等。为了保护用户和平台的安全,识别黑产行为的序列是十分关键的一步。

他们的任务是:给定一个行为序列S,表示为一个字符串,以及一个识别模式P。如果模式P是序列S的子序列,则说明存在匹配的黑产行为。
现在,给定多个序列S和对应的识别模式P,找出序列中出现匹配模式的次数,结果需要对10^9+7取模。

测试样例

示例 1:

输入:S = "ABC", P = "A"
输出:1
提示:在这个例子中,小F发现序列"ABC"中包含一个"A"作为子序列,因此输出1。

示例 2:

输入:S = "AABCCD", P = "CCD"
输出:1
提示:小S 注意到识别模式"CCD"在序列"AABCCD"中匹配了一次,因此输出1。

示例 3:

输入:S = "AABCCD", P = "C"
输出:2

完整解答

def solution(S: str, P: str) -> int:
    MOD = 10**9 + 7
    n, m = len(S), len(P)
    
    # dp[i][j] means the number of ways to form the first j characters of P using the first i characters of S
    dp = [[0] * (m + 1) for _ in range(n + 1)]
    
    # Base case: empty pattern P can be formed in 1 way (using empty subsequence of S)
    for i in range(n + 1):
        dp[i][0] = 1
    
    # Fill the dp table
    for i in range(1, n + 1):
        for j in range(1, m + 1):
            if S[i - 1] == P[j - 1]:
                dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j]) % MOD
            else:
                dp[i][j] = dp[i - 1][j]
    
    return dp[n][m]

if __name__ == '__main__':
    print(solution("ABC", "A") == 1)
    print(solution("AABCCD", "CCD") == 1)
    print(solution("AABCCD", "C") == 2)

执行结果

image.png

问题关键

给定一个行为序列S,表示为一个字符串,以及一个识别模式P。如果模式P是序列S的子序列,则说明存在匹配的黑产行为。
现在,给定多个序列S和对应的识别模式P,找出序列中出现匹配模式的次数,结果需要对10^9+7取模。

输入

  • S (str): 行为序列,一个字符串。
  • P (str): 识别模式,一个字符串。

输出

  • int: 模式P作为子序列在S中出现的次数,对(10^9+7)取模的结果。

示例

  1. 输入:S = "ABC", P = "A" 输出:1 解释:模式"A"在"ABC"中作为子序列出现1次。
  2. 输入:S = "AABCCD", P = "CCD" 输出:1 解释:模式"CCD"在"AABCCD"中作为子序列出现1次。
  3. 输入:S = "AABCCD", P = "C" 输出:2 解释:模式"C"在"AABCCD"中作为子序列出现2次。

解题思路

解决这个问题的关键是使用动态规划来计算模式P作为子序列在S中出现的次数。

动态规划设计

定义一个二维数组dp,其中dp[i][j]表示使用字符串S的前i个字符可以形成字符串P的前j个字符的子序列的方式数量。

初始化
  • dp[0][0] = 1:空模式P可以通过空序列S形成一种方式。
  • dp[i][0] = 1:任何长度的S都可以通过删除所有字符来形成空模式P,因此初始化所有dp[i][0]为1。
  • dp[0][j] = 0:空序列S不能形成任何非空模式P,除了dp[0][0]
状态转移
  • 如果S[i-1] == P[j-1],则dp[i][j]可以通过两种方式获得:
    1. 包含当前字符S[i-1],即dp[i-1][j-1]
    2. 不包含当前字符S[i-1],即dp[i-1][j]
  • 如果S[i-1] != P[j-1],则dp[i][j] = dp[i-1][j],因为不能使用S[i-1]来匹配P[j-1]

返回结果

最终结果将在dp[len(S)][len(P)]中,表示使用整个S来形成整个P的方式数量。

得到代码

def solution(S: str, P: str) -> int:
    MOD = 10**9 + 7
    n, m = len(S), len(P)
    dp = [[0] * (m + 1) for _ in range(n + 1)]
    for i in range(n + 1):
        dp[i][0] = 1  # 空模式P的初始化
    for i in range(1, n + 1):
        for j in range(1, m + 1):
            if S[i - 1] == P[j - 1]:
                dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j]) % MOD
            else:
                dp[i][j] = dp[i - 1][j]
    return dp[n][m]

代码按动态规划的逻辑来实现,通过迭代填充dp表格,最后从dp[len(S)][len(P)]获取结果。每个状态的更新都取模10^9+7,以防止整数溢出。

知识总结与学习建议

新知识点

  1. 动态规划的应用:本题是动态规划在字符串处理中的一个典型应用案例,特别是在处理子序列问题时。动态规划能够将一个复杂问题分解成一系列相似的子问题,通过解决子问题来解决整个问题。

  2. 二维DP数组的理解和使用

    • 二维DP数组通常用来存储在两个维度上的状态转移信息,例如在本题中,一个维度是字符串S的前i个字符,另一个维度是模式P的前j个字符。
    • 通过填充这样的数组,我们可以逐步构建出解决问题所需的信息。
  3. 模的应用:在处理大数问题时,为了避免整数溢出并保持结果的处理速度,经常需要对结果进行模运算。本题中的模(10^9+7)是一个常用的大质数,它能够在保证计算效率的同时减小冲突概率。

个人理解

  • 动态规划的填表技巧:在动态规划中,理解如何填表是关键。对于本题,我们从左到右、从上到下填表,每个单元格的填写都依赖于其左边和上方的单元格。这种依赖关系体现了子问题之间的联系。

  • 边界条件的重要性:在动态规划中设置正确的边界条件是成功解决问题的关键。例如,初始化dp[i][0] = 1是基于任何长度的S都可以通过删除所有字符来形成空模式P的逻辑。

对入门同学的学习建议

  1. 理解问题的本质:编码之前,彻底理解问题的要求和本质。尝试用自己的话描述问题和解决方案。
  2. 练习基础编程技能:动态规划问题需要基础,特别是数组和循环的使用。通过练习基础的数组操作和循环控制,可以为解决更复杂的问题打下基础。
  3. 学习和使用伪代码:在编写实际代码之前,使用伪代码来规划解决方案。伪代码可以帮助组织思路,明确每一步需要做什么,减少编码中的错误。
  4. 逐步构建和测试:在开发解决方案时,逐步构建和测试每个部分。不仅可以帮助发现和修正错误,还可以加深对问题的理解。

(2)AI 刷题:IP报文头解析问题(难度中)

问题描述

小R 负责解析IP报文头信息,现有一个十六进制格式的IP报文头数据 header,他需要从中解析并输出其中的总长度、标志位以及目的IP地址,用逗号分隔。
IP报文头信息依次包含多个字段,其中标识(16位)和目的IP地址(32位)是重点。输入数据为合法的十六进制IP报文头,固定长度为59个字符,每两个十六进制数字表示一个字节,字节之间以单空格分隔。

注:报文数据为大端序(即高位字节在低地址),小R需要将这些数据进行解析,输出的总长度和标志为十进制整数,目的IP地址为点分十进制格式(如192.168.20.184)。 image.png 返回规则如下:

  • 解析其中的总长度、标志位以及目的IP地址,用逗号分隔。

测试样例

示例 1:

输入:header = "45 00 10 3c 7c 48 20 03 80 06 00 00 c0 a8 01 02 c0 a8 14 b8"
输出:"4156,1,192.168.20.184"

示例 2:

输入:header = "4b ba 0d 15 d0 42 16 bc 50 25 38 33 cb e0 77 ed 56 a4 30 46"
输出:"3349,0,86.164.48.70"

示例 3:

输入:header = "f7 87 78 be cf bf ae 9e d6 bc b1 5f 38 2c 07 37 95 f8 32 c5"
输出:"30910,5,149.248.50.197"

题目解析:IP报文头解析问题

输入输出

  • 输入: 一个固定长度为59个字符的十六进制格式IP报文头字符串,每两个字符表示一个字节,字节之间用单空格分隔。
  • 输出: 一个字符串,包含三个解析结果:总长度、标志位、目的IP地址,使用逗号分隔。

示例

  • 输入: "45 00 10 3c 7c 48 20 03 80 06 00 00 c0 a8 01 02 c0 a8 14 b8"
  • 输出: "4156,1,192.168.20.184"

思路

  1. 分割字符串: 将输入的十六进制字符串按空格分割成字节列表。
  2. 提取总长度:
    • 总长度字段位于第3和第4个字节。
    • 将这两个字节的十六进制值拼接并转换为十进制。
  3. 提取标志位:
    • 标志位在第7和第8个字节的高3位。
    • 将这两个字节的十六进制值拼接,转换为十进制后,通过位运算提取高3位。
  4. 提取目的IP地址:
    • 目的IP地址位于最后4个字节。
    • 将每个字节的十六进制值转换为十进制,并格式化为点分十进制格式。
  5. 格式化输出: 将上述三个结果用逗号连接成一个字符串。

如何思考

  • 字节分割: 理解输入字符串的结构,识别每两个字符代表一个字节。
  • 大端序: 确保按照大端序解析数据,即高位字节在前。
  • 位操作: 使用位移和按位与运算提取标志位的高3位。
  • 格式转换: 熟悉十六进制到十进制的转换,以及IP地址的格式化。

代码详解/完整解答

def solution(header: str) -> str:
    # 将输入字符串按空格分割成字节列表
    bytes_list = header.split()
    
    # 提取总长度: 位于第3和第4个字节
    total_length_hex = bytes_list[2] + bytes_list[3]
    total_length = int(total_length_hex, 16)
    
    # 提取标志位: 位于第7和第8个字节的高3位
    flags_fragment_hex = bytes_list[6] + bytes_list[7]
    flags_fragment = int(flags_fragment_hex, 16)
    flags = (flags_fragment >> 13) & 0x7  # 右移13位并按位与取高3位
    
    # 提取目的IP地址: 位于最后4个字节
    dest_ip_bytes = bytes_list[16:20]
    dest_ip = '.'.join(str(int(b, 16)) for b in dest_ip_bytes)
    
    # 格式化输出结果
    return f"{total_length},{flags},{dest_ip}"

print(solution("45 00 10 3c 7c 48 20 03 80 06 00 00 c0 a8 01 02 c0 a8 14 b8") == "4156,1,192.168.20.184")
print(solution("4b ba 0d 15 d0 42 16 bc 50 25 38 33 cb e0 77 ed 56 a4 30 46") == "3349,0,86.164.48.70")
print(solution("dd fb 25 3b 41 92 12 33 cb cd a1 c8 41 3e 75 29 c4 7f 98 65") == "9531,0,196.127.152.101")

执行结果

image.png

知识总结

新知识点

  1. 十六进制与十进制转换:

    • 在计算机网络中,数据通常以十六进制表示。理解如何在编程语言中进行十六进制和十进制的转换是基础技能。
    • 思考: 使用Python的int()函数可以方便地将十六进制字符串转换为十进制整数。
  2. 大端序和小端序:

    • 数据在不同系统中可能以不同的字节序存储。大端序表示高位字节在前,小端序相反。
    • 思考: 在解析网络协议时,通常使用大端序,需要根据协议规范正确解析数据。
  3. 位操作:

    • 位操作用于直接操作二进制位,适合从数据包中提取特定位。
    • 思考: 使用位移和按位与操作可以有效提取和处理特定位的值。
  4. IP地址格式化:

    • IP地址通常以点分十进制表示,理解如何从字节转换为这种格式是解析网络数据的重要步骤。
    • 思考: 将每个字节转换为十进制,并用.连接成IP地址格式。

学习建议

  • 多练习数据转换: 在日常编程中多练习十六进制、二进制和十进制之间的转换,熟悉不同进制的表示和转换方法。
  • 理解网络协议: 学习常见的网络协议如IP、TCP、UDP等,了解它们的头部结构和字段含义,这对解析网络数据包非常有帮助。
  • 掌握位操作技巧: 位操作是处理低级数据的利器。通过练习,掌握如何使用位移、按位与、按位或等操作来提取和设置特定位。
  • 动手实践: 通过编写小程序来解析数据包头部,增强理解和动手能力。尝试从抓包工具(如Wireshark)中获取真实数据进行解析。