算法分析与设计

ObjectKaz Lv4

数学基础(了解)

指数和对数

指数的基本性质

  1. exey=ex+ye^xe^y=e^{x+y}
  2. exey=exy\frac{e^x}{e^y}=e^{x-y}
  3. (ex)y=ex+y(e^x)^y=e^{x+y}

对数的基本性质

  1. xa=bx^a=b 当且仅当logxb=a\log_x{b}=a
  2. logab=logcblogca\log_a{b} = \frac{\log_c{b}}{\log_c{a}} 其中a,b,c>0,a1a,b,c>0,a\ne1
  3. logxAB=logxA+logxB\log_x{AB}=\log_x{A}+\log_x{B}
  4. logxAB=logxAlogxB\log_x{\frac{A}{B}}=\log_x{A}-\log_x{B}
  5. logxAB=BlogxA\log_x{A^B}=B\log_x{A}
  6. alogbc=clogbaa^{\log_b c}=c^{\log_b a}
  7. $\log_b (1/a) = -\log_b a $
  8. A>0A \gt 0时,logxA<X\log_x{A}<X

常见对数

loglog 是以 2 为底的对数

  1. log1=0log 1=0
  2. log2=1log 2=1
  3. log1024=10log 1024=10
  4. log1048576=20log 1048576=20

级数

  1. 几何级数(等比数列的前 n 项和)
    i=0NAi=AN+11A1\sum_{i=0}^N A^i=\frac{A^{N+1}-1}{A-1}

    Sn=a11qn1qS_n = a_1 \frac{1-q^n}{1-q}

    1<A<1-1 < A < 1 时,级数收敛S=i=0Ai=11AS=\sum_{i=0}^{\infty} A^i=\frac{1}{1-A}

  2. 算术级数(等差数列的前 n 项和):i=1Ni=N(N+1)2N22=O(N2)\sum_{i=1}^N i = \frac{N(N+1)}{2} \approx \frac{N^2}{2}=O(N^2)

    平方和:i=1Ni2=N(N+1)(2N+1)3N33=O(N3)\sum_{i=1}^N i^2 = \frac{N(N+1)(2N+1)}{3} \approx \frac{N^3}{3}=O(N^3)

    k 次方和:i=1NikNk+1k+1=O(Nk+1)(k1)\sum_{i=1}^N i^k \approx \frac{N^{k+1}}{|k+1|}=O(N^{k+1}) (k\ne-1)

  3. 调和级数
    HN=i=1N1ilnNH_N=\sum_{i=1}^N{\frac{1}{i}}\approx\ln{N} ,其中误差γ0.57721566\gamma \approx 0.57721566 ,这个数值称为 欧拉常数

  4. 对数级数
    log1+log2+log3+...+logn=logn!=Θ(nlogn)\log 1 + \log 2 + \log 3 +...+ \log n = \log{n!} = \Theta(n \log{n})

算法概述

算法的特点

  1. 有穷性:一个算法必须总是在执行有穷步之后结束,且每一步都在有穷时间内完成。
  2. 确定性:算法中每一条指令必须有确切的含义。不存在二义性。只有一个入口和一个出口
  3. 可行性:一个算法是可行的就是算法描述的操作是可以通过已经实现的基本运算执行有限次来实现的。
  4. 输入:一个算法有零个或多个输入,这些输入取自于某个特定对象的集合。
  5. 输出:一个算法有一个或多个输出,这些输出同输入有着某些特定关系的量。

算法的评价指标

  1. 正确性:算法应满足具体问题的需求;
  2. 可读性:算法应该好读,以有利于读者对程序的理解;
  3. 健壮性:算法应具有容错处理,当输入为非法数据时,算法应对其作出反应,而不是产生莫名其妙的输出结果。
  4. 效率与存储量需求:效率指的是算法执行的时间;存储量需求指算法执行过程中所需要的最大存储空间。一般这两者与问题的规模有关。

复杂度的表示法

OO表示法(用得最多)

如果存在正常数ccn0n_0,当Nn0N \ge n_0时,T(N)cf(N)T(N) \le cf(N) 则称T(N)=O(f(N))T(N)=O(f(N)) 。此时称f(N)f(N)T(N)T(N)上界T(N)T(N)f(N)f(N)下界

此时,T(N)T(N)的增长速率比f(N)f(N)慢或相同。这一般表示 最糟糕的情况

Ω\Omega表示法

如果存在正常数ccn0n_0,当Nn0N \le n_0时,T(N)cf(N)T(N) \ge cf(N) 则称T(N)=Ω(f(N))T(N)=\Omega(f(N))

此时,T(N)T(N)的增长速率比f(N)f(N)快或相同。它一般表示 最好的情况

θ\theta表示法

T(N)=θ(f(N))T(N) = \theta(f(N)) ,当且仅当T(N)=O(f(N))T(N)=O(f(N))T(N)=Ω(f(N))T(N)=\Omega(f(N))

此时,T(N)T(N)的增长和f(N)f(N)相同,称T(N)T(N)f(N)f(N)同阶。

这一般表示 平均的情况

oo表示法

如果对于任意的cc 存在n0n_0,当Nn0N \geq n_0时,T(N)<cf(N)T(N) \lt cf(N) 则称T(N)=o(f(N))T(N)=o(f(N))

一般表示复杂度时,对精确度要求不高,可以忽略常数、低次幂、对数的底数。
对于空间复杂度,不考虑输入数据。

可以使用O(N2)O(N^2) 来代替O(2N2)O(2N^2)O(N2+N)O(N^2 + N) 这两种表示。

复杂度的运算法则

  1. T1(N)=O(f(N))T_1(N) = O(f(N))T2(N)=O(g(N))T_2(N) = O(g(N)),则:

    1. T1(N)+T2(N)=O(f(N)+g(N))T_1(N)+T_2(N) = O(f(N)+g(N))
    2. T1(N)×T2(N)=O(f(N)×g(N))T_1(N)\times T_2(N) = O(f(N) \times g(N))
  2. T(N)T(N)是一个kk次多项式,则:T(N)=Θ(Nk)T(N) = \Theta(N^k)

  3. 对任意常数kk,有logkN=O(N)\log^k{N}=O(N)。它告诉我们对数增长非常缓慢。

  4. 极限确定复杂度:

    limNf(N)g(N)={0f(N)=o(g(N))c0f(N)=θ(g(N))g(N)=o(f(N))\lim_{N\rightarrow \infty} \frac{f(N)}{g(N)} = \begin{cases} 0 & f(N)=o(g(N)) \\ c\ne 0 & f(N)=\theta(g(N)) \\ \infty & g(N)=o(f(N)) \\ \end{cases}

  5. 对于递归表达式f(n)=af(n/b)+g(n),a>1,b>1f(n)=af(n/b)+g(n),a>1,b>1,对应的时间复杂度为 T(n),设k=logbak=\log_b {a}

    • g(n)=θ(np)g(n)=\theta(n^p),且pkp \neq kq=max{p,k},T(n)=θ(nq)q=\max \{p,k\},T(n)=\theta(n^q)
    • 若 存在c0c \geq 0g(n)=θ(nk×logcn)g(n)=\theta(n^k \times \log^c n),那么T(n)=θ(nk×logc+1n)T(n)=\theta(n^k \times \log^{c+1} n)

注: 2. 第二条实际上是主定理的拓展。原描述:若 存在c0c \geq 0g(n)=O(nk)g(n)=O(n^k),那么T(n)=O(nk×logn)T(n)=O(n^k \times \log n) 3. 上面的结论只是为了一般情况下的算法分析而做的一些简化,主定理的严格的公理化定义见下:

(重要)两个函数渐进界的判断

  1. 定义法
  2. 极限法(结合洛必达法则)

f(n)=logn2,g(n)=logn+5f(n)= \log {n^2},g(n)= \log n + 5

因为limnf(n)g(n)=limn2lognlogn+5=limn11+5/logn=1\lim_{n\rightarrow \infty} \frac{f(n)}{g(n)}=\lim_{n\rightarrow \infty} \frac{2 \log n}{\log n + 5} = \lim_{n\rightarrow \infty} \frac{1}{1+5/\log n}=1

所以f(n)=θ(g(n))f(n)=\theta (g(n))

f(n)=n,g(n)=log2nf(n)= n,g(n)= \log^2 n

因为

limnf(n)g(n)=limnnlog2n\lim_{n\rightarrow \infty} \frac{f(n)}{g(n)} = \lim_{n\rightarrow \infty} \frac{n}{log^2 n}

=limn12×logn×1/n=\lim_{n\rightarrow \infty} \frac{1}{2 \times \log n \times 1/n}

=limnn2×logn=\lim_{n\rightarrow \infty} \frac{n}{2 \times \log n}

=limnn2=+=\lim_{n\rightarrow \infty} \frac{n}{2}=+\infty

所以f(n)=O(g(n))f(n)=O(g(n))

f(n)=logn,g(n)=log2nf(n)= \log n,g(n)= \log^2 n

n2n \geq 2 时,有f(n)1,g(n)1f(n) \geq 1,g(n) \geq 1
又因为g(n)=(f(n))2g(n)=(f(n))^2,且x1x \geq 1 时,x2xx^2 \geq x
所以当n2n \geq 2g(n)f(n)g(n) \geq f(n),即f(n)=O(g(n))f(n)=O(g(n))

(重要)递归方程复杂度的计算

  1. 主定理法
  2. 递归树法

大整数乘法中,时间复杂度的定义如下:

T(n)={O(1)n=13T(n/2)+O(n)n>1T(n)=\left\{\begin{matrix} O(1) & n=1 \\ 3T(n/2)+O(n) & n>1 \end{matrix}\right.

因为,k=log3,g(n)=O(n),p=1k=\log 3,g(n)=O(n),p=1

所以kpk \neq pT(N)=O(nlog3)T(N)=O(n^{log 3})

分析

第一步,计算kkk=logabk=\log_a b,分母为底数。结果为log23\log_2 3

第二步,观察非递推部分的时间复杂度,发现是O(n)O(n),所以指数pp为 1。

第三步,比较ppkk,两个不等,取最大的作为结果中nn 的幂O(nlog23)O(n^{\log_2 3})

ppkk 两个相等,那么T(n)=O(nk×logn)T(n)=O(n^k \times \log n)

如果非递归部分的时间复杂度不是多项式,那么主定理有可能不满足,此时建议采用递归树法来做。

大整数乘法中,时间复杂度的定义如下:

T(n)={O(1)n=13T(n/2)+O(n)n>1T(n)=\left\{\begin{matrix} O(1) & n=1 \\ 3T(n/2)+O(n) & n>1 \end{matrix}\right.

这个递归方程的的递归树如下:

其中,每个节点表示每个子问题非递归部分的时间复杂度,可以发现,只需要将所有节点的时间复杂度加起来,就可以得到整个问题的时间复杂度。

那么,这个递归的深度是多少?可以发现,子问题的规模是原来问题规模的1/21/2,那么,第ii 层(ii从 0 开始)子问题的规模为n2i\frac{n}{2^{i}},第ii层子问题的个数为3i3^{i},那么,这一层所有子问题的时间复杂度之和为O(3i2in)O(\frac{3^{i}}{2^{i}}n)

令子问题的规模n2i=1\frac{n}{2^{i}}=1,可以求出递归深度d=logn+1d=\log n + 1

注意,这个方程的解求的是最后一层的下标,但是这里深度是从 0 开始的,所以递归深度需要在解的基础上加 1。

由于最后一层,单个子问题时间复杂度是O(1)O(1) 而不是O(n)O(n)。所以对于最后一层,节点个数为3logn3^{\log n},这一层的时间复杂度为O(3logn)=O(nlog3)O(3^{\log n})=O(n^{\log 3})

实际上,即使最后一层,单个子问题的时间复杂度为 O(n),那么这一层的时间复杂度仍然为O(nlog3)O(n^{\log 3})

所以,整个问题的时间复杂度

T(n)=O(i=0logn1(32)i×n+nlog3)T(n)= O(\sum_{i=0}^{\log n-1} (\frac{3}{2})^i \times n + n^{\log 3})

=O(n×i=0logn1(32)i+nlog3)= O(n \times \sum_{i=0}^{\log n-1} (\frac{3}{2})^i + n^{\log 3})

=O(2n×((3/2)logn3/21)+nlog3)= O(2n \times (\frac{(3/2)^{\log n}}{3/2}-1) + n^{\log 3})

=O(43nlog32n+nlog3)= O(\frac{4}{3} n^{\log 3}-2n + n^{\log 3})

=O(nlog3)=O(n^{\log 3})

注:这里用到了对数的运算性质以及等比数列的求和公式。

分析

  1. 画出一个递归树(可选)
  2. 求出树的每一层的问题规模、子问题的个数,进而求出这一层的复杂度之和。(递归树深度从 0 开始)
  3. 计算整个问题的复杂度,即将每一层的时间复杂度加起来。

常见的复杂度

函数名称
11常数
logN\log{N}对数(高效解)
logkN\log^k{N}对数的 k 次方
NN线性(有效解)
NlogNN\log{N}
N2N^2平方级
2N2^N指数级(难解)
N!N!阶乘级

各种复杂度的增长规模:
mark

(源: Big O Cheat Sheet. )

递归与分治

递归

优点:结构清晰,可读性强,而且容易用数学归纳法来证明算法的正确性,因此它为设计算法、调试程序带来很大方便。

缺点:递归算法的 运行效率较低 ,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多。

概念

将一个规模为 n 的问题分解为 k 个规模较小的子问题,这些子问题互相独立且与原问题相同。递归地解这些子问题,然后将各个子问题的解合并得到原问题的解。

适用条件

  1. 该问题的规模缩小到一定的程度就可以容易地解决
  2. (前提)该问题可以分解为若干个规模较小的相同问题,即该问题具有 最优子结构 性质
  3. 利用该问题分解出的子问题的解可以合并为该问题的解
  4. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题

求解步骤

  1. 分解:将主问题分解为多个子问题
  2. 解决:解决各个子问题
  3. 合并:将子问题的解合并为主问题的解

动态规划

特点

  1. 最优子结构:该问题可以分解为若干个规模较小的相同问题
  2. 重叠子问题:每次产生的子问题并不总是新问题,有些子问题被反复计算多次
  3. 备忘录方法:用数组保存子问题的答案

比较

与递归分治比较

相同点:将待求解的问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
不同点:适合于用动态规划法求解的问题,经分解得到的子问题往往不是互相独立的。而用分治法求解的问题,经分解得到的子问题往往是互相独立的。

求解步骤

  1. 找出最优解的性质,并刻划其结构特征。
  2. 递归地定义最优值。
  3. 以自底向上的方式计算出最优值。
  4. 根据计算最优值时得到的信息,构造最优解。

0-1 背包问题

给定nn 种物品和一个背包。物品ii 的重量是wiw_i,其价值为viv_i,背包的容量为CC。问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?

dp(i,j){dp}(i,j) 表示前 i 个物品放入一个容量为jj 的背包时,前ii个物品的最大总价值。其中0<in,0<jC0<i\leq n,0<j \leq C

要得到dp(i,j)dp(i,j),有两种办法:

  1. 如果第ii 个物品的重量wiw_i 大于等于剩余容量jj,那么这个物品就不能放进去了,此时:v1=dp(i,j)=dp(i1,j)v_1=dp(i,j)=dp(i-1,j)

  2. 如果第ii 个物品的重量ww 小于剩余容量jj,那么这个物品就可以放进去,此时是否

如果不打算放进去,则其总价值就是v1v_1

如果打算放进去,那么:

  • 放进去之前,物品的数量减一,总价值减去wiw_i:dp(i1,jwi)dp(i-1,j-w_i)

  • 放进去之后,物品的总价值根据由放进去之前的总价值加上物品本身的总价值:v2=dp(i1,jwi)+viv_2=dp(i-1,j-w_i)+v_i

此时选择放进去之前和放进去之后,总价值最大的情况。

dp(i,j)=max{v1,v2}dp(i,j)=\max \{v_1,v_2 \}

i=1i=1 时,即选择第一个物品时,若j<wij < w_i,则dp(1,j)=vidp(1,j)=v_i,否则dp(1,j)=widp(1,j)=w_i

递归方程:

dp(i,j)={dp(i1,j)wi>jmax{dp(i1,jwi),dp(i1,j)}wij dp(i,j)=\left\{\begin{matrix} dp(i-1,j) & w_i>j \\ \max\{dp(i-1,j-w_i),dp(i-1,j)\} & w_i \leq j \end{matrix}\right.

算法的时间复杂度:O(N2)O(N^2)

算法的空间复杂度:O(N2)O(N^2)

优化空间复杂度
观察发现,要得到 dp(i,j),只需要 dp(i-1,j) 和 dp(i-1,j-w_i) 两个状态,也就是说,在设计的时候,二维数组只需要相邻的两部分,因此可以将二维数组降维一维,然后,改变内层循环的方向即可将空间复杂度降为 O(N)

贪心算法

特点

  1. 贪心选择:局部最优解能够得到整体最优解。(通常需要证明)
  2. 最优子结构:该问题可以分解为若干个规模较小的相同问题

比较

与动态规划

共同点:

  1. 都需要最优子结构性质,
  2. 都用来求有优化问题。

不同点:

动态规划:每一步作一个选择—依赖于子问题的解。
贪心方法:每一步作一个选择—不依赖于子问题的解。

动态规划方法的条件:子问题的重叠性质。
可用贪心方法的条件:最优子结构性质;贪心选择性质。

动态规划:自底向上求解;
贪心方法: 自顶向下求解。

可用贪心法时,动态规划方法可能不适用;
可用动态规划方法时,贪心法可能不适用。

搜索(回溯法和分支限界法)

回溯法

概念

回溯法也称为试探法,该方法首先暂时放弃关于问题规模大小的限制,并将问题的候选解按某种顺序逐一枚举和检验。当发现当前候选解不可能是解时,就选择下一个候选解;倘若当前候选解除了还不满足问题规模要求外,满足所有其他要求时,继续扩大当前候选解的规模,并继续试探。如果当前候选解满足包括问题规模在内的所有要求时,该候选解就是问题的一个解。在回溯法中,放弃当前候选解,寻找下一个候选解的过程称为回溯。扩大当前候选解的规模,以继续试探的过程称为向前试探。

解题步骤

  1. 针对所给问题,定义问题的解空间
  2. 确定易于搜索的解空间结构
  3. 以深度优先方式搜索解空间,并在搜索过程中用 剪枝函数 避免无效搜索

分支限界法

概念

这是一种用于求解组合优化问题的排除非解的搜索算法。类似于回溯法,分枝定界法在搜索解空间时,也经常使用树形结构来组织解空间。然而与回溯法不同的是,回溯算法使用深度优先方法搜索树结构,而分枝定界一般用宽度优先或最小耗费方法来搜索这些树。因此,可以很容易比较回溯法与分枝定界法的异同。相对而言,分枝定界算法的解空间比回溯法大得多,因此当内存容量有限时,回溯法成功的可能性更大。

和回溯法的差别

  1. 求解目标:回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。

  2. 搜索方式的不同:回溯法以深度优先的方式搜索解空间树,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树。

两种类型

  1. 队列式分支限界
  2. 优先级队列式分支限界

求解步骤

(1)针对所给问题,定义问题的解空间(对解进行编码);
(2)确定易于搜索的解空间结构(按树或图组织解) ;
(3)以广度优先或以最小耗费(最大收益)优先的方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。

  • 标题: 算法分析与设计
  • 作者: ObjectKaz
  • 创建于: 2022-01-01 12:29:37
  • 更新于: 2023-05-25 16:35:52
  • 链接: https://www.objectkaz.cn/c0f7b8931c28.html
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。