许多有用的算法在结构上是递归的:为了解决一个给定的问题,算法一次或多次递归地调用其自身以解决紧密相关的若干子问题。这些算法典型地遵守分治法(Divide and Conquer)的思想:将原问题分解成几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解。

三个步骤

分治模式在每层递归时都有三个步骤:

  1. 分解(Divide) 原问题为若干子问题,这些子问题是原问题的规模较小的实例。
  2. 解决(Conquer) 这些子问题,递归地求解各子问题。然而,若子问题的规模足够小,则直接求解。
  3. 合并(Combine) 这些子问题的解成原问题的解。

典型案例

求解x的n次幂

朴素解法

朴素解法需要 Θ(n) 的时间复杂度,即直接计算 nxxx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* power_naive - a naive way to calculate the power of a number.
* @param x the base number.
* @param n the power
*
* @return the power of the number.
*/

extern int power_naive(int x, int n)
{
int power = 1;
int i;
for (i = 0; i < n; i++)
power = power * x;
return power;
}

分治法

分治法的解法则可以达到 Θ(lgn) 的时间复杂度。

xn={xn/2xn/2,如果n为偶数x(n+1)/2x(n1)/2,如果n为奇数

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** 
* power_dnc - a divide-and-conquer way to calculate the power of number
*
* @param x the base number
* @param n the power
*
* @return the power of the number
*/
extern int power_dnc(int x, int n)
{
if (n == 1) {
return x;
}
if (n % 2 == 0){
int tmp = power_dnc(x, n/2);
return tmp * tmp;
} else {
return power_dnc(x, (n+1)/2) * power_dnc(x, (n-1)/2);
}
}

求解Fibonacci数列

斐波那契数列的定义:

Fn={0,如果n为01,如果n为1Fn1+Fn2,如果n2

朴素解法

朴素解法(直接递归求解)需要指数级[1]的时间(准确点讲是 Ω(ϕn),其中 ϕ黄金分割比 1+52

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* fibonacci_naive - the naive way to solve fibonacci series.
* @param n the nth num of the series.
*
* The naive way to solve fibonacci series.
* Very slow version.
*
*/
extern int fibonacci_naive(int n)
{
if (0 == n)
return 0;
else if (1 == n)
return 1;
else
return fibonacci_naive(n-1) + fibonacci_naive(n-2);
}

线性的解法

朴素解法在计算斐波那契数列的时候做了很多不必要的重复计算。使用动态规划、自底向上递归求解的策略可以将Fibonacci数列的计算规模降低到线性级别,即 O(n) 的时间复杂度。主要的思想基于这点:计算新的值时,用到前两个数的结果。可以用借助一维数组来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* fibonacci_linear - a linear way to solve fibonacci series.
* @param n the nth num of the series.
*
* A bottom-up way to solve fibonacci series.
* Can solve the problem in Linear time.
*
*/
extern int fibonacci_linear(int n)
{
int i;
int ans[n];

ans[0] = 0;
ans[1] = 1;

for(i=2; i <= n; i++)
ans[i] = ans[i-1] + ans[i-2];

if (0 == n)
return 0;
if (n == 1)
return 1;
return ans[n];
}

分治法求解:平方递归

(Fn+2Fn+1)=(1110)(Fn+1Fn)

(Fn+1FnFnFn1)=(1110)n

显然对斐波那契数列的求解变成了求矩阵 (1110)n 次幂,用到上面求乘方的算法可以将时间复杂度将为 O(lgn)

实现

1
TODO

矩阵乘法

定义:若 A=(aij), B=(bij)n×n的方阵,则对i,j=1,2,n,定义乘积 C=AB中的元素 cij 为: Cij=nk=1aikbkj

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define MAX 10
/**
* matrix2_multiply_naive - multiply two matrix
* @param result - the multiplying result
* @param mat1 - matirx1, which's size is mxn
* @param mat2 - matrix2, which's size is nxp
* @param m
* @param n
* @param p
*
*/
extern void matrix2_multiply_naive(int result[MAX][MAX], int mat1[MAX][MAX], int mat2[MAX][MAX], int m, int n, int p)
{
int i, j, k;
int sum;
for (i = 0; i < m; i++){
for (j = 0; j < p; j++){
sum = 0;
for (k = 0; k < n; k++){
sum += mat1[i][k] * mat2[k][j];
}
result[i][j] = sum;
}
}
}

分块解法

通常的做法是将矩阵进行分块相乘,如下图所示:

C(C0C1C2C3)=A(A0A1A2A3)×B(B0B1B2B3)

Ai,j,Bi,j,Ci,jF2n1×2n1

于是

C1,1=A1,1B1,1+A1,2B2,1

C1,2=A1,1B1,2+A1,2B2,2

C2,1=A2,1B1,1+A2,2B2,1

C2,2=A2,1B1,2+A2,2B2,2

分块相乘总共用了8次乘法,因而需要 Θ(nlog28)Θ(n3) 的时间复杂度。

Strassen算法

引入新矩阵

M1:=(A1,1+A2,2)(B1,1+B2,2) M2:=(A2,1+A2,2)B1,1 M3:=A1,1(B1,2B2,2) M4:=A2,2(B2,1B1,1) M5:=(A1,1+A1,2)B2,2 M6:=(A2,1A1,1)(B1,1+B1,2) M7:=(A1,2A2,2)(B2,1+B2,2)

可得:

C1,1=M1+M4M5+M7 C1,2=M3+M5 C2,1=M2+M4 C2,2=M1M2+M3+M6

在求解M1M2M3M4M5M6M7时需要使用7次矩阵乘法,其他都是矩阵加法和减法。因此时间复杂度下降为 O(nlog27)=O(n2.807) 。有得必有失,Strassen演算法的数值稳定性较差。

其他案例

深入阅读


  1. “Exponential time is bad; polynomial time is good.”-- Erik Demaine ↩︎

Comments

Related Issues not found

Please contact @wzpan to initialize the comment