算法分析与设计复习总结笔记

注:文本中的所有代码块均为伪代码,不考虑具体的实现形式。

1.排序

插入排序

for j = 2 to A.length
    key = A[j]
    i = j - 1
    while(i > 0 && A[i] > key){
        A[i + 1] = A[i];
        i--;
    }
	A[i + 1] = key
       

归并排序

  • 归并排序:
Merge(A, p, q, r):
if p == r:
	return
for i = 1 to q - p + 1:
	L[i] = A[i + p - 1]
for j = 1 to r - q://因为A[q]已经放入L中
	R[j] = A[q + j]
L[q - p + 2] =, R[r - q + 1] =//哨兵
i = 1, j = 1
for k = p to r:
	if L[i] > R[j]:
		A[k] = R[j]
        j = j + 1
    else A[k] = L[i]
        i = i + 1
return A
Merge_sort(A, p, r):
while(p < r):
q = (p + r) / 2 //注意这里取下整
Merge_sort(A, p, q)
Merge_sort(A, q + 1, r)
Merge(A, p, q, r)

用递归树法得出时间复杂度。(ch2)

2. 宽度优先

  • BFS
procedure BFS(v)
    VISITED(v) = 1; u = v
    将Queue初始化为空
    loop
    	for each w ∈ Adj[v] do
            if VISITED(w) = 0 then
                ENQUEUE(Q, w)
                VISITED(w) = 1
            endif
        repeat
        if Q == Empty 
            then return 
        endif
        ENQUEUE(Q, v)
    repeat
end BFS
  • 宽度优先周游
procedure BFT(G, n)
    int VISITED(n)
    for i = 1 to n 
        do VISITED(i) = 0
        repeat
    for i = 1 to n
        if VISITED(i) = 0
            then call BFS(i)
        endif
    repeat
end BFT

若G是无向连通图或强连通图,则1次完成G的周游。否则完成多次。

  • 宽度优先生成树

    在BFS的基础上,增加了边的存储。

procedure BFS(v)
    VISITED(v) = 1; u = v
    将Queue初始化为空
    *T⬅空集
    loop
    	for each w ∈ Adj[v] do
            if VISITED(w) = 0 then
                T⬅T∪{(v, w)}
                ENQUEUE(Q, w)
                VISITED(w) = 1
            endif
        repeat
        if Q == Empty 
            then return 
        endif
        ENQUEUE(Q, v)
    repeat
end BFS

结束时,T中的边组成G的一棵生成树。

D_search

改造BFS,用 来保存未被检测的结点,则得到新的检索算法。

在这里插入图片描述

在上述图中,结点访问序列为:

1 2 3 6 7 8 4 5

3.深度优先

  • DFS
procedure DFS(v)
    VISITED(v) = 1
    for w ∈ Adj[v] do
        if VISITED(w) == 0 
            then call DFS(w)
        endif
    repeat
END DFS

注意: BFS和DFS的时间和空间开销相同。

在这里插入图片描述

  • 深度优先周游DFT

    深度优先周游的应用:(与宽度优先周游的应用相同)

    • 判断图的连通性
    • 计算图的连通分量
    • 无向图的自反传递闭包矩阵
  • 深度优先生成树:与宽度优先周游相似,增加一个存储边的集合T。

T⬅Φ
procedure DFS*(v, T)
    VISITED(v) = 1
    for w ∈ Adj[v] do
        if VISITED(w) == 0 then
            T = T ∪ {(v, w)}
			call DFS*(w, T)
        endif
    repeat
end DFS*

BFS和DFS生成树的区别

在这里插入图片描述

4 背包问题

01背包

描述:n个物体,每个物体数量有1个,重量w, 价值v。

i为坐标编号,j为背包容量。

状态转移方程:x[i][j] = max(x[i - 1][j], x[i - 1][j - w[i]] + v[i])
//遍历时,需要:
for i  = 1 to n:
	for j = C to 0:
		if j > w[i]:
			x[i][j] = max(x[i - 1][j], x[i - 1][j - w[i]] + v[i])
        else x[i][j] = x[i - 1][j]
return x[n][C]

由于i情况下,只与上一种情况i - 1有关,因此状态转移方程可以降维到:

x[j] = max(x[j], x[j - w[i]] + v[i]);
//这里的j为背包此时容量,取值为0~C
//注意j遍历时,应当从大到小遍历,直到j < w[i]为止,可以减少遍历次数。

变式:如果是多重背包问题

描述:n个物体,每个物体数量有d个,重量w, 价值v。

状态转移方程只要稍作修改:x[j] = max(x[j], x[j - k * w[i]] + k * v[i])

此时的遍历改为:

for(int i = 1; i <= n; i++)
    for(int j = C; j >= w[i]; j--)
        for(int k = 1; k <= d[i] && j >= k * w[i]; k++)
            x[j] = max(x[j], x[j - k * w[i]] + k * v[i]);
return f[C];

分数背包

思路为找单位重量收益最大的先放入,直到放满为止。

//已知:w[1...n], p[1...n]
let x[1...n] be a new array
for k = 1 to n:
	x[k] = 0
Package(frac, pos)
for i = 1 to n:
	Package.frac = p[i] / w[i]
    Package.pos = i
sort(Package) //按照frac降序排序
for i = 1 to n:
	if M >= Package.frac[i]
        x[Package.pos[i]] = 1
        M = M - w[Package.pos[i]]
    else 
        x[Package.pos[i]] = M / w[Package.pos[i]]
        break
return x

5 动态规划问题

注:这部分是本课程的最核心和最重点内容!但是考试只考了一道类似于LCS的简单动态规划问题…

钢条切割问题

在这里插入图片描述

  • 自顶向下:思路是,利用递归,分为i和n - i两个方向求解,且第一个方向不再分割。n-i采用递归。

这样的好处是,思路比较简单,但是时间和空间开销很大,生成了一棵递归调用树,多个部位重复计算。

  • 带备忘的自顶向下:

思路:采用一个数组r记忆计算过的钢条切割收益,然后在递归调用时,若计算过则直接返回。
在这里插入图片描述

  • 自底向上
//数组p[i]存储长度为i的钢条的收益,r[i]存储长度为i的钢条最大收益
r[0] = 0;
for j = 1 to n:
	q = -for i = 1 to j:
		q = max(q, p[i] + r[j - i])//关键递推式
    r[j] = q
repeat
return r[n]

时间复杂度:O(n2)

  • 带存储切割方案的自底向上方法
//数组p[i]存储长度为i的钢条的收益,r[i]存储长度为i的钢条最大收益,s[i]存储i段钢条切割的第一段
r[0] = 0;
for i = 1 to n:
	s[i] = 0;//new
for j = 1 to n:
	q = -for i = 1 to j:
		if q < p[i] + r[j - i]
            q = p[i] + r[j - i]
            s[i] = i;//new
    r[j] = q
repeat
return r and s
  • 输出最优切割方案
PRINT():
while n > 0
    print s[n]
    n = n - s[n]

矩阵链乘法

在这里插入图片描述

一种直观的想法就是,写成矩阵内积的样子:

//p[i]存储Ai+1个数组的行(row),m[i, j]二维数组,存放Ai到Aj乘法链运算的最优解
//部分正确,但是部分错误的想法:
for i = 0 to n:
	m[i, i] = 0
    m[i + 1, i] = 0
for i = 1 to n:
	for j = i to n:
		m[i, j] = -for k = i to j - 1:
			m[i, j] = min(m[i, j], m[i, k] + m[k + 1, j] + p[i - 1]p[i]p[j])
return m[1, n]

但是仔细推敲一下,这样做有问题,因为

当 i = 1, j = 3,k = 1时,m[1, 3] = m[1, 1] + m[2, 3] + p0p1p3,但是此时m[2, 3]还没有计算出来。

因此,需要限定第一次乘法时,矩阵链的长度为1,然后长度慢慢往上涨,一直涨到n,此时得出结果。

for i = 1 to n:
	m[i,i] = 0 //一定不会出现m[i + 1, i]的情况,因为i >=j > k
for l = 2 to n://l = 1时,一个操作数,无法进行乘操作。
	for i = 1 to n - l + 1://i遍历到末尾时,要留出l的长度
		j = i + l - 1
        m[i, j] = +for k = i to j - 1:
			m[i, j] = min(m[i, j], m[i, k] + m[k + 1, j] + p[i - 1]p[i]p[j])
return m[1, n]

类似地,对括号方案开一个数组进行记录,得:

//s[i,j]存储进行到从i到j矩阵的运算时,先在哪两个之间打括号
for i = 1 to n:
	m[i,i] = 0 //一定不会出现m[i + 1, i]的情况,因为i >=j > k
for l = 2 to n://l = 1时,一个操作数,无法进行乘操作。
	for i = 1 to n - l + 1://i遍历到末尾时,要留出l的长度
		j = i + l - 1
        m[i, j] = +for k = i to j - 1:
				if m[i, k] + m[k + 1, j] + p[i - 1]p[i]p[j] < m[i, j]:
					m[i, j] = m[i, k] + m[k + 1, j] + p[i - 1]p[i]p[j]
                    s[i, j] = k
return m[1, n]

时间复杂度:O(n2)

打印矩阵链乘法方案:

PRINT(s, i, j):
if i == j:
	print "Ai"
else 
    print "("
    print PRINT(s, i, s[i, j])
    print PRINT(s, s[i ,j] + 1, j)
    print ")"
    

最长公共子序列

递推式如下:

在这里插入图片描述

采用自底向上的动态规划如下:

//原始思路(存在问题):
for i = 1 to n:
	for j = 1 to m:
	c[i, j] = 0;
for i = 1 to n:
	for j = 1 to m:
		if x[i] == y[j]:
			c[i, j] = c[i - 1, j - 1] + 1
        else 
            c[i, j] = max(c[i - 1, j], c[i, j - 1])
return c[n, m]

这样分析,发现有问题,因为i-1和j-1的存在,因此i,j都可以取0

因此修改为:

//在初始化中将c[i, 0], c[0, j]均初始化为0
for i = 0 to n:
	for j = 0 to m:
	c[i, j] = 0;
for i = 1 to n:
	for j = 1 to m:
		if x[i] == y[j]:
			c[i, j] = c[i - 1, j - 1] + 1
        else 
            c[i, j] = max(c[i - 1, j], c[i, j - 1])
return c[n, m]

增加一个存储如何前行的数组b,用来记录前行的方向。

//在初始化中将c[i, 0], c[0, j]均初始化为0
for i = 0 to n:
	for j = 0 to m:
	c[i, j] = 0;
for i = 1 to n:
	for j = 1 to m:
		if x[i] == y[j]:
			c[i, j] = c[i - 1, j - 1] + 1
            b[i, j] = '↖'
        else 
            aa = c[i - 1, j]
            bb = c[i, j - 1]
            c[i, j] = max(aa, bb)
            if aa >= bb
                b[i, j] = '↑'
            else b[i, j] = '←'
return c[n, m] and b

时间复杂度:O(mn)

算法改进:

  • 直接依据c[i,j]来求方向
  • 压缩数组维度(只能得到LCS的长度,得不到具体的值)
for i = 0 to m
    c[i] = 0
t = 0
for i = 1 to n:
	for j = 1 to m:
		if x[i] == y[j]:
			c[j] = c[j - 1] + 1
        else c[j] = c[j - 1]
return c[m]
  • 打印LCS
PRINT(b, X, i, j):
if i == 0 or j == 0
    return 
if b[i, j] = '↖'
    PRINT(b, X, i - 1, j - 1)
    print Xi
elseif b[i, j] = '↑'
    PRINT(b, X, i - 1, j)
else PRINT(b, X, i, j - 1)

最优二叉搜索树

有n个单词,其概率分别为p1, p2…pn

落在两个单词之间的概率,用q0, q1, q2, … qn表示,每一个区间以伪关键字di表示,共n + 1个。

一个二叉搜索树的搜索成本用(depth(i) + 1) * ti表示,ti表示一个结点。

目的就是使这个搜索成本最小,因此需要pi大的靠近根,pi小的靠近叶子。所有的叶子结点都是伪关键字结点。

状态转移方程:

在这里插入图片描述

在ki~kj的关键字中,选一个kr当作根节点,此时ki ~ kr-1和kr+1到kj变为kr的子树,每个结点的深度加1。

再加上变为根节点的pr代价,共计新增搜索代价为w(i, j)

在这里插入图片描述

对于w[i, j]和w[i, j - 1],其关系为:

在这里插入图片描述

特别地,对于i = 1, j = 1, w[i, j] = w[1, 0] + p1 + q1 = q0 + p1 + q1

for i = 1 to n + 1:
	e[i, i - 1] = q[i - 1]
    w[i, i - 1] = q[i - 1]
for l = 1 to n:
	for i = 1 to n - l + 1
        j = i + l - 1
        e[i, j] = +∞
        w[i, j] = w[i, j - 1] + pj + qj
        for k = i to j
            t = e[i, k-1] + e[k+1, j] + w[i,j]
            if t < e[i, j]
                e[i, j] = t
                root[i, j] = k
return e and root

6 函数的增长

  • 大O:渐进上界(但可能不是渐临紧确界)
  • 小o:非渐进上界
  • 大omiga: 渐进下界
  • 小omiga(w):非渐进下界
  • 在这里插入图片描述

注意theta符号给出的是:渐进紧确界

定理:在这里插入图片描述

7 分治

归并排序

  • 归并排序:
Merge(A, p, q, r):
if p == r:
	return
for i = 1 to q - p + 1:
	L[i] = A[i + p - 1]
for j = 1 to r - q://因为A[q]已经放入L中
	R[j] = A[q + j]
L[q - p + 2] =, R[r - q + 1] =//哨兵
i = 1, j = 1
for k = p to r:
	if L[i] > R[j]:
		A[k] = R[j]
        j = j + 1
    else A[k] = L[i]
        i = i + 1
return A
Merge_sort(A, p, r):
while(p < r):
q = (p + r) / 2 //注意这里取下整
Merge_sort(A, p, q)
Merge_sort(A, q + 1, r)
Merge(A, p, q, r)

用递归树法得出时间复杂度。(ch2)

最大连续子数组

FIND_MID_CROSS_SUBARRAY(A, low, mid, high):
left_sum = -∞
right_sum = -∞
sum = 0
for i = mid to low:
	if left_sum < A[i] + sum
        sum += A[i]
        left_sum = sum
       	L = i
sum = 0
for i = mid + 1 to high: //从mid + 1开始,避免mid算两次
	if right_sum < A[i] + sum:
		sum += A[i]
        right_sum = sum
        R = i
return L, R, left_sum + right_sum
FIND_MAX_SUBARRAY(A, low, high):
if low == high:
	return (low, high, A[low])
if low < high:
mid = (low + high) / 2
(left_l, left_r, left_sum) = FIND_MAX_SUBARRAY(A, low, mid)
(right_l, right_r, right_sum) = FIND_MAX_SUBARRAY(A, mid + 1, high)
(mid_l, mid_r, mid_sum) = FIMD_MID_CROSS_SUBARRAY(A, low, mid, high)
return 三者中最大的sum的三元组(l, r, sum)

8 贪心算法

证明贪心算法的最优子结构性

大意:假设一个最优解使用k个硬币,其中使用了1个面值为c的硬币。对于面值为n的纸币,最优的硬币兑换方案必须让n-c的纸币兑换方案最优,且使用k - 1个硬币。

  • 否则利用“剪切-粘贴法”证明:

​ 若n-c的纸币兑换需要小于k-1个硬币,那么最优解的硬币个数要小于k,与最优解为k个硬币的假设矛盾。故货币兑换问题具有最优子结构。

路径选择问题

  • 迭代求解:时间复杂度O(n)
假设路径已经按照结束时间排好序
CHOOSE(s, f):
A = {a1}
k = 1
for m = 2 to n:
	if s[m] >= f[k]:
		k = m
        A = A ∪ {am}
return A

路径选择问题的进阶版

图的区域着色问题:

  • 一种直观的想法就是,将上述路径选择重复进行多次,然后每次将一个最大兼容子数组从原集合中剔除,继续调用CHOOSE函数直到原集合为空。执行次数即为图的着色颜色数/最少所需教室数。

  • 但是,最坏情况下,这样的时间复杂度为O(n2)

  • 采用另一种更快的思路:

    每一个开始和结束的时间想象为一条线段,如果两条线段不重叠,就把这条线段加入上一条的后面,更新线段的末端值。否则重新开一条线段。最后的线段数为所需的最少教室数。

struct activity[1...n] //contains s, f, pos, room in each struct
priority queue q //按照存入的f升序排序
ans = 0
sort(activity) //按照开始时间和结束时间排序,升序。优先开始时间。
for i = 1 to n:
	if q == Empty:
		q.push(activity[i])
        ans = ans + 1
        activity[i].room = ans
    else 
        if q.top().f <= activity[i].s
            w = q.pop()
            q.push(activity[i])
            activity[i].room = w.room
        else 
            q.push(activity[i])
            ans = ans + 1
            activity[i].room = ans
return ans, activity

时间复杂度:O(nlogn)

分数背包问题

思路为找单位重量收益最大的先放入,直到放满为止。

//已知:w[1...n], p[1...n]
let x[1...n] be a new array
for k = 1 to n:
	x[k] = 0
Package(frac, pos)
for i = 1 to n:
	Package.frac = p[i] / w[i]
    Package.pos = i
sort(Package) //按照frac降序排序
for i = 1 to n:
	if M >= Package.frac[i]
        x[Package.pos[i]] = 1
        M = M - w[Package.pos[i]]
    else 
        x[Package.pos[i]] = M / w[Package.pos[i]]
        break
return x

由于本题需要排序,所以时间复杂度为O(nlogn)

如何在线性时间内求分数背包问题?

已知获得一个数中位数的时间复杂度为O(n)

在这里插入图片描述


货币兑换问题

证明最优子结构性

在这里插入图片描述

大意:假设一个最优解使用k个硬币,其中使用了1个面值为c的硬币。对于面值为n的纸币,最优的硬币兑换方案必须让n-c的纸币兑换方案最优,且使用k - 1个硬币。

  • 否则利用“剪切-粘贴法”证明:

​ 若n-c的纸币兑换需要小于k-1个硬币,那么最优解的硬币个数要小于k,与最优解为k个硬币的假设矛盾。故货币兑换问题具有最优子结构。

C的幂次面值硬币的贪心算法证明:

在这里插入图片描述

#### 在O(nk)下利用动态规划进行货币兑换

denomination: 货币面值

denom[1…n]:存储当前 j分纸币减少时使用的硬币面值。

在这里插入图片描述

//已知货币序列(降序)为d[1...k]
Compute_coins(d, k, n):
let A[1...n], denom[1...n] be new arrays//A记录硬币个数,denom记录兑换方案
j = C
for j = 1 to n:
	A[j] = +for i = 1 to k:
		if j >= d[i] and A[j] < A[j - d[i]] + 1:
			A[j] = A[j - d[i]] + 1
        	denom[j] = d[i]
return A, denom
PRINT(j):
if j > 0:
	print ("1 coin of " + denom[j])
    PRINT(j - denom[j])

9 回溯与分枝限界的对比

在这里插入图片描述

9.1 回溯法(DFS+剪枝)

子集和数

回溯法+限界函数

限界采用两个隐式条件,可以尽早杀死一些对答案无用的结点,使状态空间树的规模缩小。

在这里插入图片描述

第一个条件理解:当前选择方案的和数与后面所有未选择数的和相加,必须不小于M,否则整个序列和都小于M,不会存在一个答案。

第二个条件理解:当前选择方案的和数与下一个选择的数字之和必须小于M,否则由于W[1…n]按照降序排序,则M落在当前和数与下一个和数之间,不存在答案。

机器的可靠性问题

//已知:c[1...n][1...m], q[1...n][1...m], C
global current = +∞
global record[1...m], best[1...m]
FIND_METHOD(k, C, cost, Q):
if cost > current:
	return
if k > n:
	current = cost
	for j = 1 to m:
		best[j] = record[j]
    return
for j = 1 to m:
	if cost + c[k][j] < C:
		record[j] = k		//存储解决方案
		FIND_METHOD(k + 1, C - q[k][j], cost + c[k][j], Q + q[k][j])
 
Machine_design(n, C, 0, 0)
//得到current, best    

9.2 分枝限界(BFS+剪枝->引入成本估计函数)

多处理机调度问题(与作业调度问题不同)

//Queue:q, r[1...n]为n个作业的运行时间
FIND(n, k, r):
Queue q 为空						//维护一个队列,元素依照A.r排序。A.r:成本估计函数,值等于等待时间+运行时间
struct A{r, pos, tag}			  //定义一个结构,r为成本估计函数,pos记录原始数据位置,tag记录由哪一台处理机处理
A[1...n]
for i = 1 to n:					  //初始化
	A[i].r = r[i]
    A[i].pos = i
sort(A[1...n]) //按照A[i].r降序排序
if n <= k:						//n的数量小于处理机数量,直接返回r的最大值
	return A[1].r
time = 0
for j = 1 to k:					//前k个作业直接入队
	A[j].tag = j
	q.push(A[j])
time += A[j].r
for j = k + 1 to n:
	w = EXTRACT_MIN(q)			//将队列元素按照成本估计函数排序,返回成本估计最小的结点,并在队列中删除
    A[j].r += w.r
    A[j].tag = w.tag
    if A[j].r > time:
		time = A[j].r
    q.push(A[j])
return time, A

十五谜问题

f(x): 由根节点到X的路径长度

g(x):把X转换为目标状态的最小移动数

作业惩罚调度问题

上界:未选入S的惩罚求和

下界:目前S经过的点中,未选入S的惩罚求和

后记:考试原题

2022年5月8日22:07

虽然已经考完试了,但是还是想把其中发挥的一道题写下来,记录思路。

求A{a1, a2, … an}中,ai + aj = M的两个数的下标。

global record[2]
FIND_PAIR(A, n, X):
let B1 and B2 be new sets
B1 = 空集
B2 = 空集
A1 = A ∪ X
sort(A1) //按照升序,对新集合排序
if X == A1[1] or X == A1[n + 1]: //排除不存在的情况
	print "not exist"
    return 
i = 1
while A1[i] != X:				//初始化两个集合
	B1 = B1 ∪ {X - A1[i]}
	i = i + 1
for j = i + 1 to n + 1:
	B2 = B2 ∪ {A2[j]}
sort(B1) //升序排序
sort(B2) //升序排序
//下面利用归并排序中归并的思想,将两个集合B1, B2做升序排序。
//若出现L[i] == R[j]的情况,则返回对应元素的下标
//n1 = i - 1
n2 = n + 1 - (i + 1) + 1 = n - i + 1
i = 1
k = 1
for t = 1 to n:
	if B1[i] < B2[k]:
		i++
    elseif B1[i] > B2[k]:
		k++
    else record[1] = B1[i].pos
         record[2] = B2[k].pos
         return record

时间复杂度:O(nlogn)

  • 2
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值