分治算法

本文深入探讨分治算法的核心概念,解析其在不同场景下的应用,包括二分查找、快速排序、归并排序等经典算法的实现与优化,以及如何通过分治策略解决复杂问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分治算法

1.分治法概念

  1. 将一个复杂的问题分成形式一致但规模较小的问题(可以递归分解,最后的子问题小到可以直接解决)----“分”
  2. 将最后子问题可以简单的直接求解----“治”
  3. 将所有子问题的解合并起来就是原问题打得解----“合”

2.分治法特征

  1. 该问题的规模缩小到一定的程度就可以容易地解决

  2. 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。

  3. 利用该问题分解出的子问题的解可以合并为该问题的解;

  4. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。

  5. 第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;

  6. 第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;

  7. 第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法

  8. 第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用自底向上的动态规划法较好。

3、为什么用分治法?怎么正确使用分治法?

  • 为什么用分治算法?我们使用一种算法的原因大部分情况下都是为了”快“,只有在少数情况下,在程序已经足够”快“的前提下,我们才会牺牲一部分的”快“,去保全一些开发因素(比如,程序的可维护性等等),那么分治算法为什么快?我们在用这个算法之前必需理解清楚这个问题。

  • 分治算法的思想就是将一个问题规模比较大的问题划分为几个相同逻辑性质(或者直接理解为类似)的问题规模变小的子问题。我们可以从这里入手。

    举个超级简单的例子:
  • 假如有一个存在n个元素的int型数组,我们需要求该数组的和。

  • 可能有些人想不想就是一个分治算法,将这个问题分为两个子问题,然后每个子问题再分为两个子问题,当子问题的规模为只有两个数时进行相加。

  • 然而,这种办法是使用了分治算法,可是效率比直接遍历一遍相加得到的效率还要低的多.

  • 为什么?因为分治算法本身不适合这种单次遍历就可以搞定的简单问题。你们在阅读一遍分治算法的思想:分治算法的思想就是将一个问题规模比较大的问题划分为几个相同逻辑性质的问题规模变小的子问题,那么这个定义存在一个隐含的前提,当问题规模比较大时,该问题解决起来要成倍的困难!

    我们可以举这样一个简单的例子:
    我们对一个存在n个元素的数组,使用简单排序进行排序时:
      当n=1时,无需比较
    
      当n=2时,我们需要1次比较
    
      当n=3时,我们需要3次比较
    
      当n=4时,我们需要6次比较
    
      当n的数值比较大时,我们需要比较的次数越来越多将会是一个巨大的数字。
    
      **而对于前面的求和的例子:**
    
      当n=1时,无需相加
    
      当n=2时,我们需要1次相加
    
      当n=3时,我们需要2次相加
    
      当n=4时,我们需要3次相加
    
      **仔细观察这组数据,是否发现了什么?**
    
  • 对于求和的例子来说,该问题的计算量与问题规模成正比,在相同的条件下,我们根本无须使用分治算法,因为即使这个问题规模变大,他的解决问题的难易程度没有丝毫改变,它所付出的,只不过是增大了问题规模后所必须付出的计算量,概括起来就是线性增长的问题规模导致了线性增长的计算量。

  • 而对于排序的例子,当问题规模变大时,计算量的增大是成幂次型增长的,概括起来就是线性增长的问题规模导致了幂次型计算量的增长。使得问题规模大的问题解决起来更加困难。

  • 综合起来概括,在问题规模与计算量成正比的算法中,分治算法不是最好的解法,并且有可能是效率极其底下的算法。如果存在某个问题,线性增长的问题规模可能带动计算量的非线性增长,并且符合分治算法的三个特征,那么分治算法是一个很不错的选择。

4.实践得真知

1.二分查找原始代码
def binary_search(li,val):
    left = 0
    right = len(li) - 1
    while left <= right:
        mid = (left + right) // 2
        if li[mid] == val:
            return True
        elif li[mid] < val:
            left = mid + 1
        else:
            right = mid - 1 
    return False

print(binary_search([1,2,3,4,5],4))
print(binary_search([1,2,3,4,5],6))
2.二分查找用分治算法来解决
  • 为什么能用分治:
  • 首先,我们知道要用二分查找,则原数据一定是排好序了。分而治之的思想,先看中间元素是否等于查找值,若等于,问题解决;若不等于,则查找左边,右边数据。将左右数据又看做一个排好序的数据,按同样方法判断中间值是否等与查找值。
  • 分解。将输入的序列 array[m … n] 按中间位置划成两个非空子序列 array[m … k] 和 array[k+1 … n]
  • 递归求解。通过递归调用二分查找算法分别对 array[m … k] 和 array[k+1 … n]进行查找元素。
  • 合并
def binary_search(li,val):
    left,right = 0, len(li) - 1
    if left > right:
        return False
    
    # 被分解的数组中值等于查找值,解决问题
    mid = (left + right) // 2
    if li[mid] ==val:
        return True
    # 递归求解即分治
    # 合并(选择一个else输出)
    elif li[mid] < val:
        return binary_search(li[mid+1:], val)
    else:
        return binary_search(li[:mid-1], val)
    
print(binary_search([1,2,3,4,5],4))
print(binary_search([1,2,3,4,5],6))
3.快速排序
  • 其原理如下: 对于一字给定的记录,通过一趟排序后,将原序列分为两部分,其中前一部分的所有记录均比后一部分的所有记录小,然后再一次对前后两部分的记录进行快速排序,递归该过程,指导序列中所有记录均有序为止。

  • 具体而言,算法步骤如下:

    1) 分解。将输入的序列 array[m … n] 划成两个非孔子序列 array[m … k] 和 array[k+1 … n],使 array[m … k] 中任一元素的值不大于 array[k + 1 … n]中任一元素的值。

    2)递归求解。通过递归调用快速排序算法分别对 array[m … k] 和 array[k+1 … n]进行排序。

    3)合并。由于对分解出的两个子序列的排序是就地进行的,所以在 array[m … k] 和 array[k+1 … n] 都排好序后不需要执行任何计算 array[m … n]就已排好序。

以数组 {49,38,65,97,76,13,27,49}为例,整个排序过程如下所示:

1551536982419

def quick_sort(li):
    # 被分解的Nums小于1则解决了
    if len(li) <= 1:
        return li
    
    # 分解    
    temp = li[0]
    left = [i for i in li[1:] if i < temp]
    right = [i for i in li[1:] if i >= temp]
    
    # 递归求解即分治
    left = quick_sort(left)
    right = quick_sort(right)
    # 合并
    return left + [temp] + right

print(quick_sort([2,3,4,6,5,7,3]))
只选择不重复元素进行快速排序
def quick_sort(li):
    if len(li) <= 1:
        return li
    mid = li[0]
    left = [x for x in li[1:] if x < mid]
    right = [x for x in li[1:] if x > mid]
    
    left = quick_sort(left)
    right = quick_sort(right)
    
    return left + [mid] + right
print(quick_sort([2,4,5,6,3,3,2]))
原序列:【1,2,3,4,3,3】
排序后:【1,2,3,4】(去重复值)
4.归并排序
  • 归并排序是利用递归与分治技术将数据序列分成为越来越小的半子表,再对半子表排序,最后再用递归方法将排好序的半子表合并成为越来越大的有序序列。
  • 所以,**归并排序的关键是两步:第一步,划分半子表;第二步,合并半子表。**以数组 {49,38,65,97,76,13,27} 为例,归并排序的具体步骤如下:

1551536949485

def merge_sort(li):
	# 如果问题规模小于等于1,直接解决
	if len(li) <= 1:
		return li
    # 分解
    mid = (left + right) // 2
    left, right = li[:mid], li[mid:]
    
    # 递归求解即分治
    # 大问题只有规模大于1时才划分为子问题
    if len(left) > 1:
    	left = merge_sort(left)
    if len(right) > 1:
        right = merge_sort(right)
       
    # 合并
    res = []
    while left and right:
        if left[-1] >= right[-1]:
            res.append(left.pop())
        else:
            res.append(right.pop())
    # 倒序
    res.reverse()
    
    # 将剩余的元素组合进来
    return (left or right) + res
        
5.最大子序列和
  • 最大连续子序列是所有连续子序中元素和最大的一个,例如给定序列{ -2, 11, -4, 13, -5, -2 },其最大连续子序列为{11,-4,13},最大连续子序列和即为20。

  • 问题解决思路:

    现将序列等分为左右两份,则最大子列只可能出现在三个地方:

    1整个子序列出现在左半部分

    2整个子序列出现在右半部分

    3整个子序列跨越中间边界

    前两种情况可以用递归求解,而第三种情况则可以将前半部分的最大子序列和(此处的子序列必须包含前半部分的最后一个元素)与后半部分的最大子序列和(此处的子序列必须包含后半部分的第一个元素)相加得到

    **注1:**因为第三种情况跨越了中间边界,且要求的序列为连续的,因此第三种情况得到的子序列必定包含左子序列的最后一个元素以及右子序列的第一个元素。

    **注2:**若要求的序列可以为不连续的,则第三种情况可以直接用前半部分最大子序列和与后半部分最大子序列和相加得到

    分治法解决 连续 子序列 和最大问题
# 注1:因为第三种情况跨越了中间边界,且要求的序列为连续的,因此第三种情况得到的子序列必定包含左子序列
# 的最后一个元素以及右子序列的第一个元素。
def findMaxSum(li):
    # 如果问题规模小于等于1,直接解决
    if len(li) <= 1:
        return li[0]
    
    # 分解
    mid = len(li) // 2
    left = li[:mid]
    right = li[mid:]
    
    # 递归求解即分治
    leftMaxSum = findMaxSum(left)
    rightMaxSum = findMaxSum(right)
    
    # 最大和由左右连续序列组合而成
    # 用于包含左边最后一个数的累加求和
    leftAndMid = 0
    # 考虑到存在序列全为负数的情况,因为初始化为负无穷而非0
    leftAndMidMax = float('-inf')#包含左边最后一个数的最大序列和
    for i in left[::-1]:
        leftAndMid += i
        if leftAndMid > leftAndMidMax:
            leftAndMidMax = leftAndMid
            
    # 用于包含右边第一个数的累加求和
    rightAndMid = 0
    # 考虑到存在序列全为负数的情况,因为初始化为负无穷而非0
    rightAndMidMax = float('-inf')#包含右边最后一个数的最大序列和
    for i in right:
        rightAndMid += i
        if rightAndMid > rightAndMidMax:
            rightAndMidMax = rightAndMid        
            
    # 计算跨越了中间的序列 的最大和
    midMaxSum = leftAndMidMax + rightAndMidMax
    
    # 合并
    return max(leftMaxSum, midMaxSum, rightMaxSum)

A=[2,3,4,1,-1,-7,-3,-7,-6]
 
print(findMaxSum(A))



动态规划解决 连续 子序列 和最大:
def findMaxSum(li):
    # initMax 用于累加求和开始
    initMax = 0
    MaxSum = float("-inf")
    for i in li:
        initMax += i
        if i > initMax:
            initMax = i
        if initMax > MaxSum:
            MaxSum = initMax
    return MaxSum

A=[1,-2,-2,4]
 
print(findMaxSum(A))    
分治法解决非连续 子序列 和最大问题
def findMaxSum(li):
    # 如果问题规模小于等于1,直接解决
    if len(li) <= 1:
        return li[0]
    
    # 分解
    mid = len(li) // 2
    left = li[:mid]
    right = li[mid:]
    
    # 递归求解即分治
    leftMaxSum = findMaxSum(left)
    rightMaxSum = findMaxSum(right)
    
    # 合并
    midMaxSum = leftMaxSum + rightMaxSum
    return max(leftMaxSum, midMaxSum, rightMaxSum)

A=[-2,-3,-4,-1,-1,-7,-3,-7,-6]
 
print(findMaxSum(A))
动态规划解决非连续 子序列 和最大问题
def findMaxSum(li):
    # initMax 用于累加求和开始
    initMax = 0
    MaxSum = float("-inf")
    for i in li:
        if i < initMax:
            continue
        initMax += i
        if i > initMax:
            initMax = i
        if initMax > MaxSum:
            MaxSum = initMax
    return MaxSum

A = [-1, 2, -2,-3,-5,9]

print(findMaxSum(A))
6.给定一个顺序表,用分治法求出最大值
def find_max(li):
    # 如果问题规模小于等于2,可以直接求解
    if len(li) <= 2:
        return max(li)
    
    # 分解大问题 为 小问题
    mid = len(li) // 2
    left, right = li[:mid], li[mid:]
    
    # 递归求解即分治
    left = find_max(left)
    right = find_max(right)
    
    # 合并
    return max(left, right)

alist = [12,2,23,45,67,3,2,4,45,63,24,23]
print(find_max(alist))  # 67
7.给定一个顺序表,用分治法判断某个元素是否在其中
def find_num(li,num):
    # 如果问题规模小于等于1,则直接解决
    if len(li) == 1:
        if li[0] == num:
            return True
        return False
    
    # 分解大问题为小问题
    mid = len(li) // 2
    left, right = li[:mid], li[mid:]
    
    # 递归求解即分治  然后合并
    left = find_num(left, num)
    if left == True:
        return True
    right = find_num(right, num)
    
    return right
lis = [12,2,23,45,67,3,2,4,45,63,24,23]
8.对于无序顺序表,查找第K(K有效)大的数(1,2,3,4,5): 第一大:5 第二大:4 第三大:3
def find_k_max(li,k):
    # 如果问题规模小于等于1,直接解决
    # 因为K有效,所以此时K为1
    if len(li) == 1:
        return li[0]
    
    # 分解大问题为小问题
    # 这儿利用的快速排序思想,将列表以第一个元素为界限,分开
    # left左边的元素都小于temp,右边的元素都大于temp
    # 因为是查找第K大,所以right右边的个数等于k-1,则中间位置就是第k大
    temp = li[0]
    # 这儿需要注意,=号应该出现在left或者right里面,不能两个都不出现,否则快速排序就会将重复值丢掉,	  # 如【1,1,3,4】,分割后会变成 【1,3,4】
    # 不过基于这种意外出现的情况,小伙伴以后可以把不要=(等号)的快速排序作为,带有重复元素序列,但只	  # 对不重复元素进行排序,相当方便
    
    left = [i for i in li[1:] if i <= temp]
    right = [i for i in li[1:] if i > temp]
    n = len(right)
    if n == k-1:
        return temp
        
    # 递归求解即分治  然后合并(用else选择一个输出)
    elif n < k:
        return find_k_max(left,k-n-1)
    else:
        return find_k_max(right,k)

if __name__ == '__main__':
    lis = [3, 4, 5,3,4,5,6]
    print(find_k_max(lis,3))
9.对于无序顺序表,查找第K(K有效)小的数(1,2,3,4,5): 第一大:1 第二大:2 第三大:3
def find_k_min(li,k):
    # 如果问题规模小于等于1,直接解决
    # 因为K有效,所以此时K为1
    if len(li) == 1:
        return li[0]
    
    # 分解大问题为小问题
    temp = li[0]
    left = [i for i in li[1:] if i <= temp]
    right = [i for i in li[1:] if i > temp]
    n = len(left)
    if n == k-1:
        return temp
        
    # 递归求解即分治  然后合并(用else选择一个输出)
    elif n < k:
        return find_k_min(right,k-n-1)
    else:
        return find_k_min(right,k)

if __name__ == '__main__':
    lis = [3, 4, 5,3,4,5,6]
    print(find_k_min(lis,3))
10.汉诺塔
def hanoi(n,a,temp,c):
    '''
    n:总共有多少个圆盘
    a:起始柱子
    temp:中间临时柱子
    c:目标柱子
    '''
    # 如果问题规模小于等于1,则直接解决
    if n == 1:
        print("%s --> %s" % (a,c))
    
    # 分解大问题为小问题
    # 递归求解即分治
    else:
        hanoi(n-1,a,c,temp)
        hanoi(1,a,temp,c)
        hanoi(n-1,temp,a,c)

hanoi(3,'A','B','C')
11.给出无序列表,找出最大值
def find_Max(init_list):
    n = len(init_list)
    if n <= 2: # 若问题规模小于等于 2,解决
        return max(init_list)

    # 分解(子问题规模为 n/2)
    left_list, right_list = init_list[:n//2], init_list[n//2:]
    
    # 递归(树),分治
    left_max, right_max = fnd_Max(left_list), find_Max(right_list)
    
    # 合并
    return max([left_max, right_max])

if __name__ == "__main__":
    # 测试数据
    test_list = [12,2,23,45,67,3,2,4,45,63,24,23]
    # 求最大值
    print(find_Max(test_list))  # 67
12.爬楼梯,每次只能爬一节,或者2节,求总共为n节的楼梯,有多少种爬法?
def Sum(n):
    # 如果问题规模小于等于2,则直接解决
	if n<=2:
        return n
    # 因为最后一次只能爬一节,或者2节,所以总数应该等于最后一次按一节爬,加上,最后一次按照2节爬
    return Sum(n-1) + Sum(n-2)
print(Sum(10))
13.从数组 li 中找出和为 s 的数值组合,有多少种可能
def find_sum(li, sum):
    # 如果问题规模小于等于1,则直接解决
    if len(li) == 1:
        return [0,1][li[0] == sum]
    
    # 分解
    if li[0] == sum:
        return 1 + find_sum(li[1:],sum)
    else:
        return find_sum(li[1:], sum-li[0]) + find_sum(li[1:], sum)
def find2(seq, s, tmp=''):
    if len(seq)==0:   # 终止条件
        return
    
    if seq[0] == s:               # 找到一种,则
        print(tmp + str(seq[0]))  # 打印
    
    find2(seq[1:], s, tmp)                              # 尾递归 ---不含 seq[0] 的情况
    find2(seq[1:], s-seq[0], str(seq[0]) + '+' + tmp)   # 尾递归 ---含 seq[0] 的情况

# 测试
seq = [1, 2, 3, 4, 5, 6, 7, 8, 9]
s = 14 # 和
find2(seq, s)
print()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值