8算法设计与分析笔记(Authored by M.H Alsuwaiyel, Saudi)

回溯法

  • 回溯法可以被描述为有组织的穷尽搜索,它可以避免搜索所有的可能性。一般适用于求解那些有潜在的大量解,但有限个数的解已经检查过的问题。

3着色问题

  • 给出一个无向图G=(V,E),需要用三种颜色之一为每个顶点着色,三种颜色设为1,2,3,要求两个相邻的顶点不能有相同的颜色(图的相邻是指有一条边)。

  • 一种着色可以用n元组(c_1,c_2,...,c_n),where~c_i\in \{1,2,3\},1\leq i\leq n表示。一个n个顶点的图共有3^n种着色方案(包括合法的和非法的),所有可能的着色可以用一棵完全三叉树(状态空间树)表示。从根节点到每个叶子节点的路径指示着一种着色方案。

  • 几个概念:

    • n问题的解向量:问题的解能够表示成一个n维向量(x_1,x_2,...,x_n)的形式;
    • n显式约束:对分量x_i的取值限定;
    • n隐式约束:为满足问题的解而对不同分量之间施加的限定;
    • n问题的解空间:对于问题的一个实例,解向量满足显式约束条件的所有n维向量,构成了该问题实例的一个解空间。
  • 为解决三着色问题,回溯法按深度优先搜索构造解空间树:

    • 算法搜索至解空间树的任一节点时,先判断该节点是否可能包含问题的解(包含即只着色了<n个顶点)
      • 如果肯定不包含,则跳过该节点;

      • 如果可能包含则进入该子树继续DFS

      • 如果某节点的所有子节点都不可能包含问题的解,则回溯其父节点,父节点在生成新的子节点搜索;

      • 一个实例:

3-COLORREC

输入:无向图G=(V, E)。

输出:G的顶点的3着色c[1···n],其中每个c[j]为1,2或3。

for k <- 1 to n
    c[k] <- 0
end for
flag <- false
graphcolor(1) #最多输出一种solution
if flag then output c
else output "no solution"
过程:graphcolor(k)
for color <- 1 to 3
    c[k] <- color
    if c 是合法着色 then set flag <- true exit #合法着色是说每个顶点都上色且合法
    else if c是部分的then graphcolor(k+1) #如果是部分合法则继续向后填色
    end if
end for
  • 递归算法的回溯思想通过递归过程体现,每次exit,即退出当前函数,回溯上一个;下面使用迭代算法实现,可以显式给出回溯过程。

3-COLORITER

输入:无向图G=(V, E)。

输出:G的顶点的3着色c[1···n],其中每个c[j]为1,2或3。

for k <- 1 to n
    c[k] <- 0
end for
flag <- false
k <- 1
while k >= 1
    while c[k] <= 2
        c[k] = c[k] + 1
        if c为合法着色then set flag <- true并退出两个while
        else if c是部分解then k <- k + 1
    end while
    c[k] <- 0 #回溯
    k <- k - 1
end while
if flag then output c
else output "no solution"
  • 易知在最坏的情况下会生成O(3^n)个节点(一棵完全二叉树),对于检测每个节点是否合法需要n的时间,时间复杂度为O(n3^n)

8皇后问题

  • 8\times 8的国际象棋上摆放八个皇后,使其不能相互攻击,即:任意两个皇后不能处于同一行、同一列或者同一条斜线上,应该如何摆放?

    zU8AnP.png

  • 对于8皇后问题,其实跟3着色问题思路十分相似,或者说他们都是回溯法的实际例子。从当前状态出发,顺序进行可能的尝试,然后判断合法性,设置下一个状态或者回溯。

  • 状态空间树几个概念:

    • 活节点:正常节点;
    • 扩展节点:当前节点,正在对此节点进行搜索;
    • 死节点:不可行节点,无需对其孩子进行搜索;
  • 下面以较简单的四皇后问题为例说明算法,c[1···4]数组是一行中四个格子的序号,n皇后问题的思路完全一致。

4-QUEENS

输入:空。

输出:对应与4皇后问题的解向量c[1···4]。

for k <- 1 to 4
    c[k] <- 0
end for
flag <- false
k <- 1
while k >= 1
    while c[k] <= 3
        c[k] <- c[k] + 1
        if c为合法位置then set flag <- true并从两个while中退出
        else if c是部分解then k <- k + 1
        end if
    end while
    c[k] <- 0
    k <- k - 1
end while
if flag then output c
else output "no solution"

N-QUEENS

输入:空。

输出:对应与n皇后问题所有的解向量。

X(1) <- 0; k <- 1 #k指示当前行号,X(k)指示第k行的列号
while k > 0
    X(k) <- X(k) + 1
    while X(k) <= n and not PLACE(k) do #直到找到一个可以放的位置,或者不能放置
        X(k) <- X(k) + 1
    if X(k) <= n then
        if k = n then print(x)
        else k <- k + 1; X(k) <- 0 #寻找下一个位置
        end if
    else X(k) <- 0; k <- k - 1 #回溯
    end if
end while
PLACE(k)
i <- 1
while i < k 
    if X(i) = X(k) or |X(i) - X(k)| = |i - k|
        then return false
    i <- i + 1
end while
return true

一般的回溯方法

  • 一般回溯算法,可以作为一种系统的搜索方法应用于一类搜索问题中,这类问题的解由事先定义好的某个约束解向量(x_1,x_2,...,x_i)组成,i0~to~n的某个整数。这里的解向量通常不是实际问题的直接表现,需要进行规则映射。

    • 例如给定X=\{10,20,30,40,50,60\},y=60,找到X的子集Y使得Y的全部元素和等于y

      易得,Y=\{10,20,30\},\{20,40\},\{60\}。(这是非定长的向量形式),转化为Y=\{y_1,y_2,y_3,y_4,y_5,y_6\},布尔值1、0分别指示该元素是否被选中。

回溯法框架BACKTRACK

  1. n算法最初从空向量开始,先选择X_1中的最小元素作为x_1

  2. n如果(x_1)是部分解,那么选择X_2中的最小元素作为x_2

  3. n如果(x_1, x_2)是部分解,则继续考虑X_3中的最小元素作为x_3

  4. n否则,考虑X_2中的第二个元素作为x_2。依此类推。

一般说来, 当算法已经检测到部分解(x_1, x_2, …, x_j), 需要继续考虑 v = (x_1, x_2, …, x_j, x_{j+1})时,有以下几种情形:

  1. v表示问题的最终解,算法记录下它作为一个解。如果只需要一个解,算法结束;否则,继续找其它解。

  2. 如果v = (x_1, x_2, …, x_j, x_{j+1}) 是一个部分解,那么选择集合X_{j+2}中未使用过的最小元素作为x_{j+2}。(向前搜索)

  3. 如果v 既非最终解,也非部分解,那么会有以下两种情况:

    a. 如果集合X_{j+1}中还有其它未曾使用过的元素,则选择下一个未曾使用过的元素作为x_{j+1}

    b. 如果集合X_{j+1}中没有其它未曾使用过的元素,则回溯,将X_j中未曾使用的下一元素作为x_j,并x_{j+1}进行重置。如果集合X_j中没有未曾使用的元素,那么继续回溯(同样注意要进行重置)。

BACKTRACKREC

输入:集合X1, X2,···, Xn的清楚的或隐含的描述。

输出:解向量v = (x1, x2,..., xi)。

v <- ()
flag <- false
advance(1)
if flag then output v
else output "no solution"
过程:advance(k)
for every x in Xk
    xk <- x;将xk加入v
    if v是最终解 then set flag <- true exit
    else if v是部分解then advance(k+1) #如果是部分合法则继续向后
    end if
end for

BACKTRACKITER

输入:集合X1, X2,···, Xn的清楚的或隐含的描述。

输出:解向量v = (x1, x2,..., xi)。

v <- ()
flag <- false
k <- 1
while k >= 1
    while Xk没有被穷举尽
        xk <- Xk中的下一个元素;将x加入v
        if v是最终解then set flag <- true并退出两个while
        else if v是部分解then k <- k + 1
    end while
    重置Xk,使得下一个元素排在第一位 
    k <- k - 1 #回溯
end while
if flag then output v
else output "no solution"
  • 以上回溯算法最多求出一个解,若要得到全部解向量则需要稍加修改。

回溯法解决0/1背包问题

  • 物品集合为元组(X_1,X_2,...,X_n),选中第i个物品则X_i=1,否则为0。0/1背包是找到一个最优解,所以需要遍历整个状态树后找最优的。

BACKTRACKING

输入:物品集合U={u1, u2,..., un},体积分别为s1, s2,..., sn,价值分别为v1, v2,..., vn,容量为C的背包。

输出:\sum_{u_i\in S}v_i在约束条件\sum_{u_i\in S}s_i \leq C下最大,S\subseteq U

k <- 1; Pw <- 0; Pv <- 0; X <- 2 #X数组指示背包是否被选中,0/1,初始时赋值为2
                                #Pw,Pv分别指示最优方案的weight和value
while k > 0 
    X[k] <- X[k] - 1 # 每次减1,遍历0/1两种状态
    if X[k] == 1 then #左子树
        if Pw + W[k] <= C then
            Pw <- Pw + W[k]; Pv <- Pv + V[k]
        else
            X[k] <- X[k] - 1
        end if
    else if X[k] == 0 #右子树
        Pw <- Pw - W[k]; Pv <- Pv - V[k]
    end if
    if X[k] >= 0 then
        if k == n then
            if Pv > Ov then
                Ow <- Pw; Ov <- Pv; Y <- X
            end if
            k <- k - 1 #得出一个解后回溯直到找到最优解
        else
            k = k + 1
        end if
    else
        X[k] <- 2
        k <- k - 1
    end if
end while
  • 0/1背包回溯法的改进:在搜索左子树时有判断Pw+W[k]\leq C但是右子树没有,所以右子树的遍历每次都会执行。但是如果右子树的最大可能结果的填充都比现有的解决方案小,就可以跳过搜索右子树,利用上界函数判断当前状态下能够得到最优方案的上界。
  • 上界函数:按照性价比排列物品,当前遍历第k个物品,还剩m容量时,依次将k+1,k+2放入背包直到装满(注意此时的性价比是k+1···n个背包排序,而不是1···n的排序,因为此时1···k的状态已经确定不能改变),装不下的采用比例装入(联想小数背包问题),得到的结果就是上界值。
BOUND(p, w, k, M)
#p:当前价值,w:当前重量,k:当前根节点,M:背包容量,物品已经按性价比排序
b <- p; c <- w
for i <- k + 1 to n 
    c <- c + W(i)
    if c < M then b <- b + P(i)
    else return (b+(1-(c-M)/W(i))*P(i))
return b

分支界限法

  • 分支界限法类似于回溯法,但是是采用广度优先搜索或者最小耗费算法。
  • 在分支界限法中,每个活节点只有一次机会成为扩展节点。活节点一旦成为扩展节点,就一次性产生所有子节点,得到不可行的节点则直接被舍弃。
    • 队列法,依照队列先进先出的原则选取下一个节点作为扩展节点;
    • 堆结构法,按照堆的规定选取优先级最高的节点(堆的根)作为扩展节点。

最短路径问题

  • 例,计算节点a到节点t的最短路径

    • 确定下界:经过某节点xt的最短路径的下界为:ax的最短路径加上x的一条最短出边。例如a\rightarrow b \rightarrow t的下界为1+min\{ 2,9\}=3a\rightarrow c \rightarrow t的下界为4+min\{3,4,6\}=7;(最终的解就是下界)

    • 步骤总结:

      k=1

      • 根节点0对应于源点a,有3个邻接顶点b、c、d,其下界为3,7,11,压入堆。

      k=2

      • 堆中下界3最小,对于的顶点b,也即结点1。从顶点b继续进行搜索。
      • 顶点b的邻接顶点为ce,其下界为611,压入堆。

      k=3

      • 堆中下界6最小,对应顶点c,也即结点4,从顶点c继续进行搜索。

      • 顶点c邻接顶点d、e、f、g,对应的下界为13,10,8,8,压入堆。

      k=2

      • 堆中7最小,对应顶点c,也即结点2。从顶点c进行搜索。

      • 顶点c邻接顶点d、e、f、g,对应的下界为14,11,9,9,压入堆。

      k=4

      • 堆中8最小,对应顶点f,也即结点8

      • 顶点f的邻接顶点为e、t,下界分别为9、11,压入栈中。其中11为一个可行解,将bound置为11

      k=4

      • 堆中8最小,对应顶点g,也即结点9

      • 顶点g的邻接顶点为f、t,下界都是10。其中10为一个可行解,将bound置为10

      k=5

      • 堆中9最小,对应顶点e,也即结点14
  • 顶点e只有一个邻接顶点t,下界为9,从而得到一个可行解,路径长度为9,加入堆中。堆中最小的为9,且最后一个结点为t,因此是最优解。

0/1背包问题

  • 考虑n=4,承重为C=10的背包问题(价值/重量即物品的性价比)。

  • 思路:

    • 为每个搜索节点设置一个上界ub(upper~bound),一个简单的方法是:把已经选择的物品总价值v,加上背包剩余承重C-w与剩下可选择物品的最高“价值/重量比”的乘积,即ub=v+(C-w)\times (v_i/w_i)_{max}
    • 比如,已经装了物品1,则价值的上界为40+(10-4)\times 6=76
    • 如下图,每次选择的物品是使ub最大的结点

旅行商问题TSP

  • 问题描述为:一个商人要去n个城市推销货物,从某个城市出发,要求找到一条闭合路径,n个城市都经历一次,最后回到出发点,使得旅行的花费最小(依据情况的不同,花费可以是距离、时间、金钱等)。与背包问题相反,我们需要估计每个搜索节点的下界。

  • 例如,给定图G

  • 一个有效的下界:

    • 对于每个顶点is_i表示与顶点i相连顶点的两个最短距离(s_i^1,s_i^2)之和,即s_i=s_i^1+s_i^2,下界lb=s=\lceil{1\over 2}\sum_{i=1}^n s_i\rceil

    • 如果必须包含某条边(i,x),则s_i=|(i,x)|+i剩余的最短边

    • ,对于该图lb=\lceil [(1+3)+(3+6)+(1+2)+(3+4)+(2+3)]/2 \rceil=14

    • ,例如必须包含边(a,d)lb=\lceil [(5+1)+(3+6)+(1+2)+(3+5)+(2+3) ]/2\rceil=16

    • 为什么可以这样设置呢?

      • 给定n个顶点的无向图,边的权值已经确定,将符合题意的TSP回路记作T

      • 显然,在T中每个顶点只出现一次,记作1,2,...,n。每条边记作s_1^*,s_2^*,...,s_n^*

      • 我们可以得到:

        对于顶点1s_1^*+s_n^* \geq s_1^1+s_1^2=s_1

        对于顶点2s_1^*+s_2^* \geq s_2^1+s_2^2=s_2

        ······

        对于顶点ns_{n-1}^*+s_n^* \geq s_n^1+s_n^2=s_n

        累加求和解得:s_1^*+s_2^*+..+s_n^*\geq s/2,由于s_1^*+s_2^*+..+s_n^*是整数所以可以取lb=\lceil s/2 \rceil

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容