青训营学习收获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)
执行结果
问题关键
给定一个行为序列S,表示为一个字符串,以及一个识别模式P。如果模式P是序列S的子序列,则说明存在匹配的黑产行为。
现在,给定多个序列S和对应的识别模式P,找出序列中出现匹配模式的次数,结果需要对10^9+7取模。
输入
- S (str): 行为序列,一个字符串。
- P (str): 识别模式,一个字符串。
输出
- int: 模式P作为子序列在S中出现的次数,对(10^9+7)取模的结果。
示例
- 输入:S = "ABC", P = "A" 输出:1 解释:模式"A"在"ABC"中作为子序列出现1次。
- 输入:S = "AABCCD", P = "CCD" 输出:1 解释:模式"CCD"在"AABCCD"中作为子序列出现1次。
- 输入: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]可以通过两种方式获得:- 包含当前字符
S[i-1],即dp[i-1][j-1]。 - 不包含当前字符
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,以防止整数溢出。
知识总结与学习建议
新知识点
-
动态规划的应用:本题是动态规划在字符串处理中的一个典型应用案例,特别是在处理子序列问题时。动态规划能够将一个复杂问题分解成一系列相似的子问题,通过解决子问题来解决整个问题。
-
二维DP数组的理解和使用:
- 二维DP数组通常用来存储在两个维度上的状态转移信息,例如在本题中,一个维度是字符串S的前i个字符,另一个维度是模式P的前j个字符。
- 通过填充这样的数组,我们可以逐步构建出解决问题所需的信息。
-
模的应用:在处理大数问题时,为了避免整数溢出并保持结果的处理速度,经常需要对结果进行模运算。本题中的模(10^9+7)是一个常用的大质数,它能够在保证计算效率的同时减小冲突概率。
个人理解
-
动态规划的填表技巧:在动态规划中,理解如何填表是关键。对于本题,我们从左到右、从上到下填表,每个单元格的填写都依赖于其左边和上方的单元格。这种依赖关系体现了子问题之间的联系。
-
边界条件的重要性:在动态规划中设置正确的边界条件是成功解决问题的关键。例如,初始化
dp[i][0] = 1是基于任何长度的S都可以通过删除所有字符来形成空模式P的逻辑。
对入门同学的学习建议
- 理解问题的本质:编码之前,彻底理解问题的要求和本质。尝试用自己的话描述问题和解决方案。
- 练习基础编程技能:动态规划问题需要基础,特别是数组和循环的使用。通过练习基础的数组操作和循环控制,可以为解决更复杂的问题打下基础。
- 学习和使用伪代码:在编写实际代码之前,使用伪代码来规划解决方案。伪代码可以帮助组织思路,明确每一步需要做什么,减少编码中的错误。
- 逐步构建和测试:在开发解决方案时,逐步构建和测试每个部分。不仅可以帮助发现和修正错误,还可以加深对问题的理解。
(2)AI 刷题:IP报文头解析问题(难度中)
问题描述
小R 负责解析IP报文头信息,现有一个十六进制格式的IP报文头数据 header,他需要从中解析并输出其中的总长度、标志位以及目的IP地址,用逗号分隔。
IP报文头信息依次包含多个字段,其中标识(16位)和目的IP地址(32位)是重点。输入数据为合法的十六进制IP报文头,固定长度为59个字符,每两个十六进制数字表示一个字节,字节之间以单空格分隔。
注:报文数据为大端序(即高位字节在低地址),小R需要将这些数据进行解析,输出的总长度和标志为十进制整数,目的IP地址为点分十进制格式(如192.168.20.184)。
返回规则如下:
- 解析其中的总长度、标志位以及目的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"
思路
- 分割字符串: 将输入的十六进制字符串按空格分割成字节列表。
- 提取总长度:
- 总长度字段位于第3和第4个字节。
- 将这两个字节的十六进制值拼接并转换为十进制。
- 提取标志位:
- 标志位在第7和第8个字节的高3位。
- 将这两个字节的十六进制值拼接,转换为十进制后,通过位运算提取高3位。
- 提取目的IP地址:
- 目的IP地址位于最后4个字节。
- 将每个字节的十六进制值转换为十进制,并格式化为点分十进制格式。
- 格式化输出: 将上述三个结果用逗号连接成一个字符串。
如何思考
- 字节分割: 理解输入字符串的结构,识别每两个字符代表一个字节。
- 大端序: 确保按照大端序解析数据,即高位字节在前。
- 位操作: 使用位移和按位与运算提取标志位的高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")
执行结果
知识总结
新知识点
-
十六进制与十进制转换:
- 在计算机网络中,数据通常以十六进制表示。理解如何在编程语言中进行十六进制和十进制的转换是基础技能。
- 思考: 使用Python的
int()函数可以方便地将十六进制字符串转换为十进制整数。
-
大端序和小端序:
- 数据在不同系统中可能以不同的字节序存储。大端序表示高位字节在前,小端序相反。
- 思考: 在解析网络协议时,通常使用大端序,需要根据协议规范正确解析数据。
-
位操作:
- 位操作用于直接操作二进制位,适合从数据包中提取特定位。
- 思考: 使用位移和按位与操作可以有效提取和处理特定位的值。
-
IP地址格式化:
- IP地址通常以点分十进制表示,理解如何从字节转换为这种格式是解析网络数据的重要步骤。
- 思考: 将每个字节转换为十进制,并用
.连接成IP地址格式。
学习建议
- 多练习数据转换: 在日常编程中多练习十六进制、二进制和十进制之间的转换,熟悉不同进制的表示和转换方法。
- 理解网络协议: 学习常见的网络协议如IP、TCP、UDP等,了解它们的头部结构和字段含义,这对解析网络数据包非常有帮助。
- 掌握位操作技巧: 位操作是处理低级数据的利器。通过练习,掌握如何使用位移、按位与、按位或等操作来提取和设置特定位。
- 动手实践: 通过编写小程序来解析数据包头部,增强理解和动手能力。尝试从抓包工具(如Wireshark)中获取真实数据进行解析。