00 前言
之前一直做啟發式算法,最近突然對精確算法感興趣了。但是這玩意兒說實話是真的難,剛好boss又叫我學學column generation求解VRP相關的內容。一看里面有好多知識需要重新把握,所以這段 時間就打算好好學學精確算法。屆時會把學習過程記錄下來,也方便大家學習!
01 什么是branch and bound(定義)?
1.1 官方一點[1]
Branch and bound (BB, B&B, or BnB) is an algorithm design paradigm for discrete and combinatorial optimization problems, as well as mathematical optimization. A branch-and-bound algorithm consists of a systematic enumeration of candidate solutions by means of state space search: the set of candidate solutions is thought of as forming a rooted tree with the full set at the root. The algorithm explores branches of this tree, which represent subsets of the solution set. Before enumerating the candidate solutions of a branch, the branch is checked against upper and lower estimated bounds on the optimal solution, and is discarded if it cannot produce a better solution than the best one found so far by the algorithm.
The algorithm depends on efficient estimation of the lower and upper bounds of regions/branches of the search space. If no bounds are available, the algorithm degenerates to an exhaustive search.
1.2 通俗一點
分支定界算法始終圍繞着一顆搜索樹進行的,我們將原問題看作搜索樹的根節點,從這里出發,分支的含義就是將大的問題分割成小的問題。大問題可以看成是搜索樹的父節點,那么從大問題分割出來的小問題就是父節點的子節點了。分支的過程就是不斷給樹增加子節點的過程。而定界就是在分支的過程中檢查子問題的上下界,如果子問題不能產生一比當前最優解還要優的解,那么砍掉這一支。直到所有子問題都不能產生一個更優的解時,算法結束。
由此可見,其實分支定界有一股很大的枚舉意味在里面,只不過加上了定界的過程以后,變成了一種非常有規律的枚舉。
02 原理解析
為了讓大家更好理解分支定界的原理,這里小編舉一個求解整數規划的例子來給大家演示分支定界算法的具體過程。
首先,對於一個整數規划模型:
因為求解的是最大化問題,我們不妨設當前的最優解BestV為-INF,表示負無窮。
- 首先從主問題分出兩支子問題:
通過線性松弛求得兩個子問題的upper bound為Z_LP1 = 12.75,Z_LP2 = 12.2。由於Z_LP1 和Z_LP2都大於BestV=-INF,說明這兩支有搞頭。繼續往下。
- 從節點1和節點2兩個子問題再次分支,得到如下結果:
子問題3已經不可行,無需再理。子問題4通過線性松弛得到最優解為10,剛好也符合原問題0的所有約束,在該支找到一個可行解,更新BestV = 10。
子問題5通過線性松弛得到upper bound為11.87>當前的BestV = 10,因此子問題5還有戲,待下一次分支。而子問題6得到upper bound為9<當前的BestV = 10,那么從該支下去找到的解也不會變得更好,所以剪掉!
- 對節點5進行分支,得到:
子問題7不可行,無需再理。子問題8得到一個滿足原問題0所有約束的解,但是目標值為4<當前的BestV=10,所以不更新BestV,同時該支下去也不能得到更好的解了。
- 此時,所有的分支遍歷都完成,我們最終找到了最優解。
02 算法框架
分支定界法(branch and bound)是一種求解整數規划問題的最常用算法。這種方法不但可以求解純整數規划,還可以求解混合整數規划問題。上面用了求解整數規划的例子,這雖然有助於我們更好理解這個算法,但是針對整數規划這一特定問題的過程描述,有可能會對我們的思維帶來局限性。而不能更好的理解該算法的精髓。所以小編決定,在這一節里面,將一個更通用的算法框架呈現出來,以便大家能更好的了解分支定界算法的真正精髓所在。
假設我們求的是最小化問題 minimize f(x)。branch and bound的過程可以描述如下:[1]
1. Using a heuristic, find a solution xh to the optimization problem. Store its value, B = f(x_h). (If no heuristic is available, set B to infinity.) B will denote the best solution found so far, and will be used as an upper bound on candidate solutions.
2. Initialize a queue to hold a partial solution with none of the variables of the problem assigned.
3. Loop until the queue is empty:
3.1. Take a node N off the queue.
3.2. If N represents a single candidate solution x and f(x) < B, then x is the best solution so far. Record it and set B ← f(x).
3.3. Else, branch on N to produce new nodes Ni. For each of these:
3.3.1. If bound(N_i) > B, do nothing; since the lower bound on this node is greater than the upper bound of the problem, it will never lead to the optimal solution, and can be discarded.
3.3.2. Else, store Ni on the queue.
其實代碼該過程描述也很明了了。第1步可以用啟發式找一個當前最優解B出來,如果不想也可以將B設置為正無窮。對於一個最小化問題而言,肯定是子問題的lower bound不能超過當前最優解,不然超過了該子問題就廢了。
第2第3步主要是用隊列取構建一個搜索樹進行搜索,具體的搜索方式由queue這個數據結構決定的。
前面我們講了,B&B是圍繞着一顆搜索樹進行的,那么對於一棵樹而言就有很多種搜索方式:
-
Breadth-first search (BFS):廣度優先搜索,就是橫向搜索,先搜索同層的節點。再一層一層往下。這種搜索可以用FIFO queue實現。
-
Depth-first search (DFS):深度優先搜索,就是縱向搜索,先一個分支走到底,再跳到另一個分支走到底。這種搜索可以用LIFO queue也就是棧實現。
-
Best-First Search:最佳優先搜索,最佳優先搜索算法是一種啟發式搜索算法(Heuristic Algorithm),其基於廣度優先搜索算法,不同點是其依賴於估價函數對將要遍歷的節點進行估價,選擇代價小的節點進行遍歷,直到找到目標點為止。這種搜索可以用優先隊列priority queue來實現。
03 偽代碼描述[1]
按照上述框架的過程,下面提供了一個很詳細的C++偽代碼:
// C++-like implementation of branch and bound,
// assuming the objective function f is to be minimized
CombinatorialSolution branch_and_bound_solve(
CombinatorialProblem problem,
ObjectiveFunction objective_function /*f*/,
BoundingFunction lower_bound_function /*g*/)
{
// Step 1 above
double problem_upper_bound = std::numeric_limits<double>::infinity; // = B
CombinatorialSolution heuristic_solution = heuristic_solve(problem); // x_h
problem_upper_bound = objective_function(heuristic_solution); // B = f(x_h)
CombinatorialSolution current_optimum = heuristic_solution;
// Step 2 above
queue<CandidateSolutionTree> candidate_queue;
// problem-specific queue initialization
candidate_queue = populate_candidates(problem);
while (!candidate_queue.empty()) { // Step 3 above
// Step 3.1
CandidateSolutionTree node = candidate_queue.pop();
// "node" represents N above
if (node.represents_single_candidate()) { // Step 3.2
if (objective_function(node.candidate()) < problem_upper_bound) {
current_optimum = node.candidate();
problem_upper_bound = objective_function(current_optimum);
}
// else, node is a single candidate which is not optimum
}
else { // Step 3.3: node represents a branch of candidate solutions
// "child_branch" represents N_i above
for (auto&& child_branch : node.candidate_nodes) {
if (lower_bound_function(child_branch) <= problem_upper_bound) {
candidate_queue.enqueue(child_branch); // Step 3.3.2
}
// otherwise, g(N_i) > B so we prune the branch; step 3.3.1
}
}
}
return current_optimum;
}
有了偽代碼加持,相信大家對上面的過程以及branch and bound 算法也有了一個透徹的了解。后面我們還會推出關於branch and bound的相關代碼實現,包括各種數據結構實現的各種搜索方式等等,請關注我們的公眾號以獲取最新的消息:
可以關注我們的公眾號哦!獲取更多精彩消息!
04 reference
- [1] https://en.wikipedia.org/wiki/Branch_and_bound
- [2] OR-Wayne Winston