# 木块砌墙
作者:July、caopengcs、红色标记。致谢:fuwutu、demo。
时间:二零一三年八月十二日
__题目__:用 1×1×1, 1×2×1以及2×1×1的三种木块(横绿竖蓝,且绿蓝长度均为2),
![](../images/32~33/33.1.png)
搭建高长宽分别为K × 2^N × 1的墙,不能翻转、旋转(其中,0<=N<=1024,1<=K<=4)
![](../images/32~33/33.2.png)
有多少种方案,输出结果
对1000000007取模。
举个例子如给定高度和长度:N=1 K=2,则答案是7,即有7种搭法,如下图所示:
![](../images/32~33/33.3.png)
__详解__:此题很有意思,涉及的知识点也比较多,包括动态规划,快速矩阵幂,状态压缩,排列组合等等都一一考察了个遍。而且跟一个比较经典的矩阵乘法问题类似:即用1 x 2的多米诺骨牌填满M x N的矩形有多少种方案,M<=5,N<2^31,输出答案mod p的结果
![](../images/32~33/33.4.gif)
OK,回到正题。下文使用的图示说明(所有看到的都是横切面):
![](../images/32~33/33.5.jpg)
首先说明“?方块”的作用
![](../images/32~33/33.6.jpg)
“?方块”,表示这个位置是空位置,可以任意摆放。
上图的意思就是,当右上角被绿色木块占用,此位置固定不变,其他位置任意摆放,在这种情况下的堆放方案数。
###解法一、穷举遍历
初看此题,你可能最先想到的思路便是穷举:用二维数组模拟墙,从左下角开始摆放,从左往右,从下往上,最后一个格子是右上角那个位置;每个格子把每种可以摆放木块都摆放一次,每堆满一次算一种用摆放方法。为了便于描述,为木块的每个格子进行编号:
![](../images/32~33/33.7.jpg)
下面演示当n=1,k=2的算法过程(7种情况):
![](../images/32~33/33.8.jpg)
穷举遍历在数据规模比较小的情况下还撑得住,但在0<=N<=1024这样的数据规模下,此方法则立刻变得有心无力,因此我们得寻找更优化的解法。
###解法二、递归分解
递归求解就是把一个大问题,分解成小问题,逐个求解,然后再解决大问题。
####2.1、算法演示
假如有墙规模为(n,k),如果从中间切开,被分为规模问(n-1,k)的两堵墙,那么被分开的墙和原墙有什么关系呢?我们首先来看一下几组演示。
#####2.1.1、n=1,k=2的情况
首先演示,__n=1,k=2__时的情况,如下图2-1:
![](../images/32~33/33.9.jpg)
图 2-1
上图2-1中:
![](../images/32~33/33.10.jpg)
表示,左边墙的所有堆放方案数 * 右边墙所有堆放方案数 = 2 * 2 = 4
![](../images/32~33/33.11.jpg)
表示,当切开处有一个横条的时候,空位置存在的堆放方案数。左边*右边 = 1*1 = 2;剩余两组以此类推。
这个是排列组合的知识。
#####2.1.2、n=2,k=3的情况
其次,我们再来演示下面更具一般性的计算分解,即当__n=2,k=3__的情况,如下图2-2:
![](../images/32~33/33.12.jpg)
图 2-2
再从分解的结果中,挑选一组进行分解演示:
![](../images/32~33/33.13.jpg)
图 2-3
通过图2-2和图2-3的分解演示,可以说明,最终都是分解成一列求解。在逐级向上汇总。
#####2.1.3、n=4,k=3的情况
我们再假设一堵墙n=4,k=3,也就是说,宽度是16,高度是3时,会有以下分解:
![](../images/32~33/33.14.jpg)
图2-4
根据上面的分解的一个中间结果,再进行分解,如下:
![](../images/32~33/33.15.jpg)
图2-5
通过上面图2-1~图2-5的演示可以明确如下几点:
1.假设f(n)用于计算问题,那么f(n)依赖于f(n-1)的多种情况。
2.切开处有什么特殊的地方呢?通过上面的演示,我们得知被切开的两堵墙从没有互相嵌入的木块(绿色木块)到全是互相连接的木块,相当于切口绿色木块的全排列(即有绿色或者没有绿色的所有排列),即有2^k种状态(比如k=2,且有绿色用1表示,没有绿色用0表示,那么就有00、01、10、11这4种状态)。根据排列组合的性质,把每一种状态下左右木墙堆放方案数相乘,再把所有乘积求和,就得到木墙的堆放结果数。以此类推,将问题逐步往下分解即可。
3.此外,从图2-5中可以看出,除了需要考虑切口绿色木块的状态,还需要考虑最左边一列和最右边一列的绿色木块状态。我们把这两种边界状态称为左边界状态和右边界状态,分别用leftState和rightState表示。
且在观察图2-5被切分后,所有左边的墙,他们的左边界ls状态始终保持不变,右边界rs状态从0~maxState, maxState = 2^k-1(有绿色方块表示1,没有表示0;ls表示左边界状态,rs表示右边界状态):
![](../images/32~33/33.16.jpg)
图2-6
同样可以看出右边的墙的右边界状态保持不变,而左边界状态从0~maxState。要堆砌的木墙可以看做是左边界状态=0,和右边界状态=0的一堵墙。
有一点可能要特别说明下,即上文中说,有绿色方块的状态表示标为1,无绿色方块的状态表示标为0,特意又拿上图2-6标记了一些数字,以让绝大部分读者能看得一目了然,如下所示:
![](../images/32~33/33.17.jpg)
图2-7
这下,你应该很清楚的看到,在上图中,左边木块的状态表示一律为010,右边木块的状态表示则是000~111(即从下至上开始计数,右边木块rs的状态用二进制表示为:000 001 010 011 100 101 110 111,它们各自分别对应整数则是:0 1 2 3 4 5 6 7)。
####2.2、计算公式
通过图2-4、图2-5、图2-6的分解过程,我们可以总结出下面公式(leftState=最左边边界状态,rightState=最右边边界状态):
![](../images/32~33/33.18.jpg)
即:
![](../images/32~33/33.19.jpg)
接下来,分3点解释下上述公式:
__1__、上述函数返回结果是当左边状态为=leftState,右边状态=rightState时木墙的堆砌方案数,相当于直接分解的左右状态都为0的情况,即直接分解f(n,k,0,0)即可。看到这,读者可能便有疑问了,既然直接分解f(n,k,0,0)即可,为何还要加leftstate和leftstate两个变量呢?回顾下2.1.3节中n=4,k=3的演示例子,即当n=4,k=3时,其分解过程即如下图(上文2.1.3节中的图2-4)
![](../images/32~33/33.20.jpg)
也就是说,刚开始直接分解f(4,3,0,0),即n=4,k=3,leftstate=0,rightstate=0,但分解过程中leftstate和rightstate皆从0变化到了maxstate,故才让函数的第3和第4个参数采用leftstate和rightstate这两个变量的形式,公式也就理所当然的写成了f(n,k,leftstate,rightstate)。
__2__、然后我们再看下当n=4,k=3分解的一个中间结果,即给定如上图最下面部分中红色框框所框住的木块时:
![](../images/32~33/33.21.jpg)
它用方程表示即为 f(2,3,2,5),怎么得来的呢?其实还是又回到了上文2.1.3节中,当n=2,k=3 时(下图即为上文2.1.3节中的图2-5和图2-6)
![](../images/32~33/33.22.jpg)
![](../images/32~33/33.23.jpg)
左边界ls状态始终保持不变时,右边界rs状态从0~maxState;右边界状态保持不变时,而左边界状态从0~maxState。
故上述分解过程用方程式可表示为:
__f(2,3,2,5) = f(1,3,2,0) * f(1,3,0,5)__
__+ f(1,3,2,1) * f(1,3,1,5)__
__+ f(1,3,2,2) * f(1,3,2,5)__
__+ f(1,3,2,3) * f(1,3,3,5)__
__+ f(1,3,2,4) * f(1,3,4,5)__
__+ f(1,3,2,5) * f(1,3,5,5)__
__+ f(1,3,2,6) * f(1,3,6,5)__
__+ f(1,3,2,7) * f(1,3,7,5)__
说白了,我们曾在2.1节中从图2-2到图2-6正推推导出了公式,然上述过程中,则又再倒推推了一遍公式进行了说明。
__3__、最后,作者是怎么想到引入 leftstate 和rightstate 这两个变量的呢?如红色标记所说:"因为切开后,发现绿色条,在分开处不断的变化,当时也进入了死胡同,我就在想,蓝色的怎么办。后来才想明白,与蓝色无关。每一种变化就是一种状态,所以就想到了引入leftstate 和rightstate这两个变量。"
####2.3、参考代码
下面代码就是根据上面函数原理编写的。最终执行效率,n=1024,k=4 时,用时0.2800160秒(之前代码用的是字典作为缓存,用时在1.3秒左右,后来改为数组结果,性能大增)。""
```cs
//copyright@红色标记 12/8/2013
//updated@July 13/8/2013
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
namespace HeapBlock
{
public class WoolWall
{
private int n;
private int height;
private int maxState;
private int[, ,] resultCache; //结果缓存数组
public WoolWall(int n, int height)
{
this.n = n;
this.height = height;
maxState = (1 << height) - 1;
resultCache = new int[n + 1, maxState + 1, maxState + 1]; //构建缓存数组,每个值默认为0;
}
/// <summary>
/// 静态入口。计算堆放方案数。
/// </summary>
/// <param name="n"></param>
/// <param name="k"></param>
/// <returns></returns>
public static int Heap(int n, int k)
{
return new WoolWall(n, k).Heap();
}
/// <summary>
/// 计算堆放方案数。
/// </summary>
/// <returns></returns>
public int Heap()
{
return (int)Heap(n, 0, 0);
}
private long Heap(int n, int lState, int rState)
{
//如果缓存数组中的值不为0,则表示该结果已经存在缓存中。
//直接返回缓存结果。
if (resultCache[n, lState, rState] != 0)
{
return resultCache[n, lState, rState];
}
//在只有一列的情况,无法再进行切分
//根据列状态计算一列的堆放方案
if (n == 0)
{
return CalcOneColumnHeapCount(lState);
}
long result = 0;
for (int state = 0; state <= maxState; state++)
{
if (n == 1)
{
//在只有两列的情况,判断当前状态在切分之后是否有效
if (!StateIsAvailable(n, lState, rState, state))
{
continue;
}
result += Heap(n - 1, state | lState, state | lState) //合并状态。因为只有一列,所以lState和rState相同。
* Heap(n - 1, state | rState, state | rState);
}
else
{
result += Heap(n - 1, lState, state) * Heap(n - 1, state, rState);
}
result %= 1000000007;//为了防止结果溢出,根据题目要求求模。
}
resultCache[n, lState, rState] = (int)result; //将结果写入缓存数组中
resultCache[n, rState, lState] = (int)result; //对称的墙结果相同,所以直接写入缓存。
return result;
}
/// <summary>
/// 根据一列的状态,计算列的堆放方案数。
/// </summary>
/// <param name="state">状态</param>
/// <returns></returns>
private int CalcOneColumnHeapCount(int state)
{
int sn = 0; //连续计数
int result = 1;
for (int i = 0; i < height; i++)
{
if ((state & 1) == 0)
{
sn++;
}
else
{
if (sn > 0)
{
result *= CalcAllState(sn);
}
sn = 0;
}
state >>= 1;
}
if (sn > 0)
{
result *= CalcAllState(sn);
}
return result;
}
/// <summary>
/// 类似于斐波那契序列。
/// f(1)=1
/// f(2)=2
/// f(n) = f(n-1)*f(n-2);
/// 只是初始值不同。
/// </summary>
/// <param name="k"></param>
/// <returns></returns>
private static int CalcAllState(int k)
{
return k <= 2 ? k : CalcAllState(k - 1) + CalcAllState(k - 2);
}
/// <summary>
/// 判断状态是否可用。
/// 当n=1时,分割之后,左墙和右边墙只有一列。
/// 所以state的状态码可能会覆盖原来的边缘状态。
/// 如果有覆盖,则该状态不可用;没有覆盖则可用。
/// 当n>1时,不存在这种情况,都返回状态可用。
/// </summary>
/// <param name="n"></param>
/// <param name="lState">左边界状态</param>
/// <param name="rState">右边界状态</param>
/// <param name="state">切开位置的当前状态</param>
/// <returns>状态有效返回 true,状态不可用返回 false</returns>
private bool StateIsAvailable(int n, int lState, int rState, int state)
{
return (n > 1) || ((lState | state) == lState + state && (rState | state) == rState + state);
}
}
}
```
上述程序中,
* WoolWall.Heap(1024,4); //直接通过静态方法获得结果
* new WoolWall(n, k).Heap();//通过构造对象获得结果
#####2.3.1、核心算法讲解
因为它最终都是分解成一列的情况进行处理,这就会导致很慢。为了提高速度,本文使用了缓存机制来提高性能。缓存原理就是,n,k,leftState,rightState相同的墙,返回的结果肯定相同。利用这个特性,每计算一种结果就放入到缓存中,如果下次计算直接从缓存取出。刚开始缓存用字典类实现,有网友给出了更好的缓存方法——数组。这样性能好了很多,也更加简单。程序结构如下图所示:
![](../images/32~33/33.24.jpg)
上图反应了Heap调用的主要方法调用,在循环中,result 累加 lResult 和 rResult。
①在实际代码中,首先是从缓存中读取结果,如果没有从缓存中读取结果再进行计算。
分解到一列时,不再分解,直接计算结果
```cs
if (n == 0)
{
return CalcOneColumnHeap(lState);
}
```
②下面是整个程序的核心代码,通过for循环,求和state=0到state=2^k-1的两边木墙乘积:
```cs
for (int state = 0; state <= maxState; state++)
{
if (n == 1)
{
if (!StateIsAvailable(n, lState, rState, state))
{
continue;
}
result += Heap(n - 1, state | lState, state | lState) *
Heap(n - 1, state | rState, state | rState);
}
else
{
result += Heap(n - 1, lState, state)
* Heap(n - 1, state, rState);
}
result %= 1000000007;
}
```
当n=1切分时,需要特殊考虑。如下图:
![](../images/32~33/33.25.jpg)
图2-8
看上图中,因为左边墙中间被绿色方块占用,所以在(1,0)-(1,1)这个位置(位置的标记方法同解法一)不能再放绿色方块。所以一些状态需要排除,如state=2需要排除。同时在还需要合并状态,如state=1时,左边墙的状态=3。
特别说明下:依据我们上文2.2节中的公式,如果第i行有这种木块,state对应2^(i-1),加上所有行的贡献就得到state(0就是没有这种横跨木块,2^k-1就是所有行都是横跨木块),然后遍历state,还记得上文中的图2-7么?
![](../images/32~33/33.26.jpg)
当第i行被这样的木块![](../images/32~33/33.27.jpg)或这样的木块![](../images/32~33/33.28.jpg)占据时,其各自对应的state值分别为:
1.当第1行被占据,state=1;
2.当第2行被占据,state=2;
3.当第1和第2行都被占据,state=3;
4.当第3行被占据,state=4;
5.当第1和第3行被占据,state=5;
6.当第2和第3行被占据,state=6;
7.当第1、2、3行全部都被占据,state=7。
至于原因,即如2.1.3节节末所说:二进制表示为:000 001 010 011 100 101 110 111,它们各自分别对应整数则是:0 1 2 3 4 5 6 7。
具体来说,下面图中所有框出来的位置,不能有绿色的:
![](../images/32~33/33.29.jpg)
③CalcOneColumnHeap(int state)函数用于计算一列时摆放方案数。
计算方法是, 求和被绿色木块分割开的每一段连续方格的摆放方案数。每一段连续的方格的摆放方案通过CalcAllState方法求得。经过分析,可以得知CalcAllState是类似斐波那契序列的函数。
举个例子如下(分步骤讲述):
1.令state = 4546(state=2^k-1,k最大为4,故本题中state最大在15,而这里取state=4546只是为了演示如何计算),二进制是:1000111000010。位置上为1,表示被绿色木块占用,0表示空着,可以自由摆放。
2.1000111000010 被分割后 1 000 111 0000 1 0, 那么就有 000=3个连续位置, 0000=4个连续位置 , 0=1个连续位置。
3.堆放结果=CalcAllState(3) + CalcAllState(4) + CalcAllState(1) = 3 + 5 + 1 = 9。
####2.4、再次优化
上面程序因为调用性能的树形结构,形成了大量的函数调用和缓存查找,所以其性能不是很高。 为了得到更高的性能,可以让所有的运算直接依赖于上一次运算的结果,以防止更多的调用。即如果每次运算都算出所有边界状态的结果,那么就能为下一次运算提供足够的信息。后续优化请[查阅此文第3节](http://blog.csdn.net/dw14132124/article/details/9038417#t2)。
###解法三、动态规划
相信读到上文,不少读者都已经意识到这个问题其实就是一个动态规划问题,接下来咱们换一个角度来分析此问题。
####3.1、暴力搜索不可行
首先,因为木块的宽度都是1,我们可以想成2维的问题。也就是说三种木板的规格分别为1* 1, 1 * 2, 2 * 1。
通过上文的解法一,我们已经知道这个问题最直接的想法就是暴力搜索,即对每个空格尝试放置哪种木板。但是看看数据规模就知道,这种思路是不可行的。因为有一条边范围长度高达2^1024,普通的电脑,2^30左右就到极限了。于是我们得想想别的方法。
####3.2、另辟蹊径
为了方便,我们把墙看做有2^n行,k列的矩形。这是因为虽然矩形木块不能翻转,但是我们同时拥有1*2和2*1的两种木块。
假设我们从上到下,从左到右考虑每个1*1的格子是如何被覆盖的。显然,我们每个格子都要被覆盖住。木块的特点决定了我们覆盖一个格子最多只会影响到下一行的格子。这就可以让我们暂时只考虑两行。
假设现我们已经完全覆盖了前(i–1)行。那么由于覆盖前(i-1)行导致第i行也不“完整”了。如下图:
xxxxxxxxx
ooxooxoxo
我们用x表示已经覆盖的格子,o表示没覆盖的格子。为了方便,我们使用9列。
我们考虑第i行的状态,上图中,第1列我们可以用1*1的覆盖掉,也可以用1*2的覆盖前两列。第4、5列的覆盖方式和第1、2列是同样的情况。第7列需要覆盖也有两种方式,即用1*1的覆盖或者用2*1的覆盖,但是这样会导致第(i+1)行第7列也被覆盖。第9列和第7列的情况是一样的。这样把第i行覆盖满了之后,我们再根据第(i+1)行被影响的状态对下一行进行覆盖。
那么每行有多少种状态呢?显然有2^k,由于k很小,我们只有大约16种状态。如果我们对于这些状态之间的转换制作一个矩阵,矩阵的第i行第j列的数表示的是我们第m行是状态i,我们把它完整覆盖掉,并且使得第(m + 1)行变成状态j的可能的方法数,这个矩阵我们可以暴力搜索出来,搜索的方式就是枚举第m行的状态,然后尝试放木板,用所有的方法把第m行覆盖掉之后,下一行的状态。当然,我们也可以认为只有两行,并且第一行是2k种状态的一种,第二行起初是空白的,求使得第一行完全覆盖掉,第二行的状态有多少种类型以及每种出现多少次。
####3.3、动态规划
这个矩阵作用很大,其实我们覆盖的过程可以认为是这样:第一行是空的,我们看看把它覆盖了,第2行是什么样子的。根据第二行的状态,我们把它覆盖掉,看看第3行是什么样子的。
如果我们知道第i行的状态为s,怎么考虑第i行完全覆盖后,第(i+1)行的状态?那只要看那个矩阵的状态s对应的行就可以了。我们可以考虑一下,把两个这样的方阵相乘得到得结果是什么。这个方阵的第i行第j个元素是这样得到的,是第i行第k个元素与第k行第j个元素的对k的叠加。它的意义是上一行是第m行是状态i,把第m行和第(m+1)行同时覆盖住,第(m+2)行的状态是j的方法数。这是因为中间第(m+1)行的所有状态k,我们已经完全遍历了。
于是我们发现,每做一次方阵的乘法,我们相当于把状态推动了一行。那么我们要坐多少次方阵乘法呢?就是题目中墙的长度2<sup>n</sup>,这个数太大了。但是事实上,我们可以不断地平方n次。也就是说我们可以算出A<sup>2</sup>,A<sup>4</sup>, A<sup>8</sup>, A<sup>16</sup>……方法就是不断用结果和自己相乘,这样乘n次就可以了。
因此,我们最关键的问题就是建立矩阵A。我们可以这样表示一行的状态,从左到右分别叫做第0列,第1列……覆盖了我们认为是1,没覆盖我们认为是0,这样一行的状态可以表示为一个整数。某一列的状态我们可以用位运算来表示。例如,状态x第i列是否被覆盖,我们只需要判断x & (1 << i) 是否非0即可,或者判断(x >> i) & 1, 用右移位的目的是防止溢出,但是本题不需要考虑溢出,因为k很小。 接下来的任务就是递归尝试放置方案了
####3.4、参考代码
最终结果,我们最初的行是空得,要求最后一行之后也不能被覆盖,所以最终结果是矩阵的第[0][0]位置的元素。另外,本题在乘法过程中会超出32位int的表示范围,需要临时用C/C++的long long,或者java的long。
参考代码如下:
```c
//copyright@caopengcs 12/08/2013
#ifdef WIN32
#define ll __int64
#else
#define ll long long
#endif
// 1 covered 0 uncovered
void cal(int a[6][32][32],int n,int col,int laststate,int nowstate)
{
if (col >= n)
{
++a[n][laststate][nowstate];
return;
}
//不填 或者用1*1的填
cal(a,n, col + 1, laststate, nowstate);
if (((laststate >> col) & 1) == 0)
{
cal(a,n, col + 1, laststate, nowstate | (1 << col));
if ((col + 1 < n) && (((laststate >> (col + 1)) & 1) == 0))
{
cal(a,n, col + 2, laststate, nowstate);
}
}
}
inline int mul(ll x, ll y)
{
return x * y % 1000000007;
}
void multiply(int n,int a[][32],int b[][32])
{ // b = a * a
int i,j, k;
for (i = 0; i < n; ++i)
{
for (j = 0; j < n; ++j)
{
for (k = b[i][j] = 0; k < n; ++k)
{
if ((b[i][j] += mul(a[i][k],a[k][j])) >= 1000000007)
{
b[i][j] -= 1000000007;
}
}
}
}
}
int calculate(int n,int k)
{
int i, j;
int a[6][32][32],mat[2][32][32];
memset(a,0,sizeof(a));
for (i = 1; i <= 5; ++i)
{
for (j = (1 << i) - 1; j >= 0; --j)
{
cal(a,i, 0, j, 0);
}
}
memcpy(mat[0], a[k],sizeof(mat[0]));
k = (1 << k);
for (i = 0; n; --n)
{
multiply(k, mat[i], mat[i ^ 1]);
i ^= 1;
}
return mat[i][0][0];
}
```
##参考链接及推荐阅读
1. caopengcs,[木块砌墙](http://blog.csdn.net/caopengcs/article/details/9928061)
2. 红色标记,[木块砌墙](http://blog.csdn.net/dw14132124/article/details/9038417)
3. LoveHarvy,[木块砌墙](http://blog.csdn.net/wangyan_boy/article/details/9131501)
4. [在线编译测试木块砌墙问题](http://hero.pongo.cn/Question/Details?ID=36&ExamID=36)
5. hero上[木块砌墙一题](http://hero.pongo.cn/Question/Details?ExamID=36&ID=36&bsh_bid=273040296)
- 程序员如何准备面试中的算法
- 第一部分 数据结构
- 第一章 字符串
- 1.0 本章导读
- 1.1 旋转字符串
- 1.2 字符串包含
- 1.3 字符串转换成整数
- 1.4 回文判断
- 1.5 最长回文子串
- 1.6 字符串的全排列
- 1.10 本章习题
- 第二章 数组
- 2.0 本章导读
- 2.1 寻找最小的 k 个数
- 2.2 寻找和为定值的两个数
- 2.3 寻找和为定值的多个数
- 2.4 最大连续子数组和
- 2.5 跳台阶
- 2.6 奇偶排序
- 2.7 荷兰国旗
- 2.8 矩阵相乘
- 2.9 完美洗牌
- 2.15 本章习题
- 第三章 树
- 3.0 本章导读
- 3.1 红黑树
- 3.2 B树
- 3.3 最近公共祖先LCA
- 3.10 本章习题
- 第二部分 算法心得
- 第四章 查找匹配
- 4.1 有序数组的查找
- 4.2 行列递增矩阵的查找
- 4.3 出现次数超过一半的数字
- 第五章 动态规划
- 5.0 本章导读
- 5.1 最大连续乘积子串
- 5.2 字符串编辑距离
- 5.3 格子取数
- 5.4 交替字符串
- 5.10 本章习题
- 第三部分 综合演练
- 第六章 海量数据处理
- 6.0 本章导读
- 6.1 关联式容器
- 6.2 分而治之
- 6.3 simhash算法
- 6.4 外排序
- 6.5 MapReduce
- 6.6 多层划分
- 6.7 Bitmap
- 6.8 Bloom filter
- 6.9 Trie树
- 6.10 数据库
- 6.11 倒排索引
- 6.15 本章习题
- 第七章 机器学习
- 7.1 K 近邻算法
- 7.2 支持向量机
- 附录 更多题型
- 附录A 语言基础
- 附录B 概率统计
- 附录C 智力逻辑
- 附录D 系统设计
- 附录E 操作系统
- 附录F 网络协议
- sift算法
- sift算法的编译与实现
- 教你一步一步用c语言实现sift算法、上
- 教你一步一步用c语言实现sift算法、下
- 其它
- 40亿个数中快速查找
- hash表算法
- 一致性哈希算法
- 倒排索引关键词不重复Hash编码
- 傅里叶变换算法、上
- 傅里叶变换算法、下
- 后缀树
- 基于给定的文档生成倒排索引的编码与实践
- 搜索关键词智能提示suggestion
- 最小操作数
- 最短摘要的生成
- 最长公共子序列
- 木块砌墙原稿
- 附近地点搜索
- 随机取出其中之一元素