常见算法思想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)
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,928评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,748评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,282评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,065评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,101评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,855评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,521评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,414评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,931评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,053评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,191评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,873评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,529评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,074评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,188评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,491评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,173评论 2 357

推荐阅读更多精彩内容

  • 分治算法 一、基本概念 在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题...
    木叶秋声阅读 5,297评论 0 3
  • 分治算法 一、基本概念 在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题...
    Java资讯库阅读 9,772评论 0 14
  • 一、基本概念 在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或...
    扎Zn了老Fe阅读 770评论 0 1
  • https://www.cnblogs.com/steven_oyj/archive/2010/05/22/174...
    麒麟楚庄王阅读 573评论 0 0
  • 五大常用算法之一:分治算法 一、基本概念 在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就...
    鲍陈飞阅读 1,243评论 0 4