常见算法思想5:分治法

分治法

分治算法采取了各个击破的方法,将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。我们只要求出子问题的解,就可得到原问题的解。

在编程过程中,我们经常遇到处理数据相当多、求解过程比较复杂、直接求解法会比较耗时的问题。在求解这类问题时,我们可以采用“各个击破”的方法。具体做法是先把这个问题分解成几个较小的子问题,找到求出这几个子问题的解法后,再找到合适的方法,把它们组合成求整个大问题的解法。如果这些子问题还是比较大,还可以继续再把它们分成几个更小的小子问题,依此类推,直至可以直接求出解为止。这就是分治策略的基本思想。

使用分治算法解题的一般步骤如下所示:
(1)分解,将要解决的问题划分成若干个规模较小的同类问题。
(2)求解,当子问题划分得足够小时,用较简单的方法解决。
(3)合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。

分治法所能解决的问题一般具有以下4个特征。

(1)当问题的规模缩小到一定的程度就可以容易地解决问题。此特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加的。
(2)问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。此特征是应用分治法的前提。它也是大多数问题可以满足的,此特征反映了递归思想的应用。
(3)利用该问题分解出的子问题的解可以合并为该问题的解;此特征最为关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪婪法(贪心法)或动态迭代法。
(4)该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。此特征涉及分治法的效率问题。如果各子问题是不独立的,则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态迭代法更好。

PS:值得注意的是,分治是解编程题常用的一种思想,而大多数分治思想都是用递归法来实现的。

分治算法机理

分治策略的思想起源于对问题解的特性所做出的这样的观察和判断:原问题可以被划分成k个子问题,然后用一种方法将这些子问题的解合并,合并的结果就是原问题的解。既然我们知道可以以某种方式构造出来,就没有必要(使用枚举回溯)进行大批量的搜索了。
枚举、回溯、分治算法利用了计算机工作的第一个特点:高速,不怕数据量大。
分治算法思想利用了计算机工作的第二个特点:重复。

方法实践

典型的分治法应用就是二分查找法,归并排序法等,这两种算法我们在排序算法篇和查找算法篇都讲过,可以去回顾一下,这里还有其他应用:

大数相乘:

步骤简介
Karatsuba算法主要应用于两个大数的相乘,原理是将大数分成两段后变成较小的数位,然后做3次乘法,并附带少量的加法操作和移位操作。
现有两个大数,x,y。
首先将x,y分别拆开成为两部分,可得x1,x0,y1,y0。他们的关系如下:
x = x1 * 10<sup>m</sup> + x0;
y = y1 * 10<sup>m</sup> + y0。其中m为正整数,m < n,且x0,y0 小于 10<sup>m</sup>。
那么 xy = (x1 * 10<sup>m</sup> + x0)(y1 * 10<sup>m</sup> + y0)
=z2 * 10<sup>2m</sup> + z1 * 10<sup>m</sup> + z0,其中:
z2 = x1 * y1;
z1 = x1 * y0 + x0 * y1;
z0 = x0 * y0。

此步骤共需4次乘法,但是由Karatsuba改进以后仅需要3次乘法。因为:
z1 = x1 * y0+ x0 * y1
z1 = (x1 + x0) * (y1 + y0) - x1 * y1 - x0 * y0,
故z1 便可以由一次乘法及加减法得到。
实例展示
设x = 12345,y=6789,令m=3。那么有:
12345 = 12 * 1000 + 345;
6789 = 6 * 1000 + 789。

下面计算:
z2 = 12 * 6 = 72;
z0 = 345 * 789 = 272205;
z1 = (12 + 345) * (6 + 789) - z2 - z0 = 11538。
然后我们按照移位公式(xy = z2 * 10^(2m) + z1 * 10^(m) + z0)可得:
xy = 72 * 1000<sup>2</sup> + 11538 * 1000 + 272205 = 83810205。
Go语言描述

Go的math/big包使用的大数相乘算法就是Karatsuba算法,有兴趣的可看看标准包源码:

// Fast version of z[0:n+n>>1].add(z[0:n+n>>1], x[0:n]) w/o bounds checks.
// Factored out for readability - do not use outside karatsuba.
func karatsubaAdd(z, x nat, n int) {
    if c := addVV(z[0:n], z, x); c != 0 {
        addVW(z[n:n+n>>1], z[n:], c)
    }
}

// Like karatsubaAdd, but does subtract.
func karatsubaSub(z, x nat, n int) {
    if c := subVV(z[0:n], z, x); c != 0 {
        subVW(z[n:n+n>>1], z[n:], c)
    }
}

// Operands that are shorter than karatsubaThreshold are multiplied using
// "grade school" multiplication; for longer operands the Karatsuba algorithm
// is used.
var karatsubaThreshold = 40 // computed by calibrate_test.go

// karatsuba multiplies x and y and leaves the result in z.
// Both x and y must have the same length n and n must be a
// power of 2. The result vector z must have len(z) >= 6*n.
// The (non-normalized) result is placed in z[0 : 2*n].
func karatsuba(z, x, y nat) {
    n := len(y)

    // Switch to basic multiplication if numbers are odd or small.
    // (n is always even if karatsubaThreshold is even, but be
    // conservative)
    if n&1 != 0 || n < karatsubaThreshold || n < 2 {
        basicMul(z, x, y)
        return
    }
    // n&1 == 0 && n >= karatsubaThreshold && n >= 2

    // Karatsuba multiplication is based on the observation that
    // for two numbers x and y with:
    //
    //   x = x1*b + x0
    //   y = y1*b + y0
    //
    // the product x*y can be obtained with 3 products z2, z1, z0
    // instead of 4:
    //
    //   x*y = x1*y1*b*b + (x1*y0 + x0*y1)*b + x0*y0
    //       =    z2*b*b +              z1*b +    z0
    //
    // with:
    //
    //   xd = x1 - x0
    //   yd = y0 - y1
    //
    //   z1 =      xd*yd                    + z2 + z0
    //      = (x1-x0)*(y0 - y1)             + z2 + z0
    //      = x1*y0 - x1*y1 - x0*y0 + x0*y1 + z2 + z0
    //      = x1*y0 -    z2 -    z0 + x0*y1 + z2 + z0
    //      = x1*y0                 + x0*y1

    // split x, y into "digits"
    n2 := n >> 1              // n2 >= 1
    x1, x0 := x[n2:], x[0:n2] // x = x1*b + y0
    y1, y0 := y[n2:], y[0:n2] // y = y1*b + y0

    // z is used for the result and temporary storage:
    //
    //   6*n     5*n     4*n     3*n     2*n     1*n     0*n
    // z = [z2 copy|z0 copy| xd*yd | yd:xd | x1*y1 | x0*y0 ]
    //
    // For each recursive call of karatsuba, an unused slice of
    // z is passed in that has (at least) half the length of the
    // caller's z.

    // compute z0 and z2 with the result "in place" in z
    karatsuba(z, x0, y0)     // z0 = x0*y0
    karatsuba(z[n:], x1, y1) // z2 = x1*y1

    // compute xd (or the negative value if underflow occurs)
    s := 1 // sign of product xd*yd
    xd := z[2*n : 2*n+n2]
    if subVV(xd, x1, x0) != 0 { // x1-x0
        s = -s
        subVV(xd, x0, x1) // x0-x1
    }

    // compute yd (or the negative value if underflow occurs)
    yd := z[2*n+n2 : 3*n]
    if subVV(yd, y0, y1) != 0 { // y0-y1
        s = -s
        subVV(yd, y1, y0) // y1-y0
    }

    // p = (x1-x0)*(y0-y1) == x1*y0 - x1*y1 - x0*y0 + x0*y1 for s > 0
    // p = (x0-x1)*(y0-y1) == x0*y0 - x0*y1 - x1*y0 + x1*y1 for s < 0
    p := z[n*3:]
    karatsuba(p, xd, yd)

    // save original z2:z0
    // (ok to use upper half of z since we're done recursing)
    r := z[n*4:]
    copy(r, z[:n*2])

    // add up all partial products
    //
    //   2*n     n     0
    // z = [ z2  | z0  ]
    //   +    [ z0  ]
    //   +    [ z2  ]
    //   +    [  p  ]
    //
    karatsubaAdd(z[n2:], r, n)
    karatsubaAdd(z[n2:], r[n:], n)
    if s > 0 {
        karatsubaAdd(z[n2:], p, n)
    } else {
        karatsubaSub(z[n2:], p, n)
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值