第一章 算法的基本概念
算法的定义
- 有穷指令序列若满足确定性、能行性、输入、输出、有穷性,则通常称之为算法。
- 只满足前4条而不满足第5条(有穷性)的有穷指令序列通常称之为计算过程。
- 永远执行的计算过程 并非毫无用处—— OS就是计算过程。
算法的“好坏”如何衡量
- 用独立于具体计算机的客观衡量标准:问题的规模、基本运算、算法的计算量函数
描述算法时间复杂度
- 时间复杂度用T(n)(或T(n,m)等)来表示
T(n)= O(f(n))——上界 近似复杂度
若存在c > 0,和正整数n0≥1,使得当n≥n0时,总有 T(n)≤cf(n)
T(n)=Ω(f(n))——下界 近似复杂度
若存在c > 0,和正整数n0≥1,使得当n≥n0时,存在无穷多个n,使得T(n)≥cf(n)成立
T(n)=Θ(f(n))——既给出了上界也给出了下界 准确的复杂度
若存在c1,c2>0,和正整数n0≥1,使得当n≥n0时,总有T(n)≤c1 *f(n),且有无穷多个n,使得T(n)≥c2 *f(n) 成立,即:T(n)= O(f(n))与T(n)=Ω(f(n))都成立
如何评价算法
正确性、健壮性、简单性、高效性、最优性
第二章 递归和分治
什么是分治法
- 分治法的基本策略
分解:将原问题分解为子问题
解决:求解子问题
合并:组合子问题的解得到原问题的解
分治法与平衡的概念
- 使子问题规模尽量接近的做法,就是平衡
- 在使用分治法和递归时,要尽量把问题分成规模相等,或至少是规模相近的子问题以提高算法的效率
分治与递归
- 递归函数:用函数自身给出定义的函数
- 递归算法:一个算法包含对自身的调用
- 这种调用可以是直接的,也可以是间接的
- 优点:结构清晰,可读性强,而且容易用数学归纳法来证明算法的正确性,为设计算法、调试程序带来很大便利
- 缺点:递归算法的运行效率较低
递归式的解法
- 代换法:
①猜测解的形式
②用数学归纳法证明之
- 只适用于解的形式很容易猜的情形
- 递归树方法
- 每一个节点代表递归函数调用集合中一个子问题的代价,将所有层的代价相加得到总代价
- 当用递归式表示算法的时间复杂度时,可用递归树的方法
- 递归树方法模拟了算法的递归执行,可以由递归树方法产生对算法时间复杂度的较好猜测
- 主方法
其中a是子问题数目,b是子问题规模,f(n)是分解和合并的代价
注:nlgn不是多项式>n
分治法的适用条件
- 问题的规模缩小到一定程度就可以容易地解决
- 问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质
- 基于子问题的解可以合并为原问题的解
- 问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题
分治法实例
快速排序
- 分解:数组A[p…r]被划分为子数组A[p…q-1]和A[q+1…r], A[p…q-1]中的每个元素都小于等于A[q],A[q+1…r]中的每个元素都大于等于A[q],q在划分时确定
解决:通过递归调用快速排序算法,对子数组A[q+1…r]和A[p…q-1]进行排序
合并:由于子数组的排序为原地排序,解的合并不需要操作,整个数组已经排好序 - 最坏情况时间复杂度:Θ(n2)
平均情况时间复杂度:Θ(nlgn) - 随机化版本:随机选择主元
最大元、最小元
直接解法
- 分治法:
当n=2时,一次比较就可以找出两个数据元素的最大元和最小元;
当n>2时,可以把n个数据元素分为大致相等的两半 - 算法下界:上取整3n/2-2
最近点对问题
一维:n个点退化为n个实数,最近点对即为这n个实数中相差最小的两个实数
分治法求解
- 分解:用n个实数的中位数m作为分割点,分成两个点集
- 求解:在两个点集上分别找出其最接近点对{p1,p2}和{q1,q2}
- 合并:整个点集的最近点对或者是{p1,p2},或者是{q1,q2},或者是某个{p3,q3},其中p3和q3分属两个点集。如果最近点对是{p3,q3},即 |p3-q3|<d,则p3和q3两者与m的距离不超过d,即p3∈(m - d,m],q3∈(m,m + d]。
二维:有n个点,输入点集记为P
- 预处理:将点对按x坐标排序,将点对按y坐标排序
- 分解:将P进行分割,分为2部分求最近点对。选择一条垂线L,将P拆分左右两部分为PL和PR
- 解决:分别寻找PL和PR中的最近点对及距离,设其找到的最近点对距离分别是δL和 δR,置δ=min(δL, δR)
- 合并:
可能一:最近点对就是某次递归调用找出的距离为𝛿的点对
可能二:最近点对是由PL 中的一个点和PR中的一个点组成的点对 - 合并子问题
找出以L为中心线,宽度为2𝛿的带状区域(获得带状区域中排序后的点集Y’,预处理已排序)
对Y’中的每个点,检查其后面的7个点,计算距离并更新最近点对的距离 - 递归式为T(n)=2T(n/2)+f(n),f(n)=O(n)
- 时间复杂度为:O(nlogn)
寻找顺序统计量问题
- 求第i小元素问题、选择问题:设集合S中共有n个数据元素,要在S中找出第i小元素
最小元:i=1
最大元:i=n
中位数:i=⌊(n+1)/2⌋ - 求解方法:
- 排序
- 期望线性时间Θ(n)
分解:使用 Random Partition 对数组进行划分(借用快排的划分方式)
- 求解:检查主元元素是否第 i 小,若是,则返回;
若落在低区,则在低区的子数组中递归选择;
若落在高区,则在高区的子数组中递归选择。 - 一般情况:T(n)=T(9n/10)+Θ(n)= Θ(n)
- 最坏情况:T(n)=T(n-1)+Θ(n)= Θ(n2)
- 最坏情况线性时间
基本思想:保证对数组的划分是好的划分
① 将输入数组的n个元素分为⌊n/5⌋+1组;
② 寻找这⌊n/5⌋+1组中每一组的中位数;
③ 对第二步中找出的⌊n/5⌋+1组中位数,递归调用SELECT以找到其中位数x;
④ PARTITION(随机版快排划分),按中位数x对输入数组进行划分,x为第k小元素;
⑤ 如果i=k,则返回x;否则,如果i<k,则在低区递归调用SELECT寻找第i小元素;否则,如果i>k,则在高区寻找第(i-k)个最小元素。
第三章 动态规划
动态规划方法的适用范围
- 若一个问题可以分解为若干个高度重复的子问题,且问题也具有最优子结构性质,就可以用动态规划法求解。
- 思想:保存已解决的子问题的答案,在需要时使用,从而避免大量重复计算
- 解题步骤:
找出最优解的性质,并刻画其结构特征
递归地定义最优值(写出动态规划方程)
以自底向上的递推方式计算出最优值
根据计算最优值时得到的信息,以递归方法构造一个最优解
动态规划方法求解实例
矩阵连乘问题——时间复杂度Θ(n3)
int m[7][7] = {0};
int a[7] = {30, 35, 15, 5, 10, 20, 25}; //矩阵维度
int n=6;
int main()
{
// 初始化对角线为 0
for (int i = 0; i <= n; i++) {
m[i][i] = 0;
}
// r个矩阵连乘
for (int r = 2; r <= n; r++) {
//从第i个矩阵开始,连续r个矩阵有n-r+1种情况
for (int i = 1; i <= n - r + 1; i++) {
int j = i + r - 1;
m[i][j] = m[i][i]+m[i + 1][j] + a[i - 1] * a[i] * a[j];
// 改变分割位置k,k的取值范围是[i+1,j-1]
for (int k = i + 1; k < j; k++) {
int t = m[i][k] + m[k + 1][j] + a[i - 1] * a[k] * a[j];
m[i][j]=min(m[i][j],t);
}
}
}
cout<<m[1][n];
return 0;
}
最长公共子序列问题LCS——时间复杂度O(mn)
- 子序列的概念: Z是X的子序列(下标不必连续),记为Z<X
- 公共子序列的概念:Z<X和Z<Y,则称Z是X和Y 的公共子序列
- 最长公共子序列的概念:
若Z<X,Z<Y,且不存在比Z更长的X和Y 的公共子序列,则称Z是X和Y 的最长公共子序列,记为Z∈LCS(X,Y)。最长公共子序列往往不止一个。 - Brute-force法(模式匹配法),对较大的m没有实用价值
- 动态规划:
引入一个二维数组C,用C[i,j]记录Xi与Yj的LCS的长度
最大子段和
- 分治——O(nlogn)
将序列划分为左右两部分,则最大子段和可能在三处出现:左半部、右半部以及跨越左右边界的部分。 - 动态规划——O(n)
第四章 贪心算法
贪心算法的基本思想
- 适用于求解最优化问题的算法往往包含一系列步骤,每一步都有一组选择
- 贪心算法总是作出在当前看来是最好的选择
- 贪心算法并不从整体最优上加以考虑,它所作出的选择只是某种意义上的局部最优选择
- 贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解
- 在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似
- 与动态规划方法相比,贪心算法更简单,更直接
贪心算法的基本要素:
- 贪心算法通过做一系列的选择来给出某一问题的最优解。它所作出的每一个选择当前状态下的最好选择(局部),即贪心选择
- 这种贪心选择并不总能产生最优解,但对于一些问题,比如活动安排问题,可以给出最优解
- 可以根据下列步骤设计贪心算法
将最优化问题转化为这样的一个问题,即先做出选择,再解决剩下的一个子问题
证明原问题总有一个最优解是做贪心选择得到的,从而说明贪心选择的安全
说明在做出贪心选择之后,子问题的最优解与所作出的贪心选择联合起来,可以得出原问题的一个最优解
许多可以用贪心算法求解的问题,具备以下两个性质:
- 贪心选择性质
是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到
这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别
动态规划算法通常以自底向上的方式解各子问题
贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题 - 最优子结构性质
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质
问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征 - 贪心选择的证明
1.原问题总有一个最优解是做贪心选择得到的(说明贪心选择的安全性)
2.做出贪心选择之后,原问题简化为比原问题更小的但与原问题形式相同的子问题
贪心算法求解实例
活动安排问题
贪心体现在总是选择具有最早完成时间的相容活动
- 复杂度分析
如果已经排序,算法的时间复杂度为Θ(n)
如果事先没有按照结束时间增序排列,排序需O(nlogn) - 贪心算法可以获得该问题的整体最优解, 可证明:
① 活动安排问题有一个最优解以贪心选择开始;
② 做出贪心选择之后,原问题简化为比原问题更小的但与原问题形式相同的子问题。
单源最短路径Dijkstra
- 基本思想
- 设置顶点集合S,初始时,S中仅含有源,此后不断作贪心选择来扩充这个集合
- 一个顶点属于集合S当且仅当从源到该顶点的最短路径长度已知 - 设u是G的某一个顶点,把从源到u且中间只经过S中顶点的路称为从源到u的特殊路径,并用数组dist记录当前每个顶点所对应的最短特殊路径长度
- Dijkstra算法每次从V-S中取出具有最短特殊路长度的顶点u,将u添加到S中,同时对数组dist作必要的修改,检查dist(u)+[u,j]与dist[j]的大小,若dist(u)+[u,j]较小,更新 - 一旦S包含了所有V中顶点,dist就记录了从源到所有其他顶点之间的最短路径长度
- 贪心策略体现在对V-S中的点的选择上
- 对于具有n个顶点和e条边的带权有向图,如果用带权邻接矩阵表示这个图,T(n)=O(n2)
//最短路问题
int n, m, s, edge[10][10], dist[10];
bool vis[10];
void dij(int k){
memset(vis, 0, sizeof(vis));
memset(dist, 0x3f, sizeof(dist));
dist[k] = 0;
for (int i = 1; i <= n; i++)
{
int x = 0;
for (int j = 1; j <= n; j++){
if (!vis[j] && (x==0||dist[j] < dist[x]))
x = j;
}
vis[x] = 1;
for (int y = 1; y <= n; y++){
dist[y] = min(dist[y], dist[x] + edge[x][y]);
}
}
}
int main(){
memset(edge, 0x3f, sizeof(edge));
cin>>n>>m>>s;
for (int i = 0; i < m; i++){
int x, y, z;
cin >> x >> y >> z;
edge[x][y] = edge[y][x] = z;
}
dij(s);
for (int i = 1;i<= n; i++)
cout << dist[i] << " ";
return 0;
}
最小生成树
性质:设G=(V,E)是连通带权图,U是V的真子集。若(u,v)∈E,且u∈U,v∈V-U,且在所有这样的边中,(u,v)的权c[u][v]最小,那么一定存在G的一棵最小生成树,它以(u,v)为其中一条边。
Prim算法——时间复杂度为O(n2)(与Dijkstra一样)
设G=(V,E)是连通带权图,V={1,2,…,n}
- 基本思想:
① 首先置S={1}
② 然后,只要S是V的真子集,就作如下的贪心选择:选取满足条件i∈S,j∈V-S,且c[i][j]最小的边,将顶点j添加到S中
③ 这个过程一直进行到S=V时为止
④ 在这个过程中选取到的所有边恰好构成G的一棵最小生成树。 - 考虑如何有效地找出满足条件i∈S,j∈V-S,且权c[i][j]最小的边(i,j),较简单的办法是设置2个数组closest和lowcost。
closest[j]是j在S中的邻接顶点,它与j在S中的其它邻接顶点k相比有c[j][closest[j]]≤c[j][k]
lowcost[j]的值就是c[j][closest[j]] - 在Prim算法执行过程中,先找出V-S中使lowcost值最小的顶点j,然后根据数组closest选取边(j,closest[j]),最后将j添加到S中,并对closest和lowcost作必要的修改
// prim算法
int edge[100][100];
int closest[100];//以i为起点的边中的最小边的权重
int n, m, ans;
bool vis[100];
void prim(){
memset(vis, 0, sizeof(vis));
memset(closest, 0x3f, sizeof(closest));
closest[1] = 0;
for (int i = 1; i < n; i++){
int x = 0;
for (int j = 1;j<=n; j++){
if(!vis[j]&&(x==0||closest[j]<closest[x]))
x = j;
}
vis[x] = 1;
for(int y = 1; y <= n; y++){
if(!vis[y]){
closest[y] = min(closest[y], edge[x][y]);
}
}
}
}
int main(){
cin >> n >> m;
memset(edge, 0x3f, sizeof(edge));
for (int i = 0; i <m; i++){
int x, y, z;
cin >> x >> y >> z;
edge[x][y] = edge[x][y] = z;
}
prim();
for (int i = 2; i <= n;i++){
ans += closest[i];
}
cout << ans;
return 0;
}
Kruskal算法——时间复杂度为O(n+mlogm)
- 基本思想
① 首先将G的n个顶点看成n个孤立的连通分支。将所有的边按权值从小到大排序。
② 然后从第一条边开始,依边权递增的顺序查看每一条边,并按下述方法连接2个不同的连通分支:当查看到第k条边(v,w)时,如果端点v和w分别是当前2个不同的连通分支T1和T2中的顶点时,就用边(v,w)将T1和T2连接成一个连通分支,然后继续查看第k+1条边;如果端点v和w在当前的同一个连通分支中,就直接再查看第k+1条边
③ 这个过程一直进行到只剩下一个连通分支时为止。
Kruskal算法的贪心策略:将图中的边按权值由小到大排序,由小到大顺序选取各条边,若选某边后不形成回路,则将其保留作为树的一条边。
//Kruskal算法
struct rec{
int x, y, weight;
bool operator < ( const rec &a) const{
return weight < a.weight;
}
} edge[100];
int fa[10], n, m, ans;
int get(int x){
if(x==fa[x]) return x;
return fa[x]=get(fa[x]);
}
int main(){
cin >> n >> m;
for (int i = 1; i <= m;i++)
cin >> edge[i].x >> edge[i].y >> edge[i].weight;
sort(edge + 1, edge + m + 1);
for (int i = 1; i <= n; i++)
fa[i] = i;
for (int i = 1;i<= m; i++){
int x = get(edge[i].x);
int y = get(edge[i].y);
if(x==y)continue;
fa[x] = y;
ans += edge[i].weight;
}
cout<<ans<<endl;
return 0;
}
第五章 随机算法
随机算法的基本思想:
- 引入了随机因素;
- 在随机算法中:
不要求算法对所有可能的输入均正确计算;
只要求出现错误的可能性小到可以忽略的程度;
另外也不要求对同一输入,算法每次执行时给出相同的结果。 - 随机算法的特点:有不少问题,目前只有效率很差的确定性求解算法, 但用随机算法去求解,可以(很快地)获得相当可信的结果
- 优点:
① 对于某一给定的问题,随机算法所需的时间与空间复杂性,往往比当前已知的、最好的确定性算法要好
② 到目前为止设计出来的各种随机算法,无论是从理解上还是实现上,都是极为简单的
③ 随机算法避免了去构造最坏情况的例子
随机算法的分类
- Las Vegas算法:
- 在少数应用中,可能出现求不出解的情况
- 但一旦找到一个解,这个解一定是正确的
- 在求不出解时,需再次调用算法进行计算,直到获得解为止
- 对于此类算法,主要是分析算法的时间复杂度的期望值,以及调用一次产生失败(求不出解)的概率
- Monte Carlo算法
- 通常不能保证计算的结果总是正确,一般只能断定所给解的正确性不小于p(1/2<p<1)
- 通过算法的反复执行(即以增大算法的执行时间为代价),能够使发生错误的概率小到可以忽略的程度
- 由于每次执行的算法是独立的,故k次执行均发生错误的概率为(1-p)^k
- 对于判定问题(回答只能是“Yes”或“No”)
带双错的(two-sided error): 回答”Yes”或”No”都有可能错
带单错的(one-sided error):只有一种回答可能错 - Las Vegas算法可以看成是单错概率为0的Monte Carlo算法
Las Vegas和Monte Carlo算法的区别
- 两类随机算法的应用场景:
①在不允许发生错误的应用中(e.g.人造飞船、电网控制等),Monte Carlo算法不可以使用
②若小概率的出错允许的话,Monte Carlo算法比Las Vegas算法要节省许多时间,是人们常常采用的方法
Sherwood随机化方法(Las Vegas算法)
- 如果某个问题已经有了一个平均情况下较好的确定性算法,但是该算法在最坏情况下效率不高,此时引入一个随机数发生器(通常是服从均匀分布,根据问题需要也可以产生其他的分布),可将一个确定性算法改成一个随机算法,使得对于任何输入实例,该算法在概率意义下都有很好的性能。
- 如果算法(所给的确定性算法)无法直接使用Sherwood方法,则可以采用随机预处理的方法,使得输入对象服从均匀分布(或其他分布),然后再用确定性算法对其进行处理。所得效果在概率意义下与Sherwood型算法相同。
- Sherwood算法总能求得问题的一个解,且所求得的解是正确的。
- 当一个确定性算法在最坏情况和平均情况下的时间复杂度有较大差别时,可在确定性算法中引入随机性将其改造为Sherwood算法,以消除或减少问题的好坏输入实例间的差别。
随机算法求解实例
快速排序随机化版本
随机化选择主元
求第k小元素
- 算法实现
①在n个数中随机的找一个数A[i]=x, 然后将其余n-1个数与x比较,分别放入三个数组中:S1(元素均<x), S2(元素均=x), S3(元素均>x)。
②若|S1|≥k ,则调用Select(k,S1);
③若(|S1|+|S2|)≥k,则第k小元素就是x;
④否则就有(|S1|+|S2|)<k,此时调用Select(k-|S1|-|S2|,S3)。 - 定理:若以等概率方法在n个数中随机取数,则该算法用到的比较数的期望值不超过4n
如果假定n个数互不相同,如果有相同的数的话,落在S2中的可能性会更大,比较数的期望值会更小一些 - 时间复杂度为O(n)
Testing String Equality——Monte Carlo算法
- 问题描述:设A处有一个长字符串x(e.g. 长度为106),B处也有一个长字符串y,A将x发给B,由B判断是否有x=y。
- 算法:
①首先由A发一个x的长度给B,若长度不等,则x≠y;
②若长度相等,则采用“取指纹”的方法:
A对x进行处理,取出x的“指纹”,然后将x的“指纹”发给B;B也用同样的方法对y进行取"指纹",由B检查x的“指纹”是否等于y 的“指纹”;
若取k次“指纹”(每次取法不同),每次两者结果均相同,则认为x与y是相等的;
随着k的增大,误判率可趋于0。 - 常用的指纹:
令I(x)是x的编码,取Ip(x)=I(x) (mod p)作为x的指纹(p是一个小于M的素数)
误判率: - 若总是随机地去取一个小于M的素数p,则对于给定的x和y,Pr[failure] =(使得Ip(x)=Ip(y)但x≠y的素数p(p<M)的个数)/(小于M的素数的总个数)
- 误匹配的概率小于 1/n,当n很大时,误匹配的概率很小
- 设x≠y,如果取k个不同的小于2n2的素数来求Ip(x)和Ip(y)
- 即k次试验均有Ip(x)=Ip(y)但x≠y(误匹配)的概率小于 1/nk
- 当n较大、且重复了k次试验时,误匹配的概率趋于0
Pattern Matching
- 问题描述:
给定两个字符串:X=x1,…,xn,Y=y1,…,ym,看Y是否为X的子串? - Monte Carlo算法
①记X(j)=xjxj+1…xj+m-1(从X的第j位开始、长度与Y一样的子串)
②从起始位置j=1开始到j=n-m+1,不去逐一比较X(j)与Y,而仅逐一比较X(j)的指纹Ip(X(j))与Y的指纹Ip(Y)
③由于Ip(X(j+1))可以很方便地根据Ip(X(j))计算出来,故算法可以很快完成 - 时间复杂度:O(m+n)
- 计算Ip(Y)、Ip(X(1))及2m mod p的时间不超过**O(m)**次运算;
- Ip(X(j+1))的计算,只需用**O(1)**时间;
- 由于循环最多执行n-m+1次,故这部分的时间复杂度为O(n),于是,总的时间复杂性为O(m+n)。
误判率:失败的概率Pr[failure]<1/n,即失败的概率只与X的长度有关,与Y的长度无关 - Las Vegas算法
- 当Ip(Y)=Ip(X(j))时,不直接return j,而去比较Y和X(j)
- 即在return j之前加一个判断看Y和X(j)是否相等,相等则return j,否则继续执行循环
- 如果有子串X(j)与Y相匹配,该算法总能给出正确的位置(即算法出错的概率为0)
主元素问题——Monte Carlo算法
- 问题描述:设T[1:n]是一个含有n个元素的数组。当|{i|T[i]=x}|>n/2时,称元素x是数组T的主元素。对于给定的数组T,判定T数组中是否含有主元素。
- 步骤
①选一个不超过数组个数的随机数 n,然后以 n+1 作为元素下标选择元素,判断该元素是否为主元素;
②多次调用上述方法,若找到主元素则返回True。 - 计算时间和调用的次数相关
第六章 回溯法与分枝限界法
- 生成问题状态的说明:
扩展结点:一个正在产生儿子的结点称为扩展结点
活结点:一个自身已生成但其儿子还没有全部生成的节点称做活结点
死结点:一个所有儿子已经产生的结点称做死结点
回溯法与分支限界法的基本概念
- 回溯法
- 具有限界函数的深度优先生成法称为回溯
- 基本思想:
①针对所给问题,定义问题的解空间
复杂问题常有很多可能解,这些解构成解空间
确定正确的解空间很重要
②确定易于搜索的解空间结构
③以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索 - 常用剪枝函数:
用约束函数在扩展结点处剪去不满足约束的子树
用限界函数剪去得不到最优解的子树
- 分枝限界法
- 分枝限界法类似于回溯法,是在问题的解空间树上搜索问题解的算法
- 不同点:
①求解目标:回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分枝限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。
②搜索方式的不同:回溯法以深度优先的方式搜索解空间树,而分枝限界法以广度优先或以最小耗费优先的方式搜索解空间树。 - 基本思想:
分枝限界法常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树
①在分枝限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被加入活结点表中
②此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止 - 常见的两种分枝限界法:
①队列式(FIFO)分枝限界法
按照队列先进先出(FIFO)原则选取下一个节点为扩展节点
②优先队列式分枝限界法
按照优先队列中规定的优先级选取优先级最高的节点成为当前扩展节点 - 应用优先队列式分枝限界法求解具体问题时,应该根据具体问题的特点确定选用最大优先队列或者最小优先队列表示解空间的活结点表
回溯法求解时常见的两类解空间树
-
解空间树:
树的根节点位于第一层,表示搜索的初始状态
第二层的节点表示对解向量的第一个分量做出选择后到达的状态
第一层到第二层的边上标出对第一个分量选择的结果
依此类推,从树的根节点到叶子节点的路径就构成了解空间的一个可能解 -
子集树:当所给问题是从n个元素的集合S中找出S满足某种性质的子集时,相应的解空间树称为子集树。通常|S1|=…= |Sn|=C,各节点有相同数目子树,C=2时,子集树中共有2n个叶子,因此需要O(2^n)时间。
-
排列树:当所给问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。
通常|S1 |=n,…,|Sn |=1,所以排 列树中共有n!个叶子节点,需时间O(n!)
求解实例
01背包——子集树
- 问题描述:给定n种物品和一个背包。物品i的重量是Wi,其价值为Vi,背包的容量为C。应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
- 限制:在选择装入背包的物品时,对每种物品i只有2种选择,即装入背包或不装入背包。不能将物品i装入背包多次,也不能只装入部分的物品i
- 优先队列式分枝限界法:
①首先,对输入数据进行预处理,将各物品依其单位重量价值从大到小进行排列;节点的优先级为已装袋的物品价值加上剩下的最大单位重量价值的物品装满剩余容量的价值和。
②算法首先检查当前扩展结点的左儿子结点的可行性。如果该左儿子结点是可行结点,则将它加入到子集树和活结点优先队列中。当前扩展结点的右儿子结点一定是可行结点,仅当右儿子结点满足上界约束时才将它加入子集树和活结点优先队列。当扩展到叶节点时为问题的最优值。 - 步骤
①算法首先根据基于可行结点相应的子树最大价值上界优先级,从堆中选择一个节点(根节点)作为当前可扩展结点
②当扩展到叶节点时,算法结束,叶子节点对应的解即为问题的最优值
③检查当前扩展结点的左儿子结点的可行性
④如果左儿子结点是可行结点,则将它加入到子集树和活结点优先队列中
⑤当前扩展结点的右儿子结点一定是可行结点,仅当右儿子结点满足上界函数约束时,才将它加入子集树和活结点优先队列
样例:背包容量为10
TSP——排列树
- 某售货员要到若干城市去推销商品,已知各城市之间的路程(或旅费)。他要选定一条从驻地出发,经过每个城市一次,最后回到驻地的路线,使总的路程(或总旅费)最短(或最小)。
第七章 NP完全性
- 判定问题:回答是“Yes”或“No”
- 一台k带DTM(确定性Turing机)根据其当前所在状态及k个读写头当前读到的字符唯一地确定下一步的动作
与DTM不同的是,NDTM的每一步动作允许有若干个选择
P、NP、NPC、NP Hard的定义及相互关系
- P 问题是多项式时间内可解的问题
- NP 问题是多项式时间内可验证一个解的问题。
所有 NP 问题都是判定问题 - 问题 A 是一个 NPC 问题,则有
A 属于 NP 问题
对任意属于 NP 问题的 B,都可在多项式时间内规约到问题 A
若某个 NPC 问题能在多项式时间内被解,则所有 NP 问题均可在多项式时间内被解,从而 NP = P - 证明npc问题的思路是:
①先证明它至少是一个NP问题
②再证明其中一个已知的NPC问题能约化到它。 - 问题 A 是一个 NP-hard 问题,则有
问题 A 不一定是一个 NP 问题
所有 NPC 问题都可以在多项式时间内转化为 A
NPC 问题一定是 NP-hard 问题
NPC问题实例
- k-团问题:给定一个无向图G=(V,E)和一个正整数k,判定图G是否包含一个k团
- 顶点覆盖:
顶点覆盖的最优化问题:在一个无向图G中,找一个顶点数最少的顶点集,满足:任一条边的两个顶点中至少有一个在此集合中
顶点覆盖的判定问题:无向图G中是否存在顶点数为k的顶点覆盖? - 子集和问题:有一个数集A={a1,a2,…,an}及一个目标数S,问A中是否能找出一个子集A’,使得A’中元素之和为S?
第八章 近似算法
近似算法的分类不做要求
所有的NP完全问题,均未能找到多项式时间的算法,故当问题规模较大时,求得最优的精确解的可能性很小
在此情况下,往往退而去求比最优精确解稍差一点的解作为问题的近似答案
近似算法的性能
- 若一个最优化问题的最优值为c*,求解该问题的一个近似算法求得的近似最优解相应的目标函数值为c,则将该近似算法的近似比定义为max{c*/c, c/ c*},近似比不会<1
- 在通常情况下,近似比是问题输入规模n的一个函数ρ(n),即 max{c*/c, c/ c*} ≤ ρ(n)
近似算法实例
- 装箱问题
- 问题描述:设有n个物体u1,u2,…,un,每个物体的体积不超过1。另外,有足够多的、体积为1的箱子。箱子、物体均是长方体且截面相同。问如何装箱,使得所用箱子数最少?
- 记号I:表示某一问题的任一实例
- OPT(I):表示该实例的最优解
- First-Fit(FF)算法
- 从排在最前面的箱子开始,对每个箱子剩余的体积逐一进行检查,一旦碰到第一个能够装进当前物体的箱子时,就立即把该物体装入这个箱子。对每个物体反复执行上述程序。
- 算法的最坏时间复杂度:O(n2)
- FF算法满足:对于任何装箱实例I,都有FF(I)≤2OPT(I)
更为准确地,对所有装箱问题的实例I,都有FF(I)≤⌈17/10 OPT(I)⌉,且存在OPT(I)任意大的实例I,使得FF(I)≥17/10(OPT(I)-1)
- Next-Fit(NF)算法
- 先把第一个空箱置为当前箱。然后依次把物品u1,u2,…,un按下列方式装箱:若当前所指的箱子里放得下ui,则把ui放入箱中;若放不下则把ui放入下一个空箱,把当前指针指向(放ui的)该箱。
- 算法的最坏时间复杂度:O(n)
- NF算法满足:对于任何装箱实例I,都有NF(I)≤2OPT(I)-1
- Best-Fit(BF)算法
- FF算法的修改:在已装有物品的箱子中,找一个既能放下ui、又使得其剩余空间最小的箱子来放ui。
- 表面上看起来该算法要比FF法更能充分利用空间,但实际上,Johnson等人证明了BF法在最坏情况下的性能,本质上与FF法相同。
- FFD(First-Fit Decreasing)算法
- 先将所有物品从大到小排序,然后再使用FF法
- 对一切装箱实例I,有FFD(I)≤⌈4/3 OPT(I)⌉,当OPT(I)=3k+1时,有FFD(I)≤ ⌊4/3 OPT(I)⌋
- 对所有装箱问题的实例I,有FFD(I)≤ 11/9OPT(I)+1
- 顶点覆盖问题
- 算法的近似比为2。
- 算法流程:
①Cset用来存储顶点覆盖中的各顶点,初始为空。
②从边集e1中选一条边(u,v),将u、v加入cset中,并删去与u、v关联的边
③重复上述过程,直到e1为空。
- TSP问题
- 近似比为2
- TSP的特殊性质:
①代价函数c往往具有三角不等式性质,即对任意的3个顶点u,v,w∈V,有:c(u,w)≤c(u,v)+c(v,w)
②当图G中的顶点就是平面上的点,任意2顶点间的代价就是这2点间的欧氏距离时,代价函数c就具有三角不等式性质 - 满足三角不等式性质的旅行商问题:对于给定的无向图G,可以利用找图G的最小生成树的算法设计找近似最优的旅行售货员回路的算法
- 当代价函数满足三角不等式时,算法找出的路径的代价不会超过最优路径的代价的2倍