问题介绍
任务分配问题:将n个任务分配给n个人,每人一个任务,任务不能重复分配,使得总工作代价最小化。
问题分析
利用分支界限法,求出选择后的下界。例如:
{9, 2, 7, 8},
{6, 4, 3, 7},
{5, 8, 1, 8},
{7, 6, 9, 4}
上述表的含义为:行是一个人对应工作,列是每个工作对于不同人。第一行9,2, 7, 8则是在针对这个人,第一个工作的工作代价为9,第二个工作的工作代价为2,以此类推。
在这个问题中,每次求下界,这个下界就是在这个选择以内的最好情况,初始时选择2+3+1+4,哪怕违背了同一个任务不能选择多次的原则,但这绝对是最小的工作代价!所以这就是初始的下界。
状态空间树如图所示:
为第一个人选择任务,固定下一个对象,随后下界改变,例如固定选择9,则后续的第二人选择其他任务,依次下去……按照深度遍历遍历第一人选择9后的情况,得到该情况的下界,将该情况下界和当前的下界比较。
当我们有更好的下界情况,就对其他部分进行剪枝操作,毕竟,对不好的情况求解只是浪费时间。
代码
采用分支界限法,使用优先队列来存储待扩展的结点,每次选择具有最小下界估计值的结点进行扩展。在扩展过程中,生成所有可行的子结点,并计算它们的下界估计值。若某个子结点是完整的解,则更新当前的最优解。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.PriorityQueue;
public class Assignment {
static int[][] cost = {
{9, 2, 7, 8},
{6, 4, 3, 7},
{5, 8, 1, 8},
{7, 6, 9, 4}
};
static int bestSoFar = 9 + 7 + 8 + 9; // 取每一行的最大值相加
public static void main(String[] args) {
// 初始化优先队列,备用
PriorityQueue<Node> S = new PriorityQueue<>();
// 初始化一个空状态空间树的结点,即根结点
Node root = new Node();
// 计算第一个目标估计值
root.lowerBound = getLowerBound(root);
// 优先队列中加入根结点,然后准备从根结点开始对状态空间树进行深度优先遍历
S.add(root);
while (!S.isEmpty()) {
Node node = S.remove(); // 贪心策略:取出lowerBound最小的结点
if (node.lowerBound >= bestSoFar) continue; //剪枝
for (Node child : allFeasibleChildren(node)) { // 对每一个**可扩展**的子结点
if (isACompleteSolution(child)) { // 如果这个子结点已经是叶子结点
bestSoFar = Math.min(bestSoFar, getLowerBound(child)); // 更新best_so_far
} else
S.add(child); //加入到优先队列中
}
}
System.out.print(bestSoFar);
}
private static int getLowerBound(Node node) {
int lb = 0;
// 依照node的信息和权值矩阵cost,计算当前结点的下界估计值
// 1. 估计值 = 已经分配的工作代价+未分配的工作代价
for (int i = 0; i < node.depth; i++) {
lb += cost[i][node.partialSolution.get(i)];
}
for (int i = node.depth; i < 4; i++) {
lb += minRemainingCost(cost, node.partialSolution, i);
}
return lb;
}
private static int minRemainingCost(int[][] cost, HashMap<Integer, Integer> partialSolution, int i) {
int min = Integer.MAX_VALUE;
for (int j = 0; j < 4; j++) {
if (!partialSolution.containsValue(j)) {
min = Math.min(min, cost[i][j]);
}
}
return min;
}
private static Iterable<Node> allFeasibleChildren(Node node) {
// nodeList是用来返回的可迭代对象,包含参数node的所有**可扩展**子结点
ArrayList nodeList = new ArrayList();
// 1. 确保node的深度可以保证进一步扩展子结点
// Insert your code here
if (node.depth >= 4) {
return nodeList;
}
// 2. 对所有可能的孩子进行遍历
for (int i = 0; i < 4; i++) {
// 如果孩子是可扩展的(即不做重复的,先前别人做过的工作)
if (feasible(node, i)) {
// 0. 创建孩子结点:注意维护孩子结点的所有内部属性:
Node child = new Node();
// 1. partialSolution
child.partialSolution = new HashMap<>(node.partialSolution);
child.partialSolution.put(node.depth, i);
// 2. depth
child.depth = node.depth + 1;
// 3. lowerBound
child.lowerBound = getLowerBound(child);
nodeList.add(child);
}
}
return nodeList;
}
private static boolean isACompleteSolution(Node n) {
return n.depth == 4;//检查是否深度遍历完成
}
private static boolean feasible(Node n, int i) {
return !n.partialSolution.containsValue(i);//检查是否被分配
}
}
import java.util.HashMap;
public class Node implements Comparable<Node>{
// 实现这个接口是优先队列的要求:队列的元素要能比较大小
// 状态空间树上的后继(子)结点避免和父结点(以及祖先结点)重复做同样的任务。
// 如果每个结点都记录父结点,则可以判定是否和祖先结点做的任务冲突。
// 为了使得查找更容易,这里还是将所有的祖先结点都保存下来了。
// HashMap partialSolution;
HashMap<Integer, Integer> partialSolution;
int lowerBound;
int depth;
public Node() {
partialSolution = new HashMap();
lowerBound = 0;
depth = 0;
}
@Override
public int compareTo(Node o) {
// 优先队列中存放的都是Node对象,对象的大小由lowerBound来决定。
// 如果当前对象小于参数对象o,返回负数;等于参数对象o,返回0;
// 大于参数对象o,返回正数
return Integer.compare(this.lowerBound, o.lowerBound);
}
}