【Algorithm-BackTracking】回溯法
批处理作业调度
问题描述
给定n个作业的集合J=(J1,J2,… ,Jn)。每一个作业Ji都有两项任务分别在2台机器上完成。每个作业必须先有机器1处理,然后再由机器2处理。作业Ji需要机器j的处理时间为tji。对于一个确定的作业调度,设Fji是作业i在机器j上完成处理时间。则所有作业在机器2上完成处理时间和f=F2i,称为该作业调度的完成时间和。
简单描述
对于给定的n个作业,指定最佳作业调度方案,使其完成时间和达到最小。
举例说明
tji | 机器1 | 机器2 |
---|---|---|
作业1 | 2 | 1 |
作业2 | 3 | 1 |
作业3 | 2 | 3 |
这3个作业的调度方案共有6种(即3个作业的全排列),分别是123,132,213,231,312,321,它们所相应的完成时间和分别是19,18,20,21,19,19。显而易见,最佳调度方案是132,其完成时间和为18。
算法设计
从n个作业中找出有最小完成时间和的作业调度,所以批处理作业调度问题的解空间是一棵排列树。按照回溯法搜索排列树的算法框架,设开始时x=[1,2, … , n]是所给的n个作业,则相应的排列树由x[1:n]的所有排列构成。
类Flowshop的数据成员记录解空间的结点信息,以便减少传给Backtrack的参数。二维数组M是输入作业的处理时间,bestf记录当前最小完成时间和,bestx记录相应的当前最佳作业调度。
在递归函数Backtrack中,
当i>n时,算法搜索至叶子结点,得到一个新的作业调度方案。此时算法适时更新当前最优值和相应的当前最佳调度。
当i<n时,当前扩展结点位于排列树的第(i-1)层,此时算法选择下一个要安排的作业,以深度优先方式递归的对相应的子树进行搜索,对不满足上界约束的结点,则剪去相应的子树。
算法描述:
注:1、区分作业i和当前第i个正在执行的作业
给x赋初值,即其中一种排列,如x=[1,3,2];M[x[j]][i]代表当前作业调度x排列中的第j个作业在第i台机器上的处理时间;如M[x[2]][1]就意味着作业3在机器1上的处理时间。
2、bestf的初值
此问题是得到最佳作业调度方案以便使其完成时间和达到最小,所以当前最优值bestf应该赋值为较大的一个值。如9999
3、f1、f2的定义与计算
假定当前作业调度排列为:x=[1,2,3];f1[i]即第i个作业在机器1上的处理时间,f2[j]即第j个作业在机器2上的处理时间;则:
f1[1]=M[1][1] , f2[1]=f1[1]+M[1][2]
f1[2]=f1[1]+M[2][1] , f2[2]=MAX(f2[1],f1[2])+M[2][2] //f2[2]不光要等作业2自己在机器1上的处理时间,还要等作业1在机器2上的处理时间,选其大者。
f1[3]=f1[2]+M[3][1] , f2[3]=MAX(f2[2],f1[3])+M[3][2]
- f1只有当前值有用,可以覆盖赋值,所以定义为int型变量即可,减少空间消耗;
- f2需要记录每个作业的处理时间,所以定义为int *型,以便计算得完成时间和。
4、f2[0]的初值
f2[i]的计算都是基于上一个作业f2[i-1]进行的,所以要记得给f2平[0]赋值为0。
代码
1 |
|
工作分配问题
与批处理作业调度类似
题目
设有n件工作分配给n个人。将工作i分配给第j个人所需的费用为c(i,j) 。 设计一个算法,对于给定的工作费用,为每一个人都分配1件不同的工作,并使总费用达到最小。
输入格式:
输入数据的第一行有1 个正整数n (1≤n≤20)。接下来的n行,每行n个数,表示给每个人的工作费用。
输出格式:
将计算出的最小总费用输出到屏幕。
输入
3
10 2 3
2 3 4
3 4 5
输出
最小费用
9
最优序列为【第1个任务给第二个人,第2个任务给第一个人;列表示任务,对应值表示人】
2 1 3
1 |
|
三角符号问题
问题
给定第一行的符号(只有+,-)数目n,每行比上一行数目少一(形成一个倒三角),2个相同符号下面为“+”号,2个不同符号下面为“-”号,要求有多少种情况使得两种符号数目相同。
第一行为7的符号三角形之一:
分析
我们发现
由于只有两种符号,所以我们可简化为0、1形式,+号用1表示,-号用0表示
①总符号数为n(n+1)/2,如果总数为奇数,那么一定不可能符号数相等
②当某一符号数大于总数的一半时,那么此情况不存在解
代码
1 |
|
举例
比如:t=3时,让p[0][3]=0;
假如第一行为0 0 1 0
则j从1(第二行开始),并从t-j=2,第三列开始开始
第一行 | 0 | 0 | 1 | 0 p[0][3] |
---|---|---|---|---|
第二行 | 1 | |||
第二行 | 1 | 1 | ||
第二行 | 0 | 1 | 1 |
j再变成2,并从t-j=1,第二列开始……剩下的类似
最终产生
第1行 | 0 | 0 p[0][1] | 1 p[0][2] | 0 p[0][3] |
---|---|---|---|---|
第2行 | 0 | 1 | 1 | |
第3行 | 1 | 0 | ||
第4行 | 1 |
此时,已经完成了t=3的(实际上是4个,t从0开始)规模的三角型,再进入backtrack(t+1);
进入backtrack(t+1)后,如果n=4,现在t=4,已经完成了目标,sum+1并return
返回上个递归,并进行清除之前的赋值,并重新让p[0][3]=1;
第1行 | 0 | 0 p[0][1] | 1 p[0][2] | 1 p[0][3] |
---|---|---|---|---|
第2行 | 0 | 1 | 0 | |
第3行 | 1 | 1 | ||
第4行 | 0 |
此时,已经完成了t=3的规模的三角型,再进入backtrack(t+1);
同上面一样,如果n=4,t=4,也完成了目标,sum+1并return
返回上个递归,并进行清除之前的赋值,发现p[0][3]已经选择过0和1,for(i=0;i<2;i++)
已经执行完成,则整个backtrack(3)执行完成
。
则返回到backtrack(2)调用backtrack(3)的后面,清除之前的赋值,发现p[0][2]也已经选择过0和1,for(i=0;i<2;i++)
已经执行完成,则整个backtrack(2)执行完成
。
则返回到backtrack(1)调用backtrack(2)的后面,清除之前的赋值,并让p[0][1]=1 ;
再调用backtrack(2)并继续运算
n皇后问题
题目
N皇后的排列,每行一个不冲突;N<=13。
输入要求
一个数字N (6 <= N <= 13) 表示棋盘是N x N大小的。
输出要求
前三行为前三个解,每个解的两个数字之间用一个空格隔开。第四行只有一个数字,表示解的总数。
解的输出顺序为从上到下从左到右,小的优先输出
输入:
6
输出样例:
2 4 6 1 3 5
3 6 2 5 1 4
4 1 5 2 6 3
4
思路
整体思路:从第一列开始逐行遍历,如果i行j列可以放,则queue[i][j]位置设置为1,并开始遍历下一(i+1)行。如果不可以放,则设置queue[i][j]为0,并对(j+1)列开始逐行遍历。直到遍历到第八行时,将结果输出。输出的思路是,如果queue[i][j]为1则说明此位置有皇后,输出对应的j列号。
判断queue[i][j]是否可以放皇后的思路:
如果0~i-1行j列有皇后则不能放;
如果queue[i][j]的左上角有皇后则不可以放;
如果queue[i][j]的右上角有皇后则不可以放;
其它情况则可以放。
isSafe
We must check upper left diagonal , upper right diagonal and the same col .Because we place queens row by row, we can’t check lower left diagonal because all rows below “the current placing row” are empty and don’t have any queens yet.
On the other hand, checking the upper left and upper right diagonals is important because there may be queens in those diagonals that can attack the new position. That’s why you should check the same column, upper left diagonal and upper right diagonal when determining if it’s safe to place a new queen on the chessboard.
solveNQUtil
(solve N Queens Utility)
If not all queens have been placed yet, then the function enters a loop that iterates over all columns in the current(the given row) row.
For each column, it checks if it’s safe to place a new queen there using isSafe function. If it’s safe, then it places a queen in that position (board[row][i] = 1)and calls itself recursively for the next row (solveNQUtil(board,row+1)).After returning from the recursive call,it removes the queen from that position (board[row][i] = 0) and continues with the next column.
代码
1 |
|
最大团问题
和装载问题相似
了解最大团问题(Maximum Clique Problem, MCP)之前需要明白几个概念。复习一下图论知识…
完全图:如果无向图中的任何一对顶点之间都有一条边,这种无向图称为完全图。
完全子图:给定无向图G=(V,E)。如果U⊆V,且对任意u,v⊆U 有(u,v) ⊆ E,则称U 是G 的完全子图。
团(最大完全子图): U是G的团当且仅当U不包含在G 的更大的完全子图中
**最大团:**G 的最大团是指G中所含顶点数最多的团。
**空子图:**给定无向图G=(V,E)。如果U⊆V,且对任意u,v⊆U 有(u,v) ∉ E,则称U 是G 的空子图。G的空子图U是G的独立集当且仅当U不包含在G的更大空子图中。
**独立集:**对于给定无向图G=(V,E)。如果顶点集合V⊆V,若V中任何两个顶点均不相邻,则称V*为G的点独立集,或简称独立集。
**最大独立集:**G中所含顶点数最多的独立集。
例如:
图a是一个无向图,图b、c、d都是图a的团,且都是最大团。
补图:
图G的补图,通俗的来讲就是完全图Kn去除G的边集后得到的图Kn-G。在图论里面,一个图G的补图(complement)或者反面(inverse)是一个图有着跟G相同的点,而且这些点之间有边相连当且仅当在G里面他们没有边相连。
输入
5 7
1 2
1 4
1 5
2 5
2 3
3 5
4 5
输出
3
1 2 5
思路
首先假设最大团为一个空团
,往其中加入一个顶点,然后依次考虑未加入到团的每个顶点
,查看该顶点加入团之后是否仍然构成一个团
?
- 如果可以再构成团,考虑将该顶点
加入团
或者舍弃
两种情况。 - 如果不能再构成团,
直接舍弃
,然后递归判断下一顶点。
对于无连接或者直接舍弃两种情况,在递归前,可采用剪枝策略来避免无效搜索。
判断某个顶点加入团之后是否仍是一个团
- 该顶点和团中所有的顶点是否
都有连接
剪枝
程序中采用了一个比较简单的剪枝策略,即如果剩余未考虑的顶点
数加上团中顶点数
不大于当前最优解的顶点数
,可停止继续深度搜索,
- 因为
最优解需要尽可能的大
,未考虑的顶点+目前团中顶点
的总数不可能再超过
当前最优解的顶点数,无需再进入子树
否则继续深度递归。
代码
1 |
|
图的m着色问题
着色问题与最大团、n皇后问题的
判断-假设部分
相似
题目
给定图G=(V, E),需要为图G的各顶点着色,是否有一种着色法使G中相邻的两个顶点有不同的颜色?
输入要求
第一行是顶点的个数n(2≤n≤8),颜色数m(1≤m≤n)。
接下来是顶点之间的连接关系:a b;表示a和b相邻。顶点从1开始计。
当a,b同时为0时表示输入结束。
输出要求
输出着色方案总数和最少颜色数。如果无可行方案,颜色数为0。
输入:
5 8 4
1 2
1 3
1 4
2 3
2 4
2 5
3 4
4 5
输出样例:
48 4
思路
colorOf[n] 存储 n个顶点的着色方案,可以选择的颜色为1 到 m
x=1 对当前第x个顶点开始着色:
若x>n 则已求得一个解,输出着色方案即可
否则,依次对顶点x着色1到m,假设给点x着色为i
- 若点x与其它相邻点无颜色冲突,则继续为
下一顶点
着色; - 若点x与其它相邻点有相同颜色,则返回假,
尝试下一颜色
。
代码
1 |
|
装载问题
问题描述
有n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且∑wi <= c1 + c2。
问是否有一个合理的装载方案,可将这n个集装箱装上这2艘轮船。如果有,找出一种装载方案。
问题分析
如果一个给定装载问题有解,则采用下面的策略可得到最优装载方案。
(1)首先将第一艘轮船尽可能装满
;
(2)将剩余的集装箱装上第二艘轮船。
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近c1。由此可知,装载问题等价于以下特殊的0-1背包问题:
max∑ wi * xi && ∑ wi * xi <= c1, xi ∈ {0, 1}, 1 <= i <= n;
算法思路
用子集树表示解空间,则解为n元向量{x1, … ,xn }, xi∈{0, 1} 。
约束条件
当前搜索的层i <= n时,
当前扩展结点Z为子集树的内部结点,仅当满足cw+w[i] <= c时进入左子树,x[i]=1;
当cw+w[i] > c ,即装不下
,在以结点Z为根的子树中所有结点都不满足约束条件,因而该子树中解都是不可行解,因而将在该子树删去。
剪枝
由于是最优化问题, 可利用最优解性质进一步剪去不含最优解的子树:
设Z是解空间树第i层上的当前扩展结点。
设 bestw: 当前最优载重量,
cw : 当前扩展结点Z的载重量 ;
r : 剩余集装箱的重量;
在以Z为根的子树中任意叶结点所相应的载重量不超过cw + r。因此,当cw + r (限界函数) ≤ bestw时,可将Z的右子树剪去
- 因为目标
bestw要尽可能的大
,如果发现cw+r,也就是这个子树的最大重量
,已经不可能再超过bestw
,就无需进入子树
即:只有当 cw + r > bestw 时,才搜索右子树,x[i]=0;
代码
1 |
|
测试样例
输入:
3
50 50
10 40 40
输出
船1装入的货物为: 1 2
船2装入的货物为: 3