一、递归与分治
1.数组中的逆序对
Description
在数组中的两个整数,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
Input
共两行,第一行只有1个数,即数组中数字的个数n;第二行为数组的n个整数,以空格隔开,如:
3
1 2 3
Output
输出为1个数,即所给数组中逆序对的总数
python题解:
什么是逆序对?
逆序对是数组中两个数的组合,如果前面的数大于后面的数,就称为一个逆序对。例如,数组 [3, 2, 1] 中有以下逆序对:
(3, 2)(3, 1)(2, 1)
这些逆序对说明数组元素顺序相对于递增顺序有多少不对的地方。
分治法思想
这个问题可以用分治法解决。我们通过将数组递归地分成更小的部分,然后在合并的过程中统计逆序对。这种思想可以通过归并排序来实现。
代码解释:
-
定义全局变量
ans: 用来存储逆序对的总数。 -
merge_sort 函数: 这个函数实现了递归的归并排序。在每次递归的时候,数组会被一分为二,直到每个部分只有一个元素。
mid = (l + r) // 2:找到数组的中间位置。merge_sort(q, l, mid)和merge_sort(q, mid + 1, r):递归地对左右两个部分进行排序。
然后,在合并两个已经排好序的数组时,进行逆序对的计数:
- 如果左边的元素大于右边的元素,说明左边的这个元素和右边的所有元素都形成了逆序对,因为左边部分已经是排序的。
-
合并两个有序数组:
tmp是一个临时数组,用于存储排序结果。- 两个指针
i和j分别指向左右两部分的起始位置。 - 比较左右两部分的元素并进行排序,同时更新逆序对的数量。
-
返回结果: 在所有递归完成之后,逆序对的数量已经保存在
ans中,直接输出即可。
def merge_sort(q, l, r):
global ans # 全局变量用于记录逆序对数量
if l >= r: # 递归终止条件,当只有一个元素时,不再递归
return
mid = (l + r) // 2 # 找到数组中间位置
merge_sort(q, l, mid) # 递归处理左半部分
merge_sort(q, mid + 1, r) # 递归处理右半部分
i, j, k = l, mid + 1, l # i 是左半部分的指针,j 是右半部分的指针
tmp = [0] * len(q) # 临时数组存储合并后的结果
while i <= mid and j <= r:
if q[i] <= q[j]: # 左边元素 <= 右边元素时,直接合并
tmp[k] = q[i]
i += 1
else: # 发现逆序对,右边元素小于左边元素
tmp[k] = q[j]
j += 1
ans += mid - i + 1 # 左边剩余的所有元素都大于右边这个元素
k += 1
while i <= mid: # 处理左半部分剩余的元素
tmp[k] = q[i]
i += 1
k += 1
while j <= r: # 处理右半部分剩余的元素
tmp[k] = q[j]
j += 1
k += 1
for i in range(l, r + 1): # 将合并后的结果拷贝回原数组
q[i] = tmp[i]
if __name__ == "__main__":
n = int(input()) # 输入数组长度
q = list(map(int, input().split())) # 输入数组元素
ans = 0 # 初始化逆序对数量
merge_sort(q, 0, n - 1) # 调用归并排序
print(ans) # 输出逆序对数量
2.数列排序
Description
给定一组或多组数列,将这个数列按从小到大的顺序排列,这些数列的长度大于等于1,小于等于200
!本题必须用二路归并排序方法求解!
Input
输入描述:
每组数的第一行为一个整数n,第二行包含n个整数,为待排序的数,每个整数的绝对值小于10000。注意输入可能有多组
输入样例:
5
8 3 6 4 9
3
2 -3 7
Output
输出描述:
输出一行,按从小到大的顺序输出排序后的数列
python代码思路:
1. 整体框架:
程序会接收多个数据组,每组包含一个数组。对于每一组数据,使用归并排序将数组按从小到大的顺序排列,并输出结果。
2. 归并排序(Merge Sort)核心思路:
归并排序的基本思路是:
- 将一个数组拆分为左右两个部分,分别进行排序;
- 然后将两个有序的子数组合并成一个有序的大数组。
整个过程可以概括为以下三个步骤:
- 递归分解:将数组分为左右两部分,不断递归调用
merge_sort,直到每部分只有一个元素(因为一个元素的数组天然是有序的)。 - 合并:使用
merge函数将左右两个有序子数组合并成一个有序数组。 - 递归回溯:合并完成后递归地回溯到上一层,直到整个数组被排序。
3. 代码结构:
-
merge函数:
- 功能:合并两个有序的子数组片段,保证合并后的数组是有序的。
- 合并步骤:
- 初始化两个指针
i和j分别指向左右两个子数组的起始位置。 - 比较
i和j所指向的元素,将较小的元素加入临时数组temp。 - 当一个子数组的元素都合并到
temp中后,将另一个子数组剩余的元素直接加入temp。 - 最后将
temp中的元素复制回原数组的相应位置。
- 初始化两个指针
-
merge_sort函数:
- 功能:递归地将数组分成左右两部分,然后调用
merge函数将两个有序部分合并。 - 过程:
- 如果
left < right,说明当前数组片段长度大于1,继续分割。 - 计算中间位置
mid,分别对左右两个子数组递归调用merge_sort进行排序。 - 调用
merge函数将左右两个子数组合并。
- 如果
- 功能:递归地将数组分成左右两部分,然后调用
-
输入输出处理:
- 主程序部分不断读取输入数据。每一组输入数据包含两个部分:第一个数字是数组长度
n,第二部分是n个需要排序的整数。 - 使用
try-except捕获EOFError来处理输入结束的情况。对于每一组数据,读取并排序后输出结果。
- 主程序部分不断读取输入数据。每一组输入数据包含两个部分:第一个数字是数组长度
4. 细节说明:
-
递归分解:
递归时,merge_sort函数将数组不断二分,每次递归将问题规模减小到一半。例如对于数组[8, 3, 6, 4, 9],首先被分为[8, 3, 6]和[4, 9],接着[8, 3, 6]继续被分为[8, 3]和[6],以此类推,直到每个子数组只有一个元素。 -
合并过程:
当数组被分解到最小单位后,merge函数开始执行。它将两个子数组中的元素从小到大排序,并将排序后的结果重新写入原数组。例如,子数组[8]和[3]合并后为[3, 8],接着[3, 8]和[6]合并为[3, 6, 8]。 -
输入与输出:
每次读取一组数据后进行排序,然后将排序后的数组输出在同一行,以空格分隔。
5. 时间复杂度分析:
- 归并排序的时间复杂度为 O(n log n),其中
n是数组的长度。 - 在最坏情况下,归并排序依旧能保持 O(n log n) 的复杂度,这使得它比一些简单的排序算法(如冒泡排序)效率更高,特别是当数组规模较大时。
def merge(arr, left, mid, right):
# 创建临时数组存储合并后的结果
temp = []
i, j = left, mid + 1
# 合并两个有序的子数组
while i <= mid and j <= right:
if arr[i] <= arr[j]:
temp.append(arr[i])
i += 1
else:
temp.append(arr[j])
j += 1
# 处理左边剩余的元素
while i <= mid:
temp.append(arr[i])
i += 1
# 处理右边剩余的元素
while j <= right:
temp.append(arr[j])
j += 1
# 将合并后的数组复制回原数组
for i in range(len(temp)):
arr[left + i] = temp[i]
def merge_sort(arr, left, right):
if left < right:
mid = (left + right) // 2
merge_sort(arr, left, mid) # 递归处理左半部分
merge_sort(arr, mid + 1, right) # 递归处理右半部分
merge(arr, left, mid, right) # 合并两个有序部分
if __name__ == "__main__":
try:
while True:
n = int(input()) # 读取数组长度
arr = list(map(int, input().split())) # 读取数组内容
merge_sort(arr, 0, n - 1) # 调用归并排序
print(" ".join(map(str, arr))) # 输出排序结果
except EOFError:
pass # 处理输入结束
3.整数划分
Description
一个正整数 n 可以表示成若干个正整数之和,形如: n=n1+n2+…+nk ,其中 n1≥n2≥…≥nk,k≥1 。
我们将这样的一种表示称为正整数 n 的一种划分。
现在给定一个正整数 n ,请你求出 n 共有多少种不同的划分方法。
Input
共一行,包含一个整数 n 。
Output
共一行,包含一个整数,表示总划分数量。
由于答案可能很大,输出结果请对 10^9+7 取模。
Sample in 5 out 7
python代码思路:
-
动态规划: 这道题利用了动态规划的思想。我们使用
dp[n][m]来表示将整数n划分成最大加数不超过m的划分方法总数。 -
函数
q(n, m, dp):- 递归地计算划分方法。
- 当
n < 1或m < 1时,返回0,表示没有有效划分。 - 当
n == 1或m == 1时,返回1,因为只有一种划分方法。 - 如果
n < m,则递归调用q(n, n, dp),因为最大加数不能超过n。 - 如果
n == m,则表示可以有一种划分为n = n,加上其他的划分方法。 - 如果
n > m,则有两种情况:- 不使用
m,递归调用q(n, m-1, dp); - 使用
m,递归调用q(n-m, m, dp)。 最后将两种情况的结果相加并对MOD取模。
- 不使用
-
主程序:
- 读取输入的
n,并初始化二维数组dp,其值初始为-1。 - 调用
q(n, n, dp)计算划分方法总数,并输出结果。
- 读取输入的
MOD = 1000000007
def q(n, m, dp):
if n < 1 or m < 1:
return 0
if n == 1 or m == 1:
return 1
if dp[n][m] != -1:
return dp[n][m]
if n < m:
dp[n][m] = q(n, n, dp)
elif n == m:
dp[n][m] = (q(n, m - 1, dp) + 1) % MOD
else:
dp[n][m] = (q(n, m - 1, dp) + q(n - m, m, dp)) % MOD
return dp[n][m]
if __name__ == "__main__":
n = int(input()) # 输入n
dp = [[-1 for _ in range(n + 1)] for _ in range(n + 1)] # 初始化dp数组
print(q(n, n, dp)) # 输出划分方法总数
4.汉诺塔
python代码:
-
hanoi函数:
- 该函数使用递归实现汉诺塔的移动。
- 基本思路是:
- 将
n-1个盘子从柱子A移动到C,借助B。 - 将第
n个盘子直接从A移动到B。 - 将
n-1个盘子从C移动到B,借助A。
- 将
- 每次移动盘子时,输出该盘子从哪个柱子移动到哪个柱子。
-
主程序部分:
- 从输入中读取盘子的数量
n。 - 设置三个柱子的名字
A、B、C,分别代表三个柱子。 - 调用
hanoi(n, A, B, C),按照上述步骤将n个盘子从A移动到B,借助柱子C。
- 从输入中读取盘子的数量
def hanoi(n, A, B, C):
if n >= 1:
hanoi(n - 1, A, C, B)
print(f"Move disk {n} from {A} to {B}")
hanoi(n - 1, C, B, A)
if __name__ == "__main__":
n = int(input()) # 读取盘子数量
A = 'A'
B = 'B'
C = 'C'
hanoi(n, A, B, C)
二、动态规划
1.矩阵连乘
Description
我们有一组矩阵链 A1,A2,...,AnA_1, A_2, ..., A_nA1,A2,...,An,矩阵的维数是已知的。我们的目标是找到一种加括号的方式,使得矩阵连乘的总运算次数(标量乘法次数)最小。
Input
共一行,包含一个整数 n 。
Output
共一行,包含一个整数,表示总划分数量。
由于答案可能很大,输出结果请对 10^9+7 取模。
Sample in 5 out 7
动态规划解法
-
定义问题: 对于矩阵链 A1,A2,...,AnA_1, A_2, ..., A_nA1,A2,...,An,找到最小的标量乘法次数。
-
状态定义: 定义一个二维数组
m[i][j],表示从矩阵 AiA_iAi 到 AjA_jAj 的最小乘法次数。 -
状态转移方程: 我们通过在 kkk 位置将矩阵链划分为两部分 Ai⋯AkA_i \cdots A_kAi⋯Ak 和 Ak+1⋯AjA_{k+1} \cdots A_jAk+1⋯Aj,然后递归地计算每个部分的最小乘法次数,再加上合并这两部分所需的乘法次数:
m[i][j]=min(m[i][k]+m[k+1][j]+p[i−1]×p[k]×p[j])m[i][j] = \min(m[i][k] + m[k+1][j] + p[i-1] \times p[k] \times p[j])m[i][j]=min(m[i][k]+m[k+1][j]+p[i−1]×p[k]×p[j])其中,ppp 是矩阵的维数数组。
-
初始化: 当 i=ji = ji=j 时,单个矩阵没有乘法操作,乘法次数为 0。
-
最终结果: 结果保存在
m[1][n]中,表示从第一个矩阵 A1A_1A1 到第 AnA_nAn 的最小乘法次数。def matrix_chain_order(p): n = len(p) - 1 # 矩阵个数为 n, 因为 p 数组长度为 n+1 # 初始化 m 数组,m[i][j] 表示矩阵 Ai...Aj 相乘的最小运算次数 m = [[0 for _ in range(n)] for _ in range(n)] # l 是链的长度,从 2 开始,表示至少有两个矩阵 for l in range(2, n + 1): # i 表示链的起始矩阵 for i in range(n - l + 1): # j 表示链的结束矩阵 j = i + l - 1 m[i][j] = float('inf') # 初始化为一个很大的数 # 在 k 处进行划分,寻找最小值 for k in range(i, j): # 计算当前划分的乘法次数 q = m[i][k] + m[k + 1][j] + p[i] * p[k + 1] * p[j + 1] # 如果 q 比当前的 m[i][j] 小,就更新 m[i][j] if q < m[i][j]: m[i][j] = q # 返回 m[0][n-1],即从矩阵 A1 到 An 的最小运算次数 return m[0][n - 1] # 输入处理 n = int(input()) # 输入矩阵个数 p = list(map(int, input().split())) # 输入 n+1 个数字,组成矩阵的维数 # 输出最小运算次数 print(matrix_chain_order(p))

607

被折叠的 条评论
为什么被折叠?



