代码解决思路:
分支限界法常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。
常见的两种分支限界法:
- 队列式(FIFO)分支限界法
按照队列先进先出(FIFO)原则选取下一个结点为扩展结点。 - 优先队列式分支限界法
按照优先队列中规定的优先级选取优先级最高的结点成为当前扩展结点。
本例题采用广度优先+优先队列式分支限界法进行求解
首先为每个结点建立一个邻接表,邻接点按照概率从大到小进行排列,这样可以保证概率大的最先被遍历到,同时也方便后面结合maxPath数组进行快速剪枝。
然后进行广度优先遍历,建立一个队列存储到当前结点的路径的累乘概率值,然后声明一个set用来存储到该结点时路径中经过的结点,同时避免出现回路,再声明一个maxPath数组来保存到当前节点时所有路径的概率乘积的最大值,如果某条路径到当前节点vex时如果累乘概率值小于maxPath[vex],则该路径一定不是最优解,剪枝即可,因为后面的概率都小于1,累乘只会越来越小
广度优先,当队列不空的时候从队头中取出一个节点,如果该节点等于目标节点,则说明找到了一个解,更新一下全局最优概率值maxP,如果到当前节点时概率累乘值已经小于当前节点的最大累乘概率值,剪枝,否则说明从当前节点继续遍历可能得到更优解,更新一下当前节点的最大累乘概率值,然后再遍历当前节点的邻居节点,与其概率值进行累乘,然后放入到队列中去
当队列为空时得到的解一定是最优解。
import java.util.*;
public class Solution {
//用来保存最大概率
double maxP = 0.0;
/**
*广度优先加+分支限界求解
* @param adjacent 邻接表
* @param start 起始节点
* @param end 目标结点
*/
public void bfs(Map<Integer, PriorityQueue<Node>> adjacent, int start, int end) {
//这里BFSNode结点的weight不再是自己的权重而是自己这条路径上的累乘值
//即到该结点之前的某条路径的累乘概率值
Queue<BFSNode> queue = new LinkedList<>();
Set<Integer> set = new HashSet<>();
//这里不使用list,是因为不需要真的求出这条路径,而这个路径更重要的是判断谁已经在路径中了,也就是被访问过,如果被访问过,后续就不再访问,以免形成环
//其所有的祖先结点都在里面
set.add(start);
//将开始节点添加到队列中,开始结点的概率为1
queue.offer(new BFSNode(start, 1.0, set));
//如果从start到end的某条路径概率最大,则该路径上任意两点间的路径概率也必定是最大的
//根据这个性质,如果我们遍历到某点,发现累乘路径概率没有从start出发到该点的另一条累乘路径概率大,则
//该子树显然没有继续搜索的必要了
//要利用这条性质进行剪枝
double[] maxPath = new double[adjacent.size()];
while (!queue.isEmpty()) {
//取出队头节点
BFSNode node = queue.poll();
//取出顶点值
int vex = node.vex;
//取出到该顶点之前的路径权值累乘和
double w = node.pathWeight;
//取出到该顶点时已经遍历过的顶点
Set<Integer> path = node.path;
//如果当前顶点等于目标结点,找到目标结点
if (vex == end) {
//更新一下最大概率值,可能w比maxP大,也可能小(说明不是最优路径),取最大值
maxP = Math.max(maxP, w);
//找到的路径的概率不一定是最大值,所以continue继续寻找
continue;
}
//如果到该结点的概率比当前已经找到某一条路径的最大概率maxP小,那么说明这条路径不是最优的,因为后面的概率也越来越小,相乘会更小,不再执行,剪枝
//在没有找到任意一条路径时,maxP的值为0,所有的路径就不会被剪枝
if (w < maxP || w < maxPath[vex]) continue;//剪枝
//更新到结点vex的最大路径概率
maxPath[vex] = w;
//执行到此,说明当前结点之前的路径的概率乘积大于等于当前的最大概率值,有可能是最优解,继续向下执行
//遍历所有的邻居,类似于广度优先,adjacent.get(vex)是按照概率从大到小获取顶点vex的所有邻居结点
//当前结点分别和邻居结点相乘
for (Node near : adjacent.get(vex)) {
//如果路径上没有该邻居,说明该结点的权值还没有与当前累计权值相乘,相乘以后再以新节点放入队列,避免了形成环
if (!path.contains(near.vex)) {
//将结点vex的邻居结点添加到路径中
Set<Integer> newPath = new HashSet<>();
newPath.addAll(path);
newPath.add(near.vex);
//将该节点之前的累乘概率乘以该到邻居结点的概率,作为一个新结点入队
queue.offer(new BFSNode(near.vex, near.weight * w, newPath));
}
}
}
}
public double maxProbability(int n, int[][] edges, double[] succProb, int start, int end) {
//处理一下边的数据结构使找到邻边更容易
//额外使用一个邻接表用于取某结点的邻居,否则在寻找某个结点的邻接点的时候需要遍历矩阵的一行来判断是否存在边
//PriorityQueue<Node>是优先级队列,可以按照概率大小来保存邻居,从概率大的开始存
//声明一个邻接表
Map<Integer, PriorityQueue<Node>> adjacent = new HashMap<>();
for (int i = 0; i < n; i++) {
//初始化邻接表,设置优先级队列的比较规则,实现逆序排列
PriorityQueue<Node> neighbors = new PriorityQueue<>((o1, o2) -> {
if (o1.weight < o2.weight) return 1;
if (o1.weight > o2.weight) return -1;
return 0;
});
//初始化邻接表,邻居是按照概率从大到小来排的,初始都为空
adjacent.put(i, neighbors);
}
//构建邻接表
for (int i = 0; i < succProb.length; i++) {
// 取出每条边的两个顶点,存储形式为:edges = [[0,1],[1,2],[0,2]]
int node1 = edges[i][0];
int node2 = edges[i][1];
//获取键node1所映射的值,即该结点的邻居,将当前边的邻居添加进去,添加进去的结点会按照概率从大到小进行排序
//即每个结点的邻接表都是按照概率从大到小排列的
//从大到小排列可以在后面结合maxPath进行快速剪枝,如果无序排列,会增加无用的遍历
adjacent.get(node1).add(new Node(node2, succProb[i]));
adjacent.get(node2).add(new Node(node1, succProb[i]));
}
//深度优先遍历
bfs(adjacent, start, end);
return maxP;
}
public static void main(String[] args) {
int n = 1000;
int[][] edges = new int[][]{ 测试例 };
double[] succPro = new double[] {测试例};
int start = 112;
int end = 493;
Solution pp = new Solution();
System.out.println(pp.maxProbability(n, edges, succPro, start, end));
}
}
class Node{
int vex;
double weight;
Node(int vex, double weight) {
this.vex = vex;
this.weight = weight;
}
}
class BFSNode {
int vex;
//这里node结点的weight不再是自己的权重而是自己这条路径上的累积值
double pathWeight;
//这里不使用list,是因为不需要真的求出这条路径,而这个路径更重要的是判断谁已经在路径中了,也就是被访问过
//其所有的祖先结点都在里面
Set<Integer> path;
BFSNode(int vex, double pathWeight, Set<Integer> path) {
this.vex = vex;
this.pathWeight = pathWeight;
this.path = path;
}
}