美文网首页数据结构与算法
常见算法思想5:分治法

常见算法思想5:分治法

作者: GoFuncChan | 来源:发表于2020-02-19 21:49 被阅读0次

    分治法

    分治算法采取了各个击破的方法,将一个规模为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)
        }
    }
    

    相关文章

      网友评论

        本文标题:常见算法思想5:分治法

        本文链接:https://www.haomeiwen.com/subject/qkfnfhtx.html