第八章 贪心法
贪心法(greedy method)把一个复杂问题分解为一系列较为简单的局部最优选择,每一步选择都是对当前解的一个扩展,直至获得问题的完整解。
贪心法的典型应用是求解最优化问题,而且对许多问题都能得到整体最优解,即使不能得到整体最优解,通常也是最优解的很好近似。
应用贪心法的关键是确定贪心选择策略,这种贪心策略只是根据当前信息作出最好的选择,不去考虑在后面看来这种选择是否合理。(活在当下)
可以用贪心法求解最优解的问题一般具有两个重要的性质:
- 最优子结构性质(optimal substructure property):一个问题的最优解包含其子问题的最优解,也称此问题满足最优性原理。
- 贪心选择性质(greedy selection property):问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来得到。
------------------------------------------------------------------------------------------------------------------------
付款问题
付款问题(payment problem)要求找到一个付款方案,使得付出的货币张数最少。
------------------------------------------------------------------------------------------------------------------------
TSP问题
TSP问题的贪心策略可以采用最近邻点策略(Prim算法):从任意城市出发,每次在没有到过的城市中选择最近的一个,直至经过了所有城市,最后回到出发城市。
算法共进行 n-1 次贪心选择,每一次选择都需要查找满足贪心条件的最短边
时间复杂度:O(n^2)
------------------------------------------------------------------------------------------------------------------------
图着色问题
求无向连通图G=(V, E)的最小色数 k,使得用 k 种颜色对 G 中的顶点着色,可使任意两个相邻顶点着不同颜色
算法ColorGraph需要试探 k 种颜色,每种颜色需要对所有顶点进行冲突测试,设无向图有 n 个顶点,则
时间复杂度:O(k×n)
顶点的编号从 1 到 2n,当 i 是奇数时,顶点 i 与除了顶点 i+1 之外的其他所有编号为偶数的顶点邻接,当 i 是偶数时,顶点 i 与除了顶点 i-1 之外的其他所有编号为奇数的顶点邻接,这样的图称为二部图
考虑顶点顺序为1、 3、…、2n-1、2、4、…、2n得到最优解 2
考虑顶点顺序为1、2、3、…,、2n得到的解是 n
------------------------------------------------------------------------------------------------------------------------
最小生成树
最小连通子图(也称为生成树或连通分量)是指一个包含图中所有顶点且边数最少的连通子图。在无向图中,最小连通子图通常是一个树结构,即不存在环且所有顶点都相互连通。
连通图(从图中任意一个顶点出发,通过图中的边可以到达图中的其他所有顶点)的生成树是包含全部顶点的一个极小连通子图(含有n-1条边,多—构成回路;少—不连通)(n个顶点,无向连通图最少n-1条边,有向连通图最少n条边——成环)
极大连通子图:包含边最多的连通子图
连通图:任意两个顶点连通。
注意:连通图不是完全图,完全图要求必须每个点之间有边
连通分量就是极大连通子图,连通分量是无向图的概念,而强连通分量是有向图里的概念。
生成树:图全部顶点所确定的极小连通子图即为连通图的生成树。
生成树的代价:在无向连通网中,生成树上各边的权值之和
最小生成树:在无向连通网中,代价最小的生成
最小生成树问题的贪心策略可以采用最近邻点策略(Prim算法):任选一个顶点,并以此建立生成树的根结点,每一步的贪心选择把不在生成树的最近顶点添加到生成树中。
存储候选最短边集:
数组adjvex[n]:表示候选最短边的邻接点
数组lowcost[n]:表示候选最短边的权值
含义是:候选最短边(i, j)的权值为 w,其中 i∈V-U,j∈U
时间复杂度:O(n^2)
------------------------------------------------------------------------------------------------------------------------
背包问题
给定 n 个物品和一个容量为 C 的背包,物品 i(1 ≤ i ≤ n)的重量是 wi,其价值为 vi,背包问题(knapsack problem)是如何选择装入背包的物品,使得装入背包中物品的总价值最大。
想法:至少有三种看似合理的贪心策略:
(1)选择价值最大的物品。
(2)选择重量最轻的物品。
(3)选择单位重量价值最大的物品。
------------------------------------------------------------------------------------------------------------------------
活动安排问题
设有 n 个活动的集合 E={1, 2, …, n},其中每个活动都要求使用同一资源,而在同一时间只有一个活动能使用这个资源。
若区间[si, fi )与区间[sj, fj )不相交,则称活动 i 与活动 j 是相容的。活动安排问题(activity arrangement problem)要求在所给的活动集合中选出个数最多的相容活动。
想法:至少有两种看似合理的贪心策略,能够安排尽量多的活动:
(1)最早开始时间。
(2)最早结束时间。
将活动按结束时间从小到大排序
时间复杂度:O(nlog2n)
------------------------------------------------------------------------------------------------------------------------
埃及分数
古埃及人只用分子为 1 的分数,在表示一个真分数时,将其分解为若干个埃及分数之和,例如:7/8 表示为 1/2 + 1/3 + 1/24。埃及分数问题(Egypt fraction)要求把一个真分数表示为 最少 的埃及分数之和的形式。
输入:真分数的分子 A 和分母 B
输出:最少的埃及分数之和
1. E = B/A + 1;
2. 输出 1/E;
3. A = A * E – B; B = B * E;
4. 求 A 和 B 的最大公约数 R,如果 R 不为 1,则将 A 和 B 同时除以 R;
5. 如果 A 等于 1,则输出 1/B,算法结束;否则转步骤 1 重复执行;
假设真分数是 m/n
时间复杂度:O(m)
------------------------------------------------------------------------------------------------------------------------
!证明贪心法求解背包问题获得的解是整体最优解
------------------------------------------------------------------------------------------------------------------------
第九章 动态规划法
动态规划(dynamic programming)是 20 世纪 50 年代美国数学家贝尔曼(Richard Bellman)为研究最优控制问题而提出,在计算机科学领域,动态规划法成为一种通用的算法设计技术用来求解多阶段决策 最优化问题。
最优化问题(optimization problem):有 n 个输入,问题的解由这 n 个输入的一个子集组成,这个子集必须满足某些事先给定的约束条件(constraint condition),满足约束条件的解称为问题的可行解(feasible solution)。
为了衡量可行解的优劣,通常以函数的形式给出一定的评价标准,这些标准函数称为目标函数(objective function,也称评价函数),目标函数的极值(极大或极小)称为最优值(optimal value),使目标函数取得极值的可行解称为最优解(optimal solution)。
多阶段决策过程:一个决策序列在不断变化的状态中产生的过程。具有n个输入的最优化问题,其求解过程划分为若干个阶段,每一阶段的决策仅依赖于前一阶段的状态,由决策所采取的动作使状态发生转移,成为下一阶段决策的依据。
动态规划法将待求解问题分解成若干个相互重叠的子问题,每个子问题对应决策过程的一个阶段,动态规划法的求解过程由以下三个阶段组成:
(1)划分子问题:将原问题的求解过程划分为若干个阶段,每个阶段对应一个子问题,并且子问题之间具有重叠关系;
(2)设计动态规划函数:根据子问题之间的重叠关系找到子问题满足的递推关系式,这是动态规划法的关键;
(3)填写表格:根据动态规划函数设计表格,以自底向上的方式计算各个子问题的最优值并填表,实现动态规划过程。
动态规划过程只能求得问题的最优值,如果要得到使目标函数取得极值的最优解,通常在动态规划过程中记录每个阶段的决策,再根据最优决策序列通过回溯构造最优解
------------------------------------------------------------------------------------------------------------------------
网格上的最短路径
给定一个包含正整数的 m×n 网格,每次只能向下或者向右移动一步,定义路径长度是路径上经过的整数之和。请找出一条从左上角到右下角的路径,使得路径长度最小
为了得到网格的最短路径,设p(m, n)记录每个网格的最短路径是由上方还是左方到达,即p(i, j)表示在计算d(i, j)的决策,并且有:
for (i = m - 1, j = n - 1; i > 0 || j > 0; ) //回溯求最优解
{
cout<<a[i][j]<<"<--";
if (path[i][j] == 0) i--;
else j--;
}
}
------------------------------------------------------------------------------------------------------------------------
最长公共子序列
子序列(subsequence):给定序列 X=(x1, x2,…, xm)和序列 Z=(z1, z2,…, zk),Z 是 X 的子序列当且仅当存在一个递增下标序列(i1, i2,…, ik),使得对于所有j=1, 2, …, k,有 (1 ≤ ij ≤ m)
公共子序列(public subsequence):给定两个序列 X 和 Y,当序列 Z 既是 X 的子序列又是 Y 的子序列时,称 Z 是序列 X 和 Y 的公共子序列
为了得到序列 X 和 Y 对应的最长公共子序列,设S(m, n)记载求解过程中的决策,其中S(i, j)表示在计算L(i, j)时的决策,并且有
设字符数组x[m+1]存储序列 X,字符数组y[n+1]存储序列 Y,均从下标 1 开始存放,字符数组z[k]存储最长公共子序列,数组L[m+1][n+1]存储最长公共子序列的长度,S[m+1][n+1]存储相应的状态
i = m; j = n; k = L[m][n]; //回溯,得到公共子序列
while (i > 0 && j > 0)
{
if (S[i][j] == 1) { z[--k] = x[i]; --i; --j; }
else if (S[i][j] == 2) --j;
else --i;
}
------------------------------------------------------------------------------------------------------------------------
0/1背包问题
设V(n, C)表示将 n 个物品装入容量为 C 的背包获得的最大价值。
考虑重叠子问题,设V(i, j)表示将 i(1 ≤ i ≤ n)个物品装入容量为 j(1 ≤ j ≤ C)的背包获得的最大价值,在决策 xi 时,已经确定了(x1, …, xi-1),有以下两种情况:
(1)背包容量不足以装入物品 i;
(2)背包容量能够装入物品 i,此时有两种选择:
① 把第 i 个物品装入背包;
② 第 i 个物品不装入背包。
显然,取二者中价值较大者作为把前 i 个物品装入容量为 j 的背包获得的最优值。得到如下递推式:
为了确定装入背包的具体物品,从V(n, C)开始进行回溯,如果V(n, C)>V(n-1, C),表明第 n 个物品被装入背包,前 n-1 个物品被装入容量为 C-wn 的背包中;否则,第 n 个物品没有被装入背包,前 n-1 个物品被装入容量为 C 的背包中。
------------------------------------------------------------------------------------------------------------------------
多段图的最短路径
多段图(multi-segment graph):设图G=(V, E)是一个带权有向图,顶点集合 V 划分成 k 个互不相交的子集 Vi(2≤k≤n, 1≤i≤k),使得 E 中任何一条边(u, v),必有 u∈Vi,v∈Vi+m(1≤i<k, 1<i+m≤k),则称图G为多段图,称 s∈V1 为源点,t∈Vk 为终点
------------------------------------------------------------------------------------------------------------------------
TSP问题
从后往前推
设d(k, { })表示从顶点 k 回到顶点 i(终点),显然有
另:从前往后也可以,从顶点 i出发得到终点顶点 k
------------------------------------------------------------------------------------------------------------------------
近似串匹配
设样本P=p1p2…pm,文本T=t1t2…tn,假设样本是正确的,对于一个非负整数 K,样本 P 和文本 T 的 K-近似匹配(K-approximate match)是指 P 和 T 在所有对应方式下的最小编辑错误数。这里的编辑错误(也称差别)是指下列三种情况之一:
(1)修改:T 与 P 中对应字符不同;
(2)删去:T 中含有一个未出现在 P 中的字符;
(3)插入:T 中不含有在 P 中出现的一个字符
(1)字符 pi 与 tj 对应,① pi=tj;② pi≠tj;
(2)字符 pi 为多余,即字符 pi 对应于 tj 后的空格,则总差别数为D(i-1, j)+1;
(3)字符 tj 为多余,即字符 tj 对应于 pi 后的空格,则总差别数为D(i, j-1)+1。
for (j = 1; j <= n; j++){ //填写每一列
for (i = 1; i <= m; i++){
if (P[i] == T[j]) D[i][j] = min(D[i-1][j-1] , D[i-1][j]+1, D[i][j-1]+1);
else D[i][j] = min(D[i-1][j-1]+1, D[i-1][j]+1, D[i][j-1]+1);
}
}
时间复杂度:O(n * m)
------------------------------------------------------------------------------------------------------------------------
最优二叉查找树
设记录集合{r1, r2, …, rn}的查找概率分别是{p1, p2, …, pn},求这 n 个记录构成的最优二叉查找树。最优二叉查找树(optimal binary search tree)是以 n 个记录构成的二叉查找树中具有最少 平均比较次数的二叉查找树,累加pi * ci(i=1~n)最小,其中 pi 是记录 ri 的查找概率,ci 是在二叉查找树中查找 ri 的比较次数
设 C(1, n) 是最优二叉查找树 T(1, n) 的平均比较次数。
考虑重叠子问题,设T(i, j) 是由记录{ri, …, rj}(1≤i≤j≤n)构成的二叉查找树,C(i, j)是这棵二叉查找树的平均比较次数,记录 rk 为 T(i, j) 的根结点,则 rk 可以是{ri, …, rj}的任一记录
------------------------------------------------------------------------------------------------------------------------
最优性原理
每个阶段的决策都是相对于初始决策所产生的当前状态,所有阶段的最优决策构成一个最优决策序列
各子问题的解都是相对于当前状态的最优解,整个问题的最优解是由各个子问题的最优解构成
------------------------------------------------------------------------------------------------------------------------
!证明最长公共子序列问题满足最优性原理
证:序列 X={x1, x2,…, xm}记为 Xm,序列 Y={y1, y2,…, yn}记为 Yn,序列 Xm 和 Yn的最长公共子序列 Z={z1, z2,…, zk}记为 Zk,显然有下式成立:
(1)若 xm=yn,则 zk=xm=yn,且 Zk-1 是 Xm-1 和 Yn-1 的最长公共子序列;
(2)若 xm≠yn 且 zk≠xm,则 Zk 是 Xm-1 和 Yn 的最长公共子序列;
(3)若 xm≠yn 且 zk≠yn,则 Zk 是 Xm 和 Yn-1 的最长公共子序列。
可见,两个序列的最长公共子序列包含了这两个序列前缀序列的最长公共子序列。因此,最长公共子序列问题满足最优性原理
------------------------------------------------------------------------------------------------------------------------
证明0/1背包问题满足最优性原理
------------------------------------------------------------------------------------------------------------------------
证明多段图的最短路径问题满足最优性原理。
证:设 s, s1, s2, …, sp, t 是从 s 到 t 的一条最短路径,从源点 s 开始,设从 s 到下一段的顶点 s1 已经求出,则问题转化为求从 s1 到t的最短路径,显然 s1, s2, …, sp, t 一定构成一条从 s1 到 t 的最短路径,如若不然,设 s1, r1, r2, …, rq, t 是一条从 s1 到t 的最短路径,则 s, s1, r1, r2, …, rq, t 将是一条从 s 到 t 的路径且比 s, s1, s2, …, sp, t的路径长度要短,从而导致矛盾。所以,多段图的最短路径问题满足最优性原理。
------------------------------------------------------------------------------------------------------------------------
数塔问题
设d(n, n)表示一个 n 层数塔,A(n, n)表示 n 层数塔的最大数值和。
考虑初始子问题,最下层的每个数值都是一个 1 层的数塔,最大数值和就是该数塔的数值,则有:
A(n, j) = d(n, j)(1≤j≤n)
考虑重叠子问题,设A(i, j)表示第 i 层每个数塔的最大数值和A(i, j)等于该塔顶数值d(i, j)与下一层两个子数塔最大数值和的较大值相加,即有如下递推式:
A(i, j) = d(i, j) + max{A(i+1, j), A(i+1, j+1)}(1≤i≤n-1, 1≤j≤i)
为了确定最大数值和的路径,设P(i, j)记载求解过程中的决策,即计算A(i, j)时选择的是A(i+1, j)还是A(i+1, j+1),并且有
------------------------------------------------------------------------------------------------------------------------
第十章 深度优先搜索
假设从顶点 u 出发,深度优先搜索(depth-first search)的基本思想是:访问顶点 u,然后从 u 的未被访问的邻接点中选取一个顶点 v,再从 v 出发进行深度优先搜索,直至图中所有和 u 有路径相通的顶点都被访问到
------------------------------------------------------------------------------------------------------------------------
山洞寻宝图
无向图中使用DFS
略
------------------------------------------------------------------------------------------------------------------------
城堡问题
某城堡被分割成 m×n(m ≤ 50,n ≤ 50)个方块,每个方块的四面可能有墙,“#”代表有墙,没有墙分割的方块连在一起组成一个房间,城堡外围一圈都是墙。如果 1、2、4 和 8 分别对应左墙、上墙、右墙和下墙,则可以用方块周围每个墙对应的数字之和来描述该方块四面墙的情况,请计算城堡一共有多少个房间,最大的房间有多少个方块?
例:(11)10 =(1011)2 说明第一个房间存在左墙、上墙和下墙
可以把方块看成顶点,相邻的方块之间如果没有墙,则在方块对应顶点之间连一条边,从而将城堡问题抽象为一个无向图。求城堡的房间个数,实际上就是求图中有多少个连通分量,求城堡的最大房间数,就是求最大连通分量包含的顶点数。
反复调用Dfs算法直至城堡中所有方块均被访问,则调用算法Dfs的次数就是图中连通分量的个数
如何判断顶点之间的邻接关系?
将方块的数值分别与 1、2、4、8 执行按位与操作,判断对应的二进制位是否为0
if ((room[i][j] & 1) == 0) Dfs(i, j-1);
if ((room[i][j] & 2) == 0) Dfs(i-1, j);
if ((room[i][j] & 4) == 0) Dfs(i, j+1);
if ((room[i][j] & 8) == 0) Dfs(i+1, j);
------------------------------------------------------------------------------------------------------------------------
问题的解空间树
解空间(solution space):所有可能的解向量构成了问题的解空间。将问题的可能解表示为满足某个约束条件的等长向量 X=(x1, x2, …, xn),其中分量 xi(1≤i≤n)的取值范围是某个有限集合 S={a1, a2, …, ak}
问题的解空间一般用解空间树(solution space tree,也称状态空间树)的方式组织,树的根结点位于第 1 层,表示搜索的初始状态,第 2 层的结点表示对解向量的第一个分量做出选择后到达的状态,第 1 层到第 2 层的 边上标出对第一个分量选择的结果 ,依此类推,从树的根结点到叶子结点的路径就构成了解空间的一个可能解
------------------------------------------------------------------------------------------------------------------------
回溯法的设计思想
回溯法(back track method)从根结点出发,按照 深度优先 搜索解空间树,对于解空间树的某个结点,如果该结点满足问题的约束条件,则进入该子树继续进行搜索,否则跳过以该结点为根的子树,也就是剪枝
与蛮力搜索相比,回溯法的“聪明”之处在于能适时回头
问题的解空间树是虚拟的,并不需要在搜索过程中构建一棵真正的树结构
回溯法本质上属于蛮力穷举,搜索具有指数阶个结点的解空间树,在最坏情况下,时间代价肯定为指数阶。
回溯法的有效性体现在当问题规模 n 很大时,在搜索过程中对问题的解空间树实行大量剪枝
------------------------------------------------------------------------------------------------------------------------
素数环问题
把整数{1, 2, …, 20}填写到一个环中,要求每个整数只填写一次,并且相邻的两个整数之和是素数。
对每个位置从 1 开始进行试探,约束条件是正在试探的数满足如下条件:
(1)与已经填写到素数环中的整数不重复;
(2)与前面相邻的整数之和是素数;
(3)最后一个填写到素数环中的整数与第一个填写的整数之和是素数。
for (i = 1; i >=1; ){ //扩展解分量x[i]
x[i] = x[i] + 1;
while (x[i] <= n)
if (Check(x, n, i) == 1) break; //x[i]填数满足约束条件
else x[i] = x[i] + 1; //试探下一个数
if (x[i] > n) x[i--] = 0; //回溯
else if (i < n - 1) i = i + 1; //填写下一个位置
else { //求解完毕,输出解
for (j = 0; j < n; j++)
cout<<x[j]<<" ";
return;
}
}
cout<<"问题无解";
------------------------------------------------------------------------------------------------------------------------
八皇后问题
任意两个皇后都不能处于同一行、同一列或同一斜线上
棋盘的每一行必须摆放一个皇后,n 皇后问题的可能解用向量(x1, x2, …, xn)表示,即第 i 个皇后摆放在第 i 行第 xi 列的位置(1 ≤ i ≤ n且1 ≤ xi ≤ n)。由于两个皇后不能位于同一列,所以,n 皇后问题的解向量必须满足约束条件xi≠xj。
可以将 n 皇后问题的 n×n 棋盘看成是矩阵,设皇后 i 和 j 的摆放位置分别是(i, xi) 和 (j, xj),则在棋盘上斜率为 -1 的同一条斜线上,满足条件 i-xi = j-xj;在棋盘上斜率为 1 的同一条斜线上,满足条件 i+xi = j+xj。综合上述两种情况,n 皇后问题的解必须满足约束条件:|i-j|≠|xi-xj|
模板:
for (i = 0; i >= 0; ){ //摆放皇后i
x[i]++; //在下一列摆放皇后i
while (x[i] < n && Place(x, i) == 1) //Place(x, i) == 1表示发生冲突
x[i]++;
if (x[i] == n) x[i--] = -1; //重置x[i],回溯
else if (i < n - 1) //尚有皇后未摆放
i = i + 1; //准备摆放下一个皇后
else { //得到一个解,输出
for (j = 0; j < n; j++)
cout<<x[j]+1<<" "; //打印列号从1开始
cout<<endl;
return; //只求出一个解
}
}
cout<<"无解"<<endl;
------------------------------------------------------------------------------------------------------------------------
图着色问题
给定无向连通图 G=(V, E),图着色问题(graph coloring problem)求最小的整数 m,用 m 种颜色对 G 的顶点着色,使得任意两个相邻顶点着色不同
遍历每一个顶点。对于每一个顶点,从第一种颜色开始尝试
------------------------------------------------------------------------------------------------------------------------
批处理作业调度
n 个作业{1, 2, …, n}要在两台机器上处理,每个作业必须先由机器 1 处理,再由机器 2 处理,机器 1 处理作业i所需时间为 ai,机器 2 处理作业 i 所需时间为 bi(1 ≤ i ≤ n),批处理作业调度问题(batch-job scheduling problem)要求确定这 n 个作业的最优处理顺序,使得从第 1 个作业在机器 1 上处理开始,到最后一个作业在机器 2 上处理结束所需时间最少
sum1[i] = sum1[i-1] + a[x[i]]
sum2[i] = max(sum1[i], sum2[i-1] +b[x[i]]
最短完成时间bestTime = MAX
3.2 处理作业 x[i]:
3.2.1 sum1[i]=sum1[i-1]+ a[x[i]];
3.2.2 sum2[i]=max{sum1[i], sum2[i-1]}+ b[x[i]];
3.2.3 若 sum2[i] < bestTime,则转步骤 3.4,否则转步骤 3.3 实施剪枝;
3.3 回溯,x[i] = -1,i--,转步骤 3.1 重新处理第 i 个作业;
3.4 若尚有作业没被处理,则 i++,转步骤 3.1 处理下一个作业;
3.5 若 n 个作业已全部处理,则输出一个解,算法结束;
------------------------------------------------------------------------------------------------------------------------
哈密顿回路
要求从一个城市出发,经过每个城市恰好一次,然后回到出发城市。
遍历每一次选择。对于每一种选择,从第一个点开始判断
------------------------------------------------------------------------------------------------------------------------
第十一章 广度优先搜索
广度优先搜索(breadth-first search)以顶点 u 为起始点,依次访问和 u 有路径相通且路径长度为 1、2、…的顶点。广度优先搜索的基本思想是:访问顶点 u,然后依次访问 u 的各个未被访问的邻接点 v1、v2、…、vk,再分别从 v1、v2、…、vk 出发依次访问它们未被访问的邻接点,直至图中所有与顶点 u 有路径相通的顶点都被访问到。
为了使“先被访问顶点的邻接点”先于“后被访问顶点的邻接点”被访问,设置队列存储已被访问的顶点
------------------------------------------------------------------------------------------------------------------------
农夫抓牛
假设农夫和牛都位于数轴上,农夫位于点 N,牛位于点 K(K>N),农夫有以下两种移动方式:(1)从点 X 移动到 X-1 或 X+1,每次移动花费一分钟;(2)从点 X 移动到点 2X,每次移动花费一分钟,假设牛没有意识到农夫的行动,站在原地不动,农夫最少要花费多长时间才能抓住牛?
这是一个最少步数问题,适合用广度优先搜索。将数轴上每个点看作是图的顶点,对于任意点 X,有两条双向边连到点 X-1 和 X+1,有一条单向边连到点 2X,则农夫抓牛问题转化为求从顶点 N 出发到顶点 K 的最短路径长度。
2. 将起点 N 放入队列 Q,修改标志flag[N] = 1,right = rear;
3. 当队列 Q 非空时执行下述操作:
3.1 u = 队列 Q 的队头元素出队;
3.2 如果 u 等于 K,输出到达顶点 u 的步数steps,算法结束;
3.3 依次扩展结点 u 的每个子结点:
3.3.1 v = u-1;如果 flag[v] 等于 0,将 v 入队,修改标志 flag[v] = 1;
3.3.2 v = u+1;如果 flag[v] 等于 0,将 v 入队,修改标志 flag[v] = 1;
3.3.3 v = u+u;如果 flag[v] 等于 0,将 v 入队,修改标志 flag[v] = 1
front = rear = -1;
Q[++rear] = N; flag[N] = 1;
while (front != rear){... ...}
------------------------------------------------------------------------------------------------------------------------
骑士旅行
在一个国际象棋的棋盘上,给定起点(x1, y1)和终点(x2, y2),计算骑士从起点到终点最少需要移动的步数。
循环变量 k 从 0~7 从 8 个方向扩展
------------------------------------------------------------------------------------------------------------------------
A*算法的设计思想
A*(A-Star)算法是求最短路径非常有效的一种搜索方法,也是解决搜索问题常用的启发式算法。
启发式算法(Heuristic Algorithms)是一种基于经验和直觉的求解问题的技术,旨在在可接受的时间内找到一个足够好的近似解。常见的包括贪心法、爬山法、模拟退火法、遗传算法等。
所谓启发式搜索是通过启发式函数(heuristic function,也称估价函数)引导算法的搜索方向,以达到减少搜索结点的目的。
启发式函数通常利用与问题有关的某种启发信息,对于路径搜索问题,结点就是搜索空间的状态,启发信息通常是距离,启发式函数表示为:
f(n) = g(n) + h(n)
其中,f(n)是从初始状态到目标状态的估计代价;
g(n)是从初始状态到状态n的实际代价;
h(n)是从状态n到目标状态最佳路径的估计代价。
A*算法以优先队列形式组织的open表,存储搜索过程中经过的状态,每次从open表中选取 f(n) 值最小(或最大)的结点作为下一个待扩展的结点。
A*算法能够尽快找到或最优解的关键在于函数h(n)的选取,通过预估h(n)的值,降低搜索走弯路的可能性,加快搜索速度。
当h(n)始终为 0,则由g(n)决定结点的优先级,此时退化为Dijkstra算法(意味着在评估节点时完全不考虑关于目标的启发信息了,在这种情况下,只依靠 g(n),也就是从起始节点到当前节点的实际代价来决定节点的优先级。迪杰斯特拉算法主要就是根据节点已有的路径代价来进行节点的选择和扩展。)
(Prim 算法保证的是形成生成树后,n-1条边的总权值是最小的,而Dijkstra 算法保证的是从根节点到该点的路径是最短的)
A * 算法在理论上能保证找到最短路径的前提是启发式函数 是可接受的(admissible),即对于所有节点,都小于等于从节点到目标节点的实际代价。h(n)<=d(n)
以d(n)表示状态n到目标状态的最短距离,则h(n)的选取大致有如下三种情况:
- h(n) < d(n),搜索的结点数较多,搜索范围较大,效率较低,但能保证得到最优解。h(n)值越小,搜索越多的结点,也就导致算法效率越低。(那么就很需要几乎遍历所有的可能才能找到一种好的决策,特殊情况h(n)=0)(比如你站在一个十字路口,你估计往左边走离目标可能就只有 100 米,但实际上可能有 300 米。于是你就先往左边走了,走了一段后发现还没到,这时你又重新估计,可能又觉得另一个方向离目标只有几十米,你又转向那个方向走。这样你就不断地在一些不是最优的方向上探索)
(2)h(n) = d(n),搜索将严格沿着最短路径进行,此时的搜索效率最高。在没有达到终点之前,很难确切计算出距离终点还有多远。
(3)h(n) > d(n),搜索的结点数较少,搜索范围较小,效率较高,但不能保证得到最优解。(比如你看到一条路,稍微估算一下就觉得距离目标还特别远,可能就轻易放弃走这条路去探索。这样你就可能会快速地跳过一些实际上可能是比较好的路径,而只是集中精力去走那些你觉得距离目标“相对较近”的路,虽然这样会让你很快地朝着目标的大致方向前进,但是可能会错过一些隐藏的更短的路径)
因为启发仅仅是下一步将要采取措施的一个猜想,这个猜想常常根据经验和直觉来判断,所以启发式搜索可能出错。
------------------------------------------------------------------------------------------------------------------------
八数码问题
也称重排九宫问题,在一个 3×3 的方格盘上,放有 1~8 个数码,余下一格为空,空格四周上下左右的数码可以移动到空格。给定一个八数码问题的初始状态,要求找到一个移动序列到达目标状态
应用 A*算法求解八数码问题的关键是确定启发式函数,可以将实际代价函数g(n)定义为解空间树中从根结点到该状态的路径长度(即移动次数),估计代价函数h(n)定义为该状态与目标状态不相符的数码个数。
------------------------------------------------------------------------------------------------------------------------
多段图的最短路径问题
G=(V, E)是一个带权有向连通图,如果把顶点集合 V 划分成k个互不相交的子集 Vi(2≤k≤n, 1≤i≤k),使得 E 中的任一条边(u, v),必有 u∈Vi,v∈Vi+m(1≤i<k, 1<i+m≤k),称 s∈V1 为源点,t∈Vk 为终点。
多段图的最短路径问题(multi-segment graph shortest path problem)是求从源点到终点的最小代价路径。
------------------------------------------------------------------------------------------------------------------------
任务分配问题
把 n 项任务分配给 n 个人,每个人完成每项任务的成本不同,任务分配问题(task allocation problem)要求总成本最小的最优分配方案。
将g(n)定义为已经分配的任务成本,h(n)定义为其余任务的最小分配成本。任务分配问题可以采用成本矩阵表示,令 cij 表示人员i分配了任务 j(1≤i, j≤n),一般情况下,假设当前已对人员 1~i 分配了任务,启发式函数定义如下:
其实就是把整个解空间遍历了一遍
------------------------------------------------------------------------------------------------------------------------
限界剪枝法的设计思想
限界剪枝法(bound and purning method)首先确定一个合理的限界函数(bounding function),并根据限界函数确定目标函数的界[down, up]。然后,按照 广度优先策略 搜索问题的解空间树。
在分支结点上,依次扩展该结点的所有孩子结点,分别估算这些孩子结点的目标函数值,如果某孩子结点的目标函数值超出目标函数的界,则将其丢弃,因为从这个结点生成的解不会比目前已经得到的解更好;否则,将其加入open表中。依次从open表中选取使目标函数取得极值的结点成为当前扩展结点,重复上述过程,直至找到最优解。
因为限界函数常常基于问题的目标函数而确定,所以,限界剪枝法适用于求解最优化问题。
实质上,限界剪枝法是在A*算法的基础上加入剪枝操作,减少open表中的结点数量,从而提高搜索效率。
------------------------------------------------------------------------------------------------------------------------
0/1背包问题
假设 n 种物品已按单位价值由大到小排序,可以采用贪心法求解 0/1背包问题的一个下界。
如何求得 0/1背包问题的一个合理上界呢?考虑最好情况,背包中装入的全部是第 1 个物品且可以将背包装满,则可以得到一个非常简单的计算方法:
ub=C×(v1/w1)
------------------------------------------------------------------------------------------------------------------------
TSP问题
首先确定目标函数的界[down, up]。
采用贪心法确定TSP问题的一个上界
对于无向图的代价矩阵,把矩阵中每一行最小的元素相加,可以得到一个简单的下界。
但是还有一个信息量更大的下界:考虑TSP问题的一个完整解,路径上每个城市都有两条邻接边,一条是进入这个城市的,另一条是离开这个城市的,那么,把矩阵中每一行最小的两个元素相加再除以 2,就得到了一个合理的下界。(下界对应的解可能不是一个可行解)
对于下图,采用贪心法求得近似解为为 A-->C-->E-->D-->B-->A,路径长度为 1+2+3+7+3=16,这可以作为TSP问题的上界。把矩阵中每一行最小的两个元素相加再除以 2,得到TSP问题的下界:[(1+3)+(3+6)+(1+2)+(3+4)+(2+3)]/2=14。于是,得到了目标函数的界[14, 16]
------------------------------------------------------------------------------------------------------------------------
圆排列问题
给定 n 个圆的半径序列,将这些圆放到一个矩形框中,各圆与矩形框的底边相切,则圆的不同排列会得到不同的排列长度。要求找出具有最小长度的圆排列。
采用限界剪枝法求解圆排列问题,关键是设计限界函数,以便在搜索过程中选择使目标函数取得极值的结点优先进行扩充。
想法:假设已排列了圆(i1, i2, …, ik-1),则在排列圆 ik 时,目标函数可能取得的极小值 Lk 的计算公式如下,其中各记号定义如下:
xk:第 k 个位置所放圆的圆心坐标,规定第 1 个圆的圆心为坐标原点,即 x1=0。
dk:第 k 个位置所放圆的圆心坐标与第 k-1 个位置所放圆的圆心坐标的差。
Lk:第 1~k 个位置放置圆后,可能得到的目标函数的极小值。
------------------------------------------------------------------------------------------------------------------------
深度优先搜索(DFS)
特性:
使用栈(递归调用栈或显式栈):DFS 利用栈结构来记录待访问的节点。
尽可能深入:DFS 会沿着一条路径尽可能深入,直到不能再深入为止,然后回溯。
空间复杂度较高:在最坏情况下,需要栈空间与图的深度成正比。
可能找到第一个解:DFS 在某些情况下会更快地找到第一个解(例如,在解谜游戏中)。
适用场景:
连通性问题:例如判断图是否连通,或找到图中的环。
路径查找:当需要找到从起点到终点的任意一条路径时(可能不是最短路径)。
拓扑排序:在有向无环图(DAG)中进行拓扑排序。
解决迷宫或拼图问题:DFS 更适合在这些问题中快速找到解决方案(虽然不一定是最优解)。
生成排列和组合:通过递归进行回溯,DFS 可以有效地生成所有可能的排列和组合。
------------------------------------------------------------------------------------------------------------------------
广度优先搜索(BFS)
特性:
使用队列:BFS 利用队列结构来记录待访问的节点。
逐层扩展:BFS 从起始节点开始,逐层向外扩展,访问所有相邻节点,然后再访问这些相邻节点的未访问邻居。
空间复杂度较高:在最坏情况下,需要队列空间与图的宽度(即节点的最大度数)和深度成正比。
找到最短路径:BFS 可以保证找到从起点到终点的最短路径(如果权值相等)。
适用场景:
最短路径问题:在无权图中找到从起点到终点的最短路径。
网络传播模型:例如模拟病毒在网络中的传播,或消息在社交网络中的扩散。
层次遍历:在树或图结构中,按层次顺序访问节点(如二叉树的层次遍历)。
查找最小生成树:在某些特定情况下,BFS 可以用于构建最小生成树(如Prim算法在某些图上的变种)。
迷宫求解:虽然DFS也可以用于迷宫求解,但BFS可以确保找到最短路径。
------------------------------------------------------------------------------------------------------------------------
DFS和BFS总结
DFS 更适合需要深入探索的问题,以及当解的存在性比最优性更重要时。
BFS 更适合需要逐层扩展的问题,以及当寻找最短路径或最优解时。
选择DFS还是BFS,通常取决于问题的具体需求以及图的特性。
------------------------------------------------------------------------------------------------------------------------
限界剪枝法的关键问题
限界剪枝法的较高效率是以付出一定计算代价为基础的,其工作方式也造成了算法设计的复杂性,应用限界剪枝法的关键问题如下:
(1)确定合适的限界函数。好的限界函数要求:① 计算简单;②保证最优解在搜索空间中;③能在搜索的早期对超出目标函数界的结点进行丢弃,减少搜索空间,从而尽快找到问题的最优解。
(2)open表的存储结构。为了能快速在open表中选取使目标函数取得极值的结点,通常采用优先队列存储open表。如果open表的结点数不是很多,也可以简单的采用数组存储。
(3)确定最优解的各个分量。限界剪枝法跳跃式处理搜索空间中的结点,需要对每个扩展结点保存该结点到根结点的路径,或者在搜索过程中构建搜索经过的树结构。
------------------------------------------------------------------------------------------------------------------------
批处理作业调度问题
给定 n 个作业的集合 J={J1, J2, …, Jn},每个作业都有 3 项任务分别在 3台机器上完成,作业 Ji 需要机器 j 的处理时间为 tij(1 ≤ i ≤ n, 1 ≤ j ≤ 3),每个作业必须先由机器1处理,再由机器 2 处理,最后由机器 3 处理。批处理作业调度问题(batch-job scheduling problem)要求确定这 n 个作业的最优处理顺序,使得从第 1 个作业在机器 1 上处理开始,到最后一个作业在机器 3 上处理结束所需的时间最少。
批处理作业的一个最优调度应使机器 1没有空闲时间,且机器 2 和机器 3 的空闲时间最小。
例如,设J={J1, J2, J3, J4}是 4 个待处理的作业,需要的处理时间如图所示。若处理顺序为(J4, J1, J3, J2),则从作业 4 在机器 1处理到作业 2 在机器 3 处理完成的时间 41,这可以作为批处理作业调度问题的上界。(7+8+10+9+5+2=41)
如何估算部分解的下界呢?考虑理想情况,机器 1 和机器 2 无空闲,最后处理的恰好是在机器 3 上处理时间最短的作业。例如,以作业 Ji 开始的处理顺序,估算处理所需的最短时间是:
------------------------------------------------------------------------------------------------------------------------
第十二章 问题的复杂性
计算机科学的研究目标——用计算机来求解人类所面临的各种问题,问题本身的内在复杂性决定了求解这个问题的算法的计算复杂性。
NP完全理论从计算复杂性的角度研究问题的分类以及问题之间的关系,从而为算法设计提供指导。
从字源上考察:计算的原始含义是利用计算工具进行计数。
计:从言从十,有数数或计数的含义;
算:从竹从具,指计算工具。
直观的计算:数的加减乘除;函数的微分、积分;微分方程的求解;定理的证明推导等等。
计算的实质:从一个符号串 f(输入)得出另一个符号串 g(输出)
计算:计算者(人或机器)对一条可以无限延长的工作带上的符号串执行指令,一步一步地改变工作带上的符号串,经过有限步骤,最后得到一个满足预先规定的符号串的变换过程。
一条无限长的工作带:工作带上的每个单元可以存放一个符号;所有允许出现的符号属于一个预先规定好的字母表。
一个读写头:读写头可以左移一个单元、右移一个单元或者保持不动。
一个控制器:控制器在每个时刻处于一定状态,当读写头从工作带上读出一个符号后,控制器就根据这个符号和当时的机器状态,指挥读写头进行读写或者移动,并决定是否改变机器状态。
计算开始时,将输入符号串放在工作带上,每个单元放一个输入符号,其余单元都是空白符,控制器处于初始状态,读写头扫描工作带上的第一个符号,控制器决定下一步的动作。如果对于当前的状态和所扫描的符号,没有下一步的动作,则图灵机就停止计算,处于终止状态。
在计算的每一步,控制器处于某个状态,读写头扫描工作带的某个单元,控制器根据当前的状态和被扫描单元的内容,决定下一步的执行动作:
(1)把当前单元的内容改写成另一个符号;
(2)使读写头停止不动、向左或向右移动一个单元;
(3)使控制器转移到某一个状态。
------------------------------------------------------------------------------------------------------------------------
图灵机
(Turing Machine)是由英国数学家阿兰·麦席森·图灵(Alan Turing)于1936年提出的一种抽象的计算模型。
1、定义:图灵机是一种虚拟的机器,用于模拟计算和决策过程。它通过将人们使用纸笔进行数学运算的过程进行抽象,从而用一个虚拟的机器替代人们进行数学运算。
组成部分:图灵机主要由一条无限长的纸带、一个读写头、一套控制规则和一个状态寄存器组成。纸带被划分为一个个的小格子,每个格子上包含一个符号;读写头可以在纸带上左右移动,读取或修改纸带上的符号;控制规则根据当前机器状态和读写头所指的格子上的符号来确定读写头的下一步动作;状态寄存器则用于保存图灵机当前所处的状态。
2、工作原理:
运作方式:图灵机通过读写头在纸带上移动,读取和修改符号,并根据控制规则执行一系列指令来模拟计算过程。读写头根据操作的指令移动到纸带上的下一个方格,准备执行下一步操作。图灵机会不断重复执行这些步骤,直到满足终止条件。
终止条件:终止条件可以是控制器进入某个特定的状态,或者满足某个特定的判断条件。
3、重要性与意义
理论基础:图灵机为计算机科学提供了重要的理论基础,证明了通用计算理论,肯定了计算机实现的可能性,并给出了计算机应有的主要架构。
计算能力:尽管图灵机是一个简单的抽象模型,但它可以模拟计算机的任何算法,无论这个算法有多复杂。因此,图灵机被视为现代计算机的形式模型。
停机问题:图灵机还引出了著名的停机问题,即判断任意一个程序是否会在有限的时间之内结束运行的问题。这是一个不可解的问题,体现了图灵机模型的深刻性和复杂性。
4、应用与变体
应用领域:图灵机的思想和方法在计算机科学、人工智能、数学等领域有着广泛的应用。例如,现代电子计算机可以看作是对图灵机的一种模拟。
变体:图灵机有许多变体,如多带图灵机、非确定型图灵机、交替式图灵机等。尽管这些变体的具体实现方式有所不同,但它们的计算能力都是等价的,即它们识别同样的语言类。
综上所述,图灵机是一种具有深远影响的抽象计算模型,它不仅为计算机科学提供了坚实的理论基础,还在多个领域发挥着重要作用。
------------------------------------------------------------------------------------------------------------------------
构造一个识别符号串ω=anbn(n≥1)的图灵机
控制器的指令序列对应一个状态转移图,计算就是在执行指令的过程进行状态的变化,也是变换符号的过程。
图灵模型在一定程度上反映了人类最基本的、最原始的计算能力,它的基本动作非常简单、机械、确定。因此,可以用真正的机器来实现图灵机。
程序并非必须顺序执行,指令中关于下一状态的指定,实际上表明指令可以不按程序中所表示的顺序执行。这意味着,虽然程序只能按线性顺序来表示指令序列,但程序的实际执行可以与表示的顺序不同。
------------------------------------------------------------------------------------------------------------------------
可计算问题与不可计算问题
Turing论题:一个问题是可计算的当且仅当它在图灵机上经过有限步骤最后得到正确的结果。
Turing论题把人类面临的所有问题划分成两类:
(1)可计算问题
(2)不可计算问题
Turing论题中“有限步骤”是一个相当宽松的条件,即使需要计算几个世纪的问题,在理论上也都是可计算的。因此Turing论题 界定出的可计算问题几乎包括了人类遇到的所有问题。
不可计算问题的典型例子:
- 停机问题。给定一个计算机程序和一个特定的输入,判断该程序是否可以停机。 如果停机问题是可计算的,那么编译系统就能够在运行程序之前检查出程序中是否有死循环,事实上,当一个程序处于死循环时,系统无法确切地知道它只是一个很慢的程序,还是一个进入死循环的程序。
- 判断一个程序中是否包含计算机病毒。不存在一个病毒检测程序,能够检测出所有未来的新病毒。
实际的病毒检测程序做得很好,通常能够确定一个程序中是否包含特定的计算机病毒,至少能够检测现在已经知道的那些病毒,但是心怀恶意的人总能开发出病毒检测程序还不能够识别出来的新病毒。
------------------------------------------------------------------------------------------------------------------------
易解问题与难解问题
理论上可计算的问题不一定是实际可计算的。
Cook论题:一个问题是实际可计算的当且仅当它在图灵机上经过多项式步骤得到正确的结果
易解问题:可以在多项式时间内求解的问题,这类问题在可以接受的时间内实现问题求解。
难解问题:需要指数时间求解的问题,这类问题的计算时间随着问题规模(输入量的大小)的增长而快速增长,即使中等规模的输入,其计算时间也是以世纪来衡量的。
为什么把多项式时间复杂性作为易解问题和难解问题的分界线呢?
- 多项式函数与指数函数的增长率有本质的差别。
- 计算机性能的提高对多项式时间算法和指数时间算法的影响不同。
- 多项式时间复杂性忽略了系数,但不影响易解问题和难解问题的划分。
------------------------------------------------------------------------------------------------------------------------
判定问题
证比求易:求解一个问题往往比较困难,但验证一个问题相对来说比较容易。
从是否可以被验证的角度,计算复杂性理论将难解问题进一步划分:
(1)NP 问题
(2)非 NP 问题
判定问题(decision problem)是要求回答“yes”或“no”的问题
例如,停机问题就是一个判定问题,但是,停机问题不能用任何计算机算法求解,所以,并不是所有的判定问题都可以在计算机上得到求解。
在实际应用中,很多问题以求解或计算的形式出现,但是,大多数问题可以很容易转化为相应的判定问题,下面给出一些例子。
排序问题:将一个整数序列调整为非降序排列。
排序问题的判定形式:给定一个整数序列,是否可以按非降序排列。
哈密顿回路问题:在图 G=(V, E)中,从某个顶点出发,求经过所有顶点一次且仅一次再回到出发点的回路。
哈密顿回路问题的判定形式:在图 G=(V, E)中,是否存在一个回路经过所有顶点一次且仅一次然后回到出发点。
TSP问题:在一个带权图G=(V, E)中,求经过所有顶点一次且仅一次再回到出发点,且路径长度最短的回路。
TSP问题的判定形式:给定带权图G=(V, E)和一个正整数 k,是否存在一个回路经过所有顶点一次且仅一次再回到出发点,且路径长度小于等于 k。
------------------------------------------------------------------------------------------------------------------------
确定性算法与P类问题
设 A 是求解问题 Π 的一个算法,如果在算法的整个执行过程中,每一步只有一个确定的选择,并且对于同一输入实例运行算法,所得的结果严格一致,则称算法 A 是确定性算法(determinism)。
如果对于某个判定问题 Π,存在一个非负整数 k,对于输入规模为 n 的实例,能够以O(nk)的时间运行一个 确定性算法,得到 yes 或 no 的答案,则该判定问题 Π 是 P 类问题(polynomial)
P 类问题是具有多项式时间的确定性算法来求解的判定问题。
采用判定问题定义 P 类问题,主要是为了给出 NP 类问题的定义。
事实上,所有易解问题都属于 P 类问题。
------------------------------------------------------------------------------------------------------------------------
非确定性算法与NP类问题
设 A 是求解问题 Π 的一个算法,如果算法 A 采用如下猜测并验证的方式,则称算法 A 是非确定性算法(nondeterminism):
(1)猜测阶段:对问题的输入实例产生一个任意字符串 ω,对于算法的每一次运行,串 ω 的值可能不同,因此,猜测以一种非确定的形式工作。
(2)验证阶段:用一个确定性算法验证两件事:首先,检查在猜测阶段产生的串 ω 是否是合理的形式,如果不是,则算法停下来并得到 no;另一方面,如果串 ω 是合理的形式,再验证 ω 是否是问题的解,如果是问题的解,则算法停下来并得到 yes,否则,算法停下来并得到 no。
非确定性算法不是一个实际可行的算法。
如果对于某个判定问题 Π,存在一个非负整数 k,对于输入规模为 n 的实例,能够以O(nk)--多项式时间内的时间运行一个 非确定性算法,得到 yes 或 no 的答案,则该判定问题 Π 是 NP类问题(nondeterministic polynomial)
NP 类问题的特点是可以在多项式时间内验证一个解的正确性,但要找到这个解本身可能非常困难,不一定能在多项式时间内找到确定性的求解方法。
对于 NP 类 判定问题,关键是该问题存在一个确定性算法,并且能够以多项式时间检查和验证在猜测阶段所产生的答案
例如,考虑TSP问题的判定形式,假定算法 A 是求解 TSP 判定问题的非确定性算法, (1)算法 A 以非确定的形式猜测一个路径是 TSP 判定问题的解;
(2)用确定性算法检查这个路径是否经过所有顶点一次且仅一次并返回出发点,
① 如果答案为 yes,则继续验证这个回路的总长度是否 ≤ k,如果答案仍为 yes,则算法输出 yes;
② 否则算法输出 no。
NP类问题是难解问题的一个子集。并不是任何一个在常规计算机上需要指数时间的问题(即难解问题)都是 NP 类问题。
例如,汉诺塔问题不是 NP类问题,因为对于 n 层汉诺塔需要O(2^n)步输出正确的移动过程,一个非确定性算法不能在多项式时间猜测并验证一个答案。(汉诺塔问题也不属于P问题)
P 类问题存在多项式时间的确定性算法进行判定或求解,显然也可以构造多项式时间的非确定性算法进行判定。因此,P 类问题属于 NP 类问题,即P⊆NP
NP 类问题存在多项式时间的非确定性算法进行猜测并验证,但是,不一定能够构造一个多项式时间的确定性算法进行判定或求解。因此,人们猜测P≠NP。但是,这个不等式是成立还是不成立,至今没有得到证明。
------------------------------------------------------------------------------------------------------------------------
问题变换
假设问题 Π' 存在一个算法 A,对于问题 Π' 的输入实例 I',算法 A 求解问题 Π' 得到一个输出 O'。另外一个问题 Π 的输入实例是 I,对应于输入 I,问题 Π 有一个输出 O,则问题 Π 变换到问题 Π' 是一个三步的过程:
(1)输入转换:把问题 Π 的输入 I 转换为问题 Π' 的输入 I';
(2)问题求解:对问题 Π' 应用算法 A 产生一个输出 O';
(3)输出转换:把问题 Π' 的输出 O' 转换为问题 Π 对应于输入 I 的正确输出
若在O(τ(n))的时间内完成上述输入和输出转换,则称问题 Π 以 τ(n) 时间变换到问题 Π',记为 Π∝τ(n)Π',其中,n 为问题规模;
若在多项式时间内完成上述输入和输出转换,则称问题 Π 以多项式时间变换到问题 Π',记为Π∝pΠ'。
问题变换的主要目的不是给出解决一个问题的算法,而是给出比较两个问题计算复杂性的一种方式。
------------------------------------------------------------------------------------------------------------------------
NP完全问题的定义
令 Π 是一个判定问题,如果问题 Π 属于 NP 类问题,并且对 NP 类问题中的每一个问题 Π',都有 Π' ∝pΠ(问题 Π'在多项式时间变换成问题Π ),则称判定问题 Π 是一个NP完全问题(NP complete problem),也称 NPC 问题。
NP 完全问题是 NP 类问题中最有代表性的一类问题,NP 完全问题有一个重要性质:如果一个 NP 完全问题能够在多项式时间内得到求解,那么 NP 类问题中的每一个问题都可以在多项式时间内得到求解。
目前还没有一个 NP 完全问题发现有多项式时间算法。这些问题也许存在多项式时间算法,因为计算机科学是相对新生的科学,肯定还会有新的算法设计技术有待发现;这些问题也许不存在多项式时间算法,但目前缺乏足够的技术来证明这一点。
证明一个判定问题 Π 是 NP 完全问题需要经过两个步骤:
(1)证明问题 Π属于 NP 类问题,也就是说,可以在多项式时间以非确定性算法实现验证;
(2)证明一个已知的 NP 完全问题能够在多项式时间变换为问题 Π。
1971年,Cook在Cook定理中证明了SAT可满足问题是NP完全的。
1972年,Karp证明了十几个问题都是NP完全的。
------------------------------------------------------------------------------------------------------------------------
基本的NP完全问题
SAT问题(boolean satisfiability problem)。也称为合取范式的可满足问题,来源于许多实际的逻辑推理及应用。对于合取范式 A = A1∧A2∧…∧An,子句 Ai = a1∨a2∨…∨ak(1≤i≤n),ai 为某一布尔变量或布尔变量的非。SAT 问题是指:是否存在一组对所有布尔变量的赋值(true或false),使得合取范式 A 的值为真。
最大团问题(maximum clique problem)。图 G=(V, E)的团是图 G 的一个完全子图,该子图中任意两个互异的顶点都有一条边相连。团问题是对于给定的无向图 G=(V, E)和正整数 k,是否存在具有 k 个顶点的团。
补充:
完全图:如果无向图中的任何一对顶点之间都有一条边,这种无向图称为完全图。
完全子图:给定无向图G=(V,E)。如果U⊆V,且对任意u,v⊆U 有(u,v) ⊆ E,则称U 是G 的完全子图。
团(最大完全子图): U是G的团当且仅当U不包含在G 的更大的完全子图中
最大团:G 的最大团是指G中所含顶点数最多的团。
图着色问题(graph coloring problem)。给定无向连通图 G=(V, E) 和正整数k,是否可以用 k 种颜色对 G 中的顶点着色,使得任意两个相邻顶点的着色不同。
哈密顿回路问题(hamiltonian cycle problem)。在图 G=(V, E)中,是否存在经过所有顶点一次且仅一次并回到出发顶点的回路。
TSP问题(traveling salsman problem)。给定带权图 G=(V, E) 和正整数 k,是否存在一条哈密顿回路,其路径长度小于等于 k。
顶点覆盖问题(vertex cover problem)。设图 G=(V, E) ,V' 是顶点 V 的子集,若图 G 的任一条边至少有一个顶点属于 V',则称 V' 为图 G 的顶点覆盖。顶点覆盖问题是对于图 G=(V, E) 和正整数 k,是否存在顶点 V 的一个子集 V',使得图 G 的任一条边至少有一个顶点属于 V' 且 |V'| ≤ k。(集合覆盖问题)
子集和问题(sum of subset problem)。给定一个整数集合 S 和一个正整数 k,判定是否存在 S 的一个子集 S',使得 S' 中的整数之和等于 k。
背包问题是一种组合优化的NP完全问题。
注:涉及“所有组合”的问题通常是NP完全问题
------------------------------------------------------------------------------------------------------------------------
k带图灵机
k 带图灵机由一个有限状态控制器和 k 条无限长的工作带(k ≥ 1)组成,每个工作带都有一个由控制器操纵的可以独立移动的读写头。
k 带图灵机是一个 6 元组,即 M =(Q, I, T, q0, qF, δ),其中:
(1)Q:有限个状态的集合;
(2)I:输入符号的集合,即字母表;
(3)T:有限个带符号的集合,包括字母表和空白符 B,即 T=I +{B};
(4)q0:初始状态;
(5)qF:终止(或接受)状态;
(6)δ:转移函数,从 Q´Tk 的某一子集映射到 Q´(T´{L, R, S})k 的函数。
其中,Q´Tk 的某一子集包含一个状态和 k 个工作带的单元符号的 k+1 元组,表示 k带图灵机的瞬间图象,称为瞬象;
{L, R, S}为读写头的动作,L 表示左移一个单元,R 表示右移一个单元,S 表示停止不动。
令 M =(Q, I, T, q0, qf, δ)是一个 k 带图灵机,ω 是输入符号串,↑ 表示读写头的位置,指向右侧第一个符号,则 M 开始的瞬象 ρ 如下:
ρ = (q0, ↑ω, ↑B, …, ↑B)
对于某个瞬象,移动函数 δ 将给出一个新的瞬象,对应一个状态和k个序偶,每个序偶由一个单元符号及读写头的移动方向组成,形式上可表示为:
ρ=(q, ω11↑ω12, ω21↑ω22, …, ωk1↑ωk2 )
其中,q∈Q,表示图灵机在此格局下的状态,ωi1↑ωi2 是第 i 个(1 ≤ i ≤ k)工作带上的内容,如果 ωi1 为空,则第 i 个读写头指向第 i 个工作带上第一个非空的符号;如果 ωi2 为空,则第 i 个读写头指向符号串 ωi1 之后的第一个空白符;当 ωi1 和 ωi2 都为空,表明第 i 个工作带是空的。
设 ρ1, ρ2, …, ρn, …是一个瞬象序列,这个序列可以是有穷的,也可以是无穷的。如果每一个ρi+1都由ρi经过转移函数δ得到,则称这个序列是一个计算。对于任意一个给定的输入符号串 ω∈I*,从初始瞬象 ρ0 开始,图灵机 M 在ω 上的计算有三种可能:
(1)计算是一个有穷序列 ρ1, ρ2, …, ρn, 其中 ρn 是一个可接受的停机格局,则称计算停机在接受状态;
(2)计算是一个有穷序列 ρ1, ρ2, …, ρn, 其中 ρn 是一个停机格局,但不是可接受格局,则称计算停机在拒绝状态;
(3)计算是一个无穷序列 ρ1, ρ2, …, ρn, …,称计算永不停机。
------------------------------------------------------------------------------------------------------------------------
NP类问题的计算机处理
NP 类问题是计算机难以处理的,但在实际应用中却经常会遇到,因此,人们提出了解决 NP 类问题的各种方法。
- 采用先进的算法设计技术。当实际应用中问题规模不是很大时,采用动态规划法、回溯法、界限剪枝法等算法设计技术还是能够解决问题的。
(2)充分利用限制条件。许多问题,虽然理论上归结为一个 NP 类问题,但实际应用中可能包含某些限制条件,有些问题增加了限制条件后,可能会改变性质。例如,
① 0/1背包问题中,限定物品的重量和价值均为正整数;图着色问题中,限定图为可平面图;
② TSP问题中,限定边的代价满足三角不等式,等。
(3)近似算法。很多问题允许最终解有一定程度的误差。近似算法是求解NP类问题的一个可行的方法。
(4)概率算法。将随机性的操作加入到算法运行中,同时允许结果以较小的概率出现错误,并以此为代价,获得算法运行时间大幅度减少。
(5)并行计算。虽然从原理上讲增加处理机的个数不能根本解决NP类问题,但并行计算是解决计算密集型问题的必经之路。
(6)智能算法。遗传算法、蚁群算法等来源于自然界的优化思想,称为智能算法,是解决最优化问题的有力手段。
------------------------------------------------------------------------------------------------------------------------
第十三章 近似算法
许多 NP 类问题实质上是最优化问题,即要求在满足约束条件的前提下,使某个目标函数达到极值(极大或极小)的最优解。
现实世界中,很多问题的输入数据是用测量方法获得的,而测量的数据本身就存在着一定程度的误差,这类问题允许最优解有一定程度的误差,近似最优解常常能满足实际问题的需要。
近似算法的基本思想是用近似最优解代替最优解,以换取算法设计上的简化和时间复杂性的降低。换言之,近似算法找到的可能不是一个最优解,但一定会为待求解的问题提供一个解。
如何衡量近似最优解的质量呢?
假设近似算法求解最优化问题,且每个可行解对应的目标函数值均为正数。若一个最优化问题的最优值为c*,求解该问题的近似算法求得的近似最优值为 c,则将该近似算法的近似比(approximate ratio)η 定义为:
也可以用相对误差(relative error)λ 表示近似算法的近似程度,定义为:
------------------------------------------------------------------------------------------------------------------------
求π的近似值
用圆内接正多边形的边长和半径之间的关系,不断将边数翻倍并求出边长,重复这一过程,正多边形的周长就逐渐逼近圆的周长,只要圆内接正多边形的边数足够多,就可以求得所需精度的π值。
------------------------------------------------------------------------------------------------------------------------
顶点覆盖问题
无向图 G=(V, E)的顶点覆盖是求顶点集 V 的一个子集 V',使得若(u, v)是 G 的一条边,则 v∈V' 或 u∈V'(至少一个点)。顶点覆盖问题(vertex cover problem)是求图 G 的最小顶点覆盖,即含有顶点数最少的顶点覆盖。
想法:顶点覆盖问题是一个 NP 难问题,目前尚未找到一个多项式时间算法。可以采用如下策略找到一个近似最小顶点覆盖:
初始时边集 E'=E,顶点集 V'={ },每次从边集 E' 中任取一条边(u, v),把顶点 u 和 v 加入顶点集 V',再把与顶点 u 和 v 相邻接的所有边从边集 E' 中删除,重复上述过程,直到边集 E' 为空,最后得到的顶点集 V' 是无向图的一个顶点覆盖。由于每次把尽量多的相邻边从边集 E' 中删除,可以期望 V' 中的顶点数尽量少,但不能保证 V' 中的顶点数最少。
------------------------------------------------------------------------------------------------------------------------
TSP问题
TSP问题(traveling salesman problem)求图 G 的最短哈密顿回路
想法:设 cij 表示边(i, j)∈E 的代价,由于顶点之间的距离为欧氏距离,对于图 G 的任意 3 个顶点 i、j 和 k,满足三角不等式:cij+cjk≥cik 。可以证明,满足三角不等式的 TSP问题仍为 NP 难问题。
求解 TSP问题的近似算法:
(1)采用 Prim算法生成图的最小生成树 T;
(2)对 T 进行深度优先遍历,得到遍历序列 L;
(3)由序列 L 构成的回路就是哈密顿回路。
------------------------------------------------------------------------------------------------------------------------
装箱问题
设有 n 个物品和若干个容量为 C 的箱子,n 个物品的体积分别为{s1, s2, …, sn},且有 si≤C(1≤i≤n),装箱问题(packing problem)把所有物品装入箱子,求占用箱子数最少的装箱方案。
想法:最优装箱方案通过把 n 个物品划分为若干个子集,每个子集的体积和小于 C,然后取子集个数最少的划分方案。但是,这种划分可能的方案数有 2n种,在多项式时间内不能够保证找到最优装箱方案。采用贪心法设计装箱问题的近似算法,下面是三种贪心策略:
(1)首次适宜法。依次取每一个物品,将该物品装入第一个能容纳它的箱子中。
(2)最适宜法。依次取每一个物品,将该物品装到目前最满并且能够容纳它的箱子中,使得该箱子装入物品后的闲置空间最小。
(3)首次适宜降序法。将物品按体积从大到小排序,然后用首次适宜法装箱。
------------------------------------------------------------------------------------------------------------------------
多机调度问题
设有 n 个作业{1, 2, …, n},由 m 台机器{M1, M2, …, Mm}进行加工处理,作业 i 所需的处理时间为 ti(1≤i≤n),每个作业均可在任何一台机器上加工处理,但不可间断、拆分。多机调度问题(multi-machine scheduling problem)要求给出一种作业调度方案,使得 n 个作业在尽可能短的时间由 m 台机器加工处理完成。
想法:可以采用贪心法设计多机调度问题的近似算法,贪心策略是最长处理时间作业优先,即把处理时间最长的作业分配给最先空闲的机器,这样可以保证处理时间长的作业优先处理,从而在整体上获得尽可能短的处理时间。
(1)当 m ≥ n时,只要将机器 i 的[0, ti)时间区间分配给作业 i 即可;
(2)当 m<n时,首先将 n 个作业按其所需的处理时间从大到小排序,然后依此顺序将作业分配给最先空闲的处理机。
第 1 个循环完成前 m 个作业的分配,时间开销为O(m);第 2 个循环完成后 n-m 个作业的分配,时间开销为O((n-m)×m),通常情况下 m<<n(O(n)<O(nm)<O(n^2)),则算法的时间复杂度为O(nlog2n)
------------------------------------------------------------------------------------------------------------------------
?带权顶点覆盖问题
对于无向图 G=(V, E),每个顶点 v∈V 都有一个权值 w(v),对于顶点集 V 的一个子集 V',若(u, v)∈E,则 v∈V' 或 u∈V',称集合 V' 是图 G 一个的顶点覆盖。在图 G 的所有顶点覆盖中,所含顶点权值之和最小的顶点覆盖称为 G 的最小权值顶点覆盖。
想法:最小权值顶点覆盖的近似算法采用定价法,不断寻找紧致的顶点加入顶点覆盖集合
设 cij 表示边(i, j)上的权值,wi 表示顶点 i 的权值,Si 表示与顶点 i 相关联的所有边的集合,定价法要求与顶点 i 所关联的所有边的权值之和必须小于等于该顶点的权值,即对于每个顶点 i∈E,满足:
如果边(i, j)相关联的两个顶点 i 和 j 均满足上式,则称边(i, j)满足边上权值的公平性,并且将满足的顶点称为紧致顶点(compact vertex)。定价法对边上权值的设定与寻找覆盖顶点同步进行。
证明定价法是一个 2 倍近似算法?
------------------------------------------------------------------------------------------------------------------------
子集和问题
给定一个正整数集合 S={s1, s2,…, sn},子集和问题(sum of sub-set problem)要求在集合 S 中,找出不超过正整数 C 的最大和。
想法:考虑蛮力法求解子集和问题:
(1)将所有子集和的集合初始化为 L0={0};
(2)求子集和包含 s1的情况,即 L0 的每一个元素加上 s1(用 L0+s1 表示),则L1=L0∪(L0+s1)={0, s1};
(3)再求子集和包含 s2 的情况,即 L1 的每一个元素加上 s2,则 L2=L1∪(L1+s2)={0, s1, s2, s1+s2};
依此类推,一般情况下,为求子集和包含 si(1 ≤ i ≤ n)的情况,即 Li-1 的每一个元素加上 si,则 Li=Li-1∪(Li-1+si)。
因为子集和问题要求不超过正整数 C,所以,每次合并时都要在 Li 中删除所有大于 C 的元素。
在每次合并结束并且删除所有大于 C 的元素后,在不超过近似误差 ε 的前提下,以 δ=ε/n 作为修整参数,对于元素 z,删去满足条件 (1-δ)×y≤z≤y 的元素 y,尽可能减少下次参与迭代的元素个数,从而获得算法时间性能的提高。
例:对L2进行修整:L2={0, 102, 206},删去元素104
L3=L2∪(L2+201)={0, 102, 206}∪{201, 303, 407}
={0, 102, 201, 206, 303}
对L3进行修整: L3={0, 102, 201, 303},删去元素206
因为对于元素201,206*0.95<=201<=206,所以需要删除y=206