算法设计与分析python

一、递归与分治

1.数组中的逆序对

Description

在数组中的两个整数,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

Input

共两行,第一行只有1个数,即数组中数字的个数n;第二行为数组的n个整数,以空格隔开,如:

3

1 2 3

Output

输出为1个数,即所给数组中逆序对的总数

python题解:

什么是逆序对?

逆序对是数组中两个数的组合,如果前面的数大于后面的数,就称为一个逆序对。例如,数组 [3, 2, 1] 中有以下逆序对:

  • (3, 2)
  • (3, 1)
  • (2, 1)

这些逆序对说明数组元素顺序相对于递增顺序有多少不对的地方。

分治法思想

这个问题可以用分治法解决。我们通过将数组递归地分成更小的部分,然后在合并的过程中统计逆序对。这种思想可以通过归并排序来实现。

代码解释:

  1. 定义全局变量 ans: 用来存储逆序对的总数。

  2. merge_sort 函数: 这个函数实现了递归的归并排序。在每次递归的时候,数组会被一分为二,直到每个部分只有一个元素。

    • mid = (l + r) // 2:找到数组的中间位置。
    • merge_sort(q, l, mid)merge_sort(q, mid + 1, r):递归地对左右两个部分进行排序。

    然后,在合并两个已经排好序的数组时,进行逆序对的计数:

    • 如果左边的元素大于右边的元素,说明左边的这个元素和右边的所有元素都形成了逆序对,因为左边部分已经是排序的。
  3. 合并两个有序数组

    • tmp 是一个临时数组,用于存储排序结果。
    • 两个指针 ij 分别指向左右两部分的起始位置。
    • 比较左右两部分的元素并进行排序,同时更新逆序对的数量。
  4. 返回结果: 在所有递归完成之后,逆序对的数量已经保存在 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)核心思路:

归并排序的基本思路是:

  • 将一个数组拆分为左右两个部分,分别进行排序;
  • 然后将两个有序的子数组合并成一个有序的大数组。

整个过程可以概括为以下三个步骤:

  1. 递归分解:将数组分为左右两部分,不断递归调用 merge_sort,直到每部分只有一个元素(因为一个元素的数组天然是有序的)。
  2. 合并:使用 merge 函数将左右两个有序子数组合并成一个有序数组。
  3. 递归回溯:合并完成后递归地回溯到上一层,直到整个数组被排序。

3. 代码结构:

  1. merge函数

    • 功能:合并两个有序的子数组片段,保证合并后的数组是有序的。
    • 合并步骤:
      • 初始化两个指针 ij 分别指向左右两个子数组的起始位置。
      • 比较 ij 所指向的元素,将较小的元素加入临时数组 temp
      • 当一个子数组的元素都合并到 temp 中后,将另一个子数组剩余的元素直接加入 temp
      • 最后将 temp 中的元素复制回原数组的相应位置。
  2. merge_sort函数

    • 功能:递归地将数组分成左右两部分,然后调用 merge 函数将两个有序部分合并。
    • 过程:
      • 如果 left < right,说明当前数组片段长度大于1,继续分割。
      • 计算中间位置 mid,分别对左右两个子数组递归调用 merge_sort 进行排序。
      • 调用 merge 函数将左右两个子数组合并。
  3. 输入输出处理

    • 主程序部分不断读取输入数据。每一组输入数据包含两个部分:第一个数字是数组长度 n,第二部分是 n 个需要排序的整数。
    • 使用 try-except 捕获 EOFError 来处理输入结束的情况。对于每一组数据,读取并排序后输出结果。

4. 细节说明:

  1. 递归分解
    递归时,merge_sort 函数将数组不断二分,每次递归将问题规模减小到一半。例如对于数组 [8, 3, 6, 4, 9],首先被分为 [8, 3, 6][4, 9],接着 [8, 3, 6] 继续被分为 [8, 3][6],以此类推,直到每个子数组只有一个元素。

  2. 合并过程
    当数组被分解到最小单位后,merge 函数开始执行。它将两个子数组中的元素从小到大排序,并将排序后的结果重新写入原数组。例如,子数组 [8][3] 合并后为 [3, 8],接着 [3, 8][6] 合并为 [3, 6, 8]

  3. 输入与输出
    每次读取一组数据后进行排序,然后将排序后的数组输出在同一行,以空格分隔。

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代码思路:

  1. 动态规划: 这道题利用了动态规划的思想。我们使用 dp[n][m] 来表示将整数 n 划分成最大加数不超过 m 的划分方法总数。

  2. 函数 q(n, m, dp)

    • 递归地计算划分方法。
    • n < 1m < 1 时,返回 0,表示没有有效划分。
    • n == 1m == 1 时,返回 1,因为只有一种划分方法。
    • 如果 n < m,则递归调用 q(n, n, dp),因为最大加数不能超过 n
    • 如果 n == m,则表示可以有一种划分为 n = n,加上其他的划分方法。
    • 如果 n > m,则有两种情况:
      1. 不使用 m,递归调用 q(n, m-1, dp)
      2. 使用 m,递归调用 q(n-m, m, dp)。 最后将两种情况的结果相加并对 MOD 取模。
  3. 主程序

    • 读取输入的 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代码:

  1. hanoi函数

    • 该函数使用递归实现汉诺塔的移动。
    • 基本思路是:
      1. n-1 个盘子从柱子 A 移动到 C,借助 B
      2. 将第 n 个盘子直接从 A 移动到 B
      3. n-1 个盘子从 C 移动到 B,借助 A
    • 每次移动盘子时,输出该盘子从哪个柱子移动到哪个柱子。
  2. 主程序部分

    • 从输入中读取盘子的数量 n
    • 设置三个柱子的名字 ABC,分别代表三个柱子。
    • 调用 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

动态规划解法

  1. 定义问题: 对于矩阵链 A1,A2,...,AnA_1, A_2, ..., A_nA1​,A2​,...,An​,找到最小的标量乘法次数。

  2. 状态定义: 定义一个二维数组 m[i][j],表示从矩阵 AiA_iAi​ 到 AjA_jAj​ 的最小乘法次数。

  3. 状态转移方程: 我们通过在 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 是矩阵的维数数组。

  4. 初始化: 当 i=ji = ji=j 时,单个矩阵没有乘法操作,乘法次数为 0。

  5. 最终结果: 结果保存在 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))
    

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值