实验内容:
本实验要求基于算法设计与分析的一般过程(即待求解问题的描述、算法设计、算法描 述、算法正确性证明、算法分析、算法实现与测试),通过分支限界法(包括队列式和优先 队列式)的在实际问题求解实践中,加深理解其基本原理和思想以及求解步骤。求解的问题为布线问题。作为挑战:可以考虑分支限界法在其他问题(如旅行商问题、0-1 背包问 题)。
实验目的:
◆ 理解回溯法的核心思想以及求解过程(确定解的形式及解空间组织,分析出搜索过程中的剪枝函数即约束函数与限界函数);
◆ 掌握对几种解空间树(子集树、排列数、满 m 叉树)或图的分支限界的搜索方法;
◆ 从算法分析与设计的角度,对布线等问题的基于分支限界法求解过程及思路有更进一步的理解。
实验步骤:
步骤 1:理解问题,给出问题的描述。
印刷电路板将布线区域划分成 nxm 个方格阵列,精确的电路布线问题要求确定连接方格 a 的中点到方格b的中点的最短布线方案。在布线时,电路只能沿直线或直角布线,为了避免线路相交,已布了线的方格做了封锁标记, 其他线路不允许穿过被封锁的方格。
步骤 2:算法设计,包括算法策略与数据结构的选择;
用队列式分支限界法来考虑布线问题。布线问题的解空间是一个图,则 从起始位置 a 开始将它作为第一个扩展结点。与该扩展结点相邻并可达的方格成为可行结点被加入到活结点队列中,并且将这些方格标记为 1,即从起始方格 a 到这些方格的距离为 1。接着,从活结点队列中取出队首结点作为 下一个扩展结点,并将与当前扩展结点相邻且未标记过的方格标记为 2,并存入活结点队列。这个过程一直继续到算法搜索到目标方格 b 或活结点队列为空时为止。即加入剪枝的广度优先搜索.
步骤 3:描述算法。采用源代码以外的形式(如伪代码、流程图等)来描 述;
步骤 4:算法的正确性证明。需要这个环节,在理解的基础上对算法的正确性给予证明 (这里可以略去。有兴趣可以深入一下“多米诺性质”);
若源到同一个顶点有多条路径,将长路径的分支全部舍弃,而保存更短路径的分 支。并且由于题目的贪心选择性质,每次从优先队列中取最短路径,最终得到的解也必 然是最优的。
步骤 5:算法复杂性分析,包括时间复杂性和空间复杂性(关于平均情况下的时间复杂 性分析方法,有兴趣可以深入一下“蒙特卡洛方法”);
(1)时间复杂度 分支限界法求布线问题,按照 m 叉树(m=4)的分析,空间树最坏情况下的结点为 4n 个,而空间树的深度 n 却是未知的,因此通过这种方法很难确定该算法的时间复杂度。 那怎么办呢?我们要看看到底生成了多少个结点。 实际上,每个方格进入活结点队列最多 1 次,不会重复进入,因此对于 m×n 的方 格阵列,活结点队列最多处理 O(mn)个活结点,生成每个活结点需要 O(1)的时间,因此 算法时间复杂度为 O(mn)。构造最短布线路径需要 O(L)时间,其中 L 为最短布线路径长 度。
(2)空间复杂度 空间复杂度为 O(n)。
步骤 6:算法实现与测试。附上代码算法运行结果截图;
#include <iostream> #include <queue> #include <bits/stdc++.h> using namespace std; class Position { public: int row; // 行 int col; // 列 }; const int m = 7; // 列数 const int n = 7; int gird[n + 2][m + 2]; bool FindPath(Position start, Position finish, int &PathLen, Position *&path) { if ((start.col == finish.col) && (start.row == finish.row)) // 起点和终点是同一个点时 { PathLen = 0; return true; } // 初始化相对位移 Position offset[4]; // 右 offset[0].col = 0; offset[0].row = 1; // 下 offset[1].col = 1; offset[1].row = 0; // 左 offset[2].col = 0; offset[2].row = -1; // 上 offset[3].col = -1; offset[3].row = 0; int NumofNbrs = 4; Position here, nbr; // nbr 为扩展节点 here.col = start.col; // here 为每次移动的中心位置点 here.row = start.row; // 标记可达方格的位置 gird[start.col][start.row] = 1; queue<Position> Q; do { for (int i = 0; i < NumofNbrs; i++) { nbr.col = here.col + offset[i].col; nbr.row = here.row + offset[i].row; if (gird[nbr.col][nbr.row] == 0) // 该位置还未被标记 { gird[nbr.col][nbr.row] = gird[here.col][here.row] + 1; if ((nbr.col == finish.col) && (nbr.row == finish.row)) { break; // 退出 for 循环 } Q.push(nbr); } } // 判断是否到达了目标位置 finish if ((nbr.col == finish.col) && (nbr.row == finish.row)) { break; // 退出 while 循环 } if (Q.empty()) { return false; // 此时无解 } here = Q.front(); Q.pop(); } while (true); PathLen = gird[finish.col][finish.row] - 1; path = new Position[PathLen]; here = finish; for (int j = PathLen - 1; j >= 0; j--) { path[j] = here; // 找到前驱节点 for (int i = 0; i < NumofNbrs; i++) { nbr.col = here.col + offset[i].col; nbr.row = here.row + offset[i].row; if (gird[nbr.col][nbr.row] == j + 1) { break; } } here = nbr; } return true; } int main() { int PathLen; Position start, finish, *path; int x1, y1, x2, y2; cout << "请输入布线起点:"; cin >> x1 >> y1; cout << "请输入布线终点:"; cin >> x2 >> y2; start.col = x1; start.row = y1; finish.col = x2; finish.row = y2; cout << "布线的起点:" << start.col << "," << start.row << endl; cout << "布线的终点:" << finish.col << "," << finish.row << endl; // 设置方格阵列“围墙” for (int i = 0; i <= m + 1; i++) { gird[0][i] = gird[m + 1][i] = -1; } // 设置方格行“围墙” for (int i = 0; i <= n + 1; i++) { gird[i][0] = gird[i][n + 1] = -1; } gird[1][3] = -1; gird[2][3] = -1; gird[2][4] = -1; gird[3][5] = -1; gird[4][4] = -1; gird[4][5] = -1; gird[5][1] = -1; gird[5][5] = -1; gird[6][1] = -1; gird[6][2] = -1; gird[6][3] = -1; gird[7][1] = -1; gird[7][2] = -1; gird[7][3] = -1; cout << "布线方格阵如下(0 表示允许布线,-1 表示不允许布线)" << endl; for (int i = 0; i <= m + 1; i++) { for (int j = 0; j <= n + 1; j++) { cout << setw(2) << gird[i][j] << " "; } cout << endl; } FindPath(start, finish, PathLen, path); cout << "布线后方格阵如下(0 表示允许布线,-1 表示不允许布线)" << endl; for (int i = 0; i <= m + 1; i++) { for (int j = 0; j <= n + 1; j++) { cout << setw(2) << gird[i][j] << " "; } cout << endl; } cout << "布线长度为:" << PathLen << endl; cout << "布线路径如下" << endl; for (int i = 0; i < PathLen; i++) { cout << "(" << path[i].col << "," << path[i].row << ")" << "→"; } system("pause"); return 0; }
实验总结:
分支限界法的使用首先要确定一个合理的限界函数,并根据限界函数确定目标函数的界 [down ,up],按照广度优先策略搜索问题的解空间树,在分直结点上依次扩展该结点的孩子 结点,分别估算孩子结点的目标函数可能值,如果某孩子结点的目标函数可能超出目标函数 的界,则将其丢弃;否则将其加入待处理结点表,依次从待处理结点表中选取使目标函数取 得极值的结点成为当前扩展结点,重复上述过程,直到得到最优解。
分支限界采用广度优先搜索的方式去搜索解空间树。搜索过程中,先生成所有的子节点 (分支),然后对所有分支计算一个函数值(限界),并根据这些函数值(计算出的上界或者 下界),从中选择一个使目标函数最优(限界最优)的子节点作为扩展结点,使得搜索朝着 最优解的方向快速推进,从而很快求得一个最优解。
即就是每次从所有子节点中找出一个最 有潜力的,作为扩展结点进行下一次的 BFS。