一、概论
1、算法设计的目标:
(1)正确性
(2)可使用性(用户友好性)
(3)可读性
(4)健壮性
(5)高效率与低存储量需求
数据结构时算法设计的基础。算法的操作对象是数据结构,在设计算法时通常要构建适合这种算法的数据结构。数据结构设计主要是选择数据的存储方式,例如确定求解问题中的数据采用数组存储还是链表存储等。算法设计就是在选定的存储结构上设计一个满足要求的好算法
另外,数据结构关注的是数据的逻辑结构、存储结构以及基本操作,而算法更多的是关注如何在数据结构的基础上解决实际问题。算法是编程思想,数据结构则是这些思想的逻辑基础
二、递归
1、递归的定义:
在数学与计算机科学中,递归是指在函数的定义中又调用函数自身的方法。若p函数定义中调用p函数,称之为直接递归;若p函数定义中调用q函数,而q函数定义中又调用p函数,称之为间接调用(任何间接递归都可以等价地转化为直接递归)。
2、能够用递归解决的问题应该满足以下三个条件:
(1)需要解决的问题可以转化为一个或多个子问题来解决,而这些子问题的求解方法与原问题完全相同,只是在数量规模上不同
(2)递归调用的次数必须是有限的
(3)必须有结束递归的条件来终止递归
3、何时使用递归:
(1)定义是递归的:
eg.求n!和斐波那契(Fibonacci)数列
(2)数据结构是递归的:
eg.单链表、二叉树的二叉链存储结构
(3)问题的求解方法是递归的:
eg.汉诺塔问题
4、递归算法的执行过程:
(1)递归执行是通过系统栈实现的
(2)每递归调用一次就需将参数、局部变量和返回地址等作为一个栈元素进栈一次,最多的进栈元素个数称为递归深度,n越大,递归深度越深
(3)每当遇到递归出口或本次递归调用执行完毕时需要退栈一次,并恢复参数值等,当全部执行完毕时栈应该为空
5、这种自上而下将问题分解,再自下而上求值、合并,求出最后问题解的过程称为递归求解过程,它是一种分而治之的算法设计方法
6、提取递归模型的基本模型:
(1)对原问题f(sn)进行分析,抽象出合理的小问题f(sn-1)(与数学归纳法中假设n=k-1时等式成立相似)
(2)假设f(sn-1)是可解的,在此基础上确定f(sn)的解,即给出f(sn)与f(sn-1)之间的关系(与数学归纳法中求证n=k时等式成立的过程相似)
(3)确定一个特定情况(如f(0)或f(1))的解,由此作为递归出口(与数学归纳法求证n = 1或n = 0时等式成立相似)
7、用递归树求解递归方程:
主方法中递归方程的一般形式:T(n) = aT(n/b) + f(n)
使用二分法:
在保留的两个升序序列中重复上述过程直到两个序列中只含有一个元素为止,较小者即为所求的中位数
最后整个序列a的最大连续子序列和为maxLeftSum、maxRightSum和maxMidSum 中的最大值
设有n = 2^k个选手要进行网球循环赛,设计一个满足一下要求的比赛日程表:
解:采用分治策略可以将所有选手分为两半,2^k各选手的比赛日程表就可以通过2^(k - 1)个选手设计的比赛日程来决定,将n = 2^k问题划分为4个部分:
设X和Y都是n位的二进制整数,现在要计算它们的乘积X * Y
先将n位的二进制整数X和Y各分为两段,每段的长为n / 2位,因此,X = A * 2^(n / 2) + B,Y = C * 2^(n / 2) + D,这样,X和Y的乘积为:
X * Y = (A * 2^(n / 2) + B) * (C * 2^(n / 2) + D)
??????= A * C * 2^n + (A * D + B * C) * 2^(n / 2) + B * D
1、回溯法实际上是一个类似穷举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时就“回溯”(即回退),尝试其他路径
不同点:
深度优先遍历的目的是“遍历”,本质是无序的,也就是说访问次序不重要,重要的是是否被访问过,因此再实现上只需要对每个位置记录是否被访问就足够了
回溯法的目的是“求解过程”,本质是有序的,也就是说必须每一步都是要求的次序,因此再实现上要使用访问状态来记录,也就是对于每个顶点记录已经访问过的邻居方向,回溯之后从新的未访问过的方向去访问其他邻居
深度优先遍历对已经访问过的顶点不再访问,所有顶点仅访问一次
回溯法中已经访问过的顶点可能再次访问
深度优先遍历不含剪枝
很多回溯法采用剪枝条件剪除不必要的分枝以提高效能
第i层上的某个分枝结点的对应状态为dfs(i, tw, tv, rw, op),其中tw表示装入背包中的物品总重量,tv表示背包中物品的总价值,rw表示考虑第i个物品时剩余物品的重量,op记录一个解向量
有n个集装箱要装上两艘载重量分别为c1和c2的轮船
解:首先将第一艘轮船尽可能装满,然后将剩余的集装箱装在第二艘轮船上
使用回溯法:
对于每一个顶点,试探每一种着色(判断顶点与相邻顶点是否存在相同的着色),若可以着色,则进入下一个顶点着色,并进行回溯
有n个作业(编号为1-n)要在由两台机器M1和M2组成的流水线上完成加工,每个作业加工的顺序都是先在M1上加工,然后在M2上加工,M1和M2加工作业i所需的时间分别为ai和bi
采用回溯法进行求解:
用f1数组表示在M1上执行完当前作业i的总时间,用f2数组表示在M2上执行完当前作业i的总时间,对于排序树第i层的某个结点,若选择执行作业x[j],显然其在M1上执行完的时间为f1 = f1 + a[x[j]],考虑M2的时间可分为:
所以f2[i] = max(f1 + b[x[j]], f2[i - 1] + b[x[j]])
剪枝条件:当第i层求出的f2[i](即执行作业x[i]的总时间)已经大于大于bestf(当前求出的执行全部作业的最优总时间)时就没有必要从该结点向下扩展了,让其成为死结点
2、
方法 | 解空间搜索方法 | 存储结点的数据结构 | 结点存储特性 | 常用应用 |
回溯法 | 深度优先 | 栈 | 活结点的所有可行子结点被遍历后才从栈中出栈 | 找出满足条件的所有解 |
分枝限界法 | 广度优先 | 队列、优先队列 | 每个结点只有一次成为活结点的机会 | 找出满足条件的一个解或者特定意义的最优解 |
3、设计合适的限界函数:
(1)一般先要确定问题解的特性,如果目标函数是求最大值,则设计上界限界函数ub(根节点的ub值通常大于或等于最优解的ub值),若si是sj的双亲结点,则应满足ub(si)>=ub(sj),找到一个可行解ub(sk)后将所有小于ub(sk)的结点剪枝
(2)如果目标函数是求最小值,则设计下界限界函数lb(根节点的lb值一定要小于或等于最优解的lb值),若si是sj的双亲结点,则应满足lb(si)<=lb(sj),找到一个可行解ub(sk)后将所有大于ub(sk)的结点剪枝
队列式分枝限界法将活结点表组织成一个队列,并按照队列先进先出原则选取下一个结点为扩展结点
优先队列式分枝限界法的主要特点是将活结点表组成一个优先队列,并选取优先级最高的活结点作为当前扩展结点
在一般情况下,结点的优先级用与该结点相关的一个数值p来表示,如价值、费用、重量等。最大优先队列规定p值越大优先级越高,常用大根堆来实现;最小优先队列规定p值越小优先级越高,常用小根堆来实现
5、采用分枝限界法求解的关键问题:
(1)如何确定合适的限界函数
(2)如何组织待处理结点的活结点表
(3)如何确定解向量的各个分量
6、求解0/1背包问题
采用分枝限界法(采用上界设计方式):
对于第i层的某个结点e,用e.w表示结点e已装入的总重量,用e.v表示已装入的总价值,如果所有剩余的物品都能装入背包,那么价值的上界e.ub显然是e.v + v[j](j = i + 1到n);如果所有剩余的物品不能全部装入背包,假设物品i + 1到物品k能够全部装入,而物品k + 1只能装入一部分,那么价值的上界e.ub应是e.v + v[j](j = i + 1到n) + (物品k + 1装入的部分重量) * (物品k + 1的单位价值),这样每个结点实际装入背包的价值一定小于等于该上界
限界函数给出每一个可行结点相应的子树可能获得的最大价值的上界,如果这个上界不比当前最大值更大,则说明相应的子树中不含问题的最优解,因此该结点可以剪去(剪枝)
求解最优解的过程是先将求出上界的根节点进队,在队不空时循环:出队一个结点e,检查其左子结点并求出上界,若满足约束条件(e.w + w[e.i + 1] <= W),将其进队,否则该左子结点变成死结点;再检查其右子结点并求出其上界,若它是可行的(即其上界大于当前已找到可行解的最大总价值maxv,否则沿该结点搜索下去不可能找到一个更优的解),则将该右子结点进队,否则该右子结点被剪枝。循环这一过程,直到队列为空,算法最后输出最优解向量和最大总价值
在结点e进队时先判断是否为叶子结点(当e.i = n时为叶子结点),若是叶子结点,表示找到一个可行解,通过比较将最优解向量保存在bestx中,将最大总价值保存在maxv中,可行解对应的结点不进队,否则将结点进队
给定一个带权有向图G = (V, E),其中每条边的权是一个正整数,另外还给定V中的一个顶点v,成为源点。计算从源点到其他所有各顶点的最短路径长度,这里的长度是指路上各边权之和
解:
用dist数组存放从源点v出发的最短路径长度,dist[i]表示从源点v到顶点i的最短路径长度,初始时所有dist[i]值为∞
用prev数组存放最短路径,prev[i]表示从源点v到顶点i的最短路径中顶点i的前驱结点
采用广度优先遍历方法查找最短路径,在扩展顶点i时若顶点i到顶点j有边,剪枝的原则是如果结果这条边到达顶点j的路径长度更短(即dist[j]更小),则将顶点j作为子结点,否则不会将顶点j作为子结点,所有子结点进队
对于按1-n顺序执行的调度方案,f1数组表示在M1上执行完当前作业i的总时间,f2数组表示在M2上执行完当前作业i的总时间,计算公式如下:
f1 = f1 + a[i];
f2[i] = max(f1 + f2[i - 1]) + b[i];
对应结点e的lb的算法为:
void bound(NodeType &e) {
int sum = 0;
for(int i = 1; i <= n; ++i)
if(e.y[i] == 0)
sum += b[i];
e.lb = e.f1 + sum;
}
六、贪心法
1、贪心法的基本思路实在对问题求解时总是做出在当前看来是是最好的选择,也就是说贪心法不从整体最优上加以考虑,所做出的仅是在某种意义上的局部最优解,在求解问题时,通常求解问题直接给出或者可以分析出某些约束条件,满足约束条件的问题称为可行解。另外,求解问题直接给出或者可以分析出衡量可行解好坏的目标函数,使目标函数取最大(或最小)值的可行解称为最优解
1)田忌最快的马比齐威王最快的马快,即a[righta] > b[rightb],则二者比赛(两个最快的马比赛),田忌赢
2)田忌最快的马比齐威王最快的马慢,即a[righta] <?b[rightb],则选择田忌最慢的马与齐威王最快的马比赛,田忌输
3)田忌最快的马与齐威王最快的马的速度相同,即a[righta] < b[rightb]
(1)田忌最慢的马比齐威王最慢的马快,即a[lefta] > b[leftb],则两者比赛(两个最慢的马比赛),田忌赢
(2)田忌最慢的马比齐威王最慢的马慢,并且田忌最慢的马比齐威王最快的马慢,即a[lefta] < b[leftb]且a[lefta] < b[rightb],则选择田忌最慢的马与齐威王最快的马比赛,田忌输
(3)其他情况,即a[righta] = b[rightb]且a[lefta] <= b[leftb]且a[lefta] >= b[rightb],则a[lefta] >= b[rightb] = a[righta],即a[lefta] = a[righta],b[leftb] >= a[lefta] = b[rightb],即b[leftb] = b[rightb],说明比赛区间的所有马的速度全部相同,任何两匹马比赛都没有输赢
对于给定的作业(a, b),当a <= b时让a比较小的作业尽可能先执行,否则让b比较小的作业尽可能后执行
求将正整数n无序拆分成最大数为k的拆分方案个数,所有的拆分方案不重复
使用动态规划,设f(n, k)为将数n无序拆分成最多不超过k个数之和(称为n的k拆分)的分方案个数:
①拆分中包含k的情况,即一部分为单个k,另一部分为{x1, x2, ..., xi},后者的和为n - k,后者中可能再次出现k,因此是(n - k)的k拆分,所以这种拆分方案个数为f(n - k, k)
②拆分中不包含k的情况,则拆分中的所有拆分数都比k小,即n的(k - 1)拆分,拆分方案个数为f(n, k - 1)
因此,f(n, k) = f(n - k, k) + f(n, k - 1)
即:
1 当n = 1或者k = 1时
f(n, n) 当n < k时
f(n, k) = f(n, n - 1) + 1 当n = k时
f(n - k, k) + f(n, k - 1) 其他情况
dp[j]表示前j个元素中的最大连续子序列和
状态转移方程为:
dp[0] = 0 边界条件
dp[j] = max{dp[j - 1] + a[j], a[j]} 1 <= j <= n
给定高度为n的一个整数三角形,找出从顶部到底部的最小路径和,注意从每个整数出发只能向下移动到相邻的整数
三角形采用二维数组a[0..n-1][0..n-1]存放,用二维数组dp[i][j]表示从顶部a[0][0]查找到(i, j)结点时的最小路径和,状态转移方程为:
dp[0][0] = a[0][0] 顶部边界
dp[i][0] = dp[i - 1][0] + a[i][0] 考虑第一列的边界,1 <= i <= n-1
dp[i][i] = dp[i - 1][i - 1] + a[i][i]; 考虑对角线的边界
其他有两条可达路径的结点
dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j]) + a[i][j]
字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后形成的字符序列,给定两个序列A和B,称序列Z是A和B的公共子序列,是指Z同是A和B的子序列,该问题是求两序列A和B的最长公共子序列
采用动态规划,定义二维动态规划数组dp,其中dp[i][j]为子序列(a0, a1, ... ai-1)和(b0, b1, ..., bj-1)的最长公共子序列的长度,对应的状态转移方程为:
dp[i][j] = 0 i=0或j=0,边界条件
dp[i][j] = dp[i - 1][j - 1] + 1 a[i - 1] = b[j - 1]
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]) a[i - 1] ≠ b[j - 1]
采用动态规划,对应的状态转移方程为:
dp[i][0] = 1 若s[i] == t[0],初始化dp的第1列,0 <= i < n
dp[0][j] = 1 若s[0] == t[j],初始化dp的第1行,0 <= j < m
dp[i][j] = dp[i - 1][j - 1] + 1 若s[i] == t[j],1 <= i < n,1?<= j < m?????????????
采用动态规划,dp[i]表示a[0...i]中以a[i]结尾的最长递增子序列的长度,对应的状态转移方程为:
dp[i] = 1 0 <= i <= n - 1
dp[i] = max(dp[i], dp[j] + 1) 若a[i] > a[j], 0<=i<=n-1, 0<=j<=i-1
设A和B是两个字符串,现在要用最少的字符操作次数将字符串A转换为字符串B,字符操作有以下三种:
采用动态规划,dp[i][j]表示a[1..i](1<=i<=m)与b[1..j](1<=j<=n)的最优编辑距离(即a[1..i]转换为b[1..j]的最少操作次数
故状态转移方程为:
dp[i][j] = dp[i - 1][j - 1] 当a[i - 1] = b[j - 1]时
当a[i - 1] ≠ b[j - 1]时
dp[i][j] = min(dp[i - 1][j - 1] + 1, d[i][j - 1] + 1, dp[i - 1][j] + 1)
采用动态规划,dp[i][r]表示背包剩余容量为r(1 <= r <= W),已考虑物品1、2、…、i(1 <= i <= n)时背包装入物品的最优价值,对应的状态转移方程为:
dp[i][0] = 0(背包不能装入任何物品,总价值为0)
dp[0][r] = 0(没有任何物品可装入,总价值为0)
dp[i][r] = dp[i - 1][r](当r < w[i]时物品i放不下)
在放入和不放入之间选择最优解
dp[i][r] = max(dp[i - 1][r], dp[i - 1][r - w[i]] + v[i])
有n种重量和价值分别为wi、vi(0 <= i < n)的物品,从这些物品种挑选出总重量不超过W的物品,求出挑选物品价值综合最大的方案,每种物品可以挑选任意多件
采用动态规划,dp[i][j]表示从前i个物品种选出重量不超过j的物品的最大总价值,对应的状态转移方程为:
dp[i][0] = 0(背包不能装入任何物品,总价值为0)
dp[0][r] = 0(没有任何物品可装入,总价值为0)
dp[i][j] = max(dp[i][j], dp[i - 1][j - k * w[i]] + k * v[i])
fk[i][j] = k(物品i取k件)
或者:
dp[i][j] = dp[i - 1][j](当j < w[i]时物品i放不下)
在放入和不放入之间选择最优解
dp[i][j] = max(dp[i - 1][j], dp[i][j - w[i]] + v[i])
某公司有三个商店A、B、C,将新招聘的五名员工分配给这三个商店,各商店得到新员工后每年的盈利情况如表,求分配给各商店各多少员工才能使公式的盈利最大?
采用动态规划,dp[i][s]表示考虑商店i-商店m并分配总共s个人后的最优盈利,pnum[i][s]表示求出dp[i][s]时对应商店i的分配人数,v[i][j]表示商店i有j各员工时的盈利情况,状态转移方程为:
dp[m + 1][j] = 0
// pnum[i][s] = dp[i][s]取最大值的j(0 <= j <= n)
dp[i][s] = max(v[i][j] + dp[i + 1][s - j])
显然dp[1][n]为最优盈利