package bgu.bio.algorithms.graphs;
import gnu.trove.list.array.TIntArrayList;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Random;
import bgu.bio.adt.graphs.FlexibleUndirectedGraph;
import bgu.bio.adt.tuples.IntDoublePair;
/**
* This class finds the maximum weighted clique in the current graph according
* to the algorithm presented in: <i>Improving the maximum-weight clique
* algorithm for the dense graphs</i> by <i>Kumlander Deniss</i> from 2006. In
* this implementation the case where you don't select a node from a color class
* is implemented using a dummy node. <br/>
* <br/>
* <b>This class is not Thread safe.</b>
*
* @author milon
*/
public class MaximalWeightedClique {
private static NumberFormat numberFormat = new DecimalFormat("0.00");
/**
* Used in the coloring process for random coloring
*/
private Random rand;
/**
* Helper list, helps in keeping the ordering of the nodes in the graph
*/
private TIntArrayList nodeOrdering;
/**
* used to color the graph
*/
private GraphColoring coloring;
/**
* Holds the current graph
*/
private FlexibleUndirectedGraph graph;
/**
* Holds the temporary graph
*/
private FlexibleUndirectedGraph tmpGraph;
private FlexibleUndirectedGraph subgraph1, subgraph2;
/**
* A {@link Comparator} used to sort the colors by decreasing node weights
*/
private Comparator<ArrayList<IntDoublePair>> classesComparator;
/**
* A {@link Comparator} used to sort each color class according to the
* weights
*/
private Comparator<IntDoublePair> nodeComparator;
/**
* Contains the maximal number of milliseconds the code should run
*/
private long timeout;
/**
* true if and only if the last call to
* {@link #findMaximumWeightedClique(double[])} or
* {@link #findMaximumWeightedClique(double[], TIntArrayList)} timed out
*/
private boolean timedout;
/**
* if given is used as the initial score value of the "best" clique
*/
private double pruneStart;
/**
* The dummy node in the graph, this is used to represent the case where you
* don't choose a node
*/
private final IntDoublePair dummy;
/**
* Holds statistical information
*/
private double[] stats;
/**
* Holds statistical information header texts
*/
private String[] statsNames;
private long type1, type2;
public MaximalWeightedClique() {
this.dummy = new IntDoublePair(-1, 0.0);
this.classesComparator = new ClassesComparatorByDegree(false);
this.nodeComparator = new NodeInformationComparator();
coloring = new GraphColoring();
nodeOrdering = new TIntArrayList();
rand = new Random();
statsNames = new String[] { "Amount", "Nodes", "Density", "MaxDegree",
"Colors", "NewNodes/Nodes", "Color by Degeneracy",
"Color by Degree", "Color by Random", "Timeout" };
stats = new double[statsNames.length];
this.graph = new FlexibleUndirectedGraph();
this.tmpGraph = new FlexibleUndirectedGraph();
this.subgraph1 = new FlexibleUndirectedGraph();
this.subgraph2 = new FlexibleUndirectedGraph();
}
/**
* Calculate the maximal weighted clique in the current graph (set by
* {@link #setGraph(FlexibleUndirectedGraph)}). if {@link #setTimeout(long)}
* or {@link #setPruneScore(double)(double)} were given, then they may be a
* case where not the optimal result is calculated but a local optimal.
*
* @param weights
* the weights of the nodes in the graph
* @return the maximal weighted clique in the graph, if exist
*/
public TIntArrayList findMaximumWeightedClique(double[] weights) {
return findMaximumWeightedClique(weights, null);
}
/**
* Same as {@link #findMaximumWeightedClique(double[])} only with the
* additional constraint that we want the clique to contain <b>at least
* one</b> of the nodes given in the additional list
*
* @see {@link #findMaximumWeightedClique(double[])}
* @param weights
* the weights of the nodes in the graph
* @param mandatory
* the list of mandatory nodes. if the given list is null it is
* equivalent to the case where all the nodes are mandatory
* @return The maximal clique containing at least one of the nodes in the
* mandatory list
*/
public TIntArrayList findMaximumWeightedClique(double[] weights,
TIntArrayList mandatory) {
type1 = 0;
type2 = 0;
FlexibleUndirectedGraph tmp = graph;
this.graph.copyTo(tmpGraph);
graph = tmpGraph;
detachExtendableNodes(weights, mandatory);
// split the graph to two subgraphs for the coloring
subgraph1.clear();
subgraph2.clear();
graph.split(mandatory, subgraph1, subgraph2);
int[] color1 = new int[subgraph1.getNodeNum()];
int amountOfColors1 = colorGraph(subgraph1, color1, weights);
int[] color2 = new int[subgraph2.getNodeNum()];
int amountOfColors2 = colorGraph(subgraph2, color2, weights);
int amountOfColors = Math.max(amountOfColors1, amountOfColors2);
// step 0
this.updateStats(mandatory, amountOfColors);
ArrayList<ArrayList<IntDoublePair>> colorClasses = new ArrayList<ArrayList<IntDoublePair>>();
int mandatoryClasses = fillColorClasses(weights, mandatory, color1,
amountOfColors1, color2, amountOfColors2, colorClasses);
WeightedClique clique = new WeightedCliqueTree();
clique.setGraph(graph);
ArrayList<IntDoublePair> bestClique = new ArrayList<IntDoublePair>(
colorClasses.size());
// calculate the maximum gain from each position for the pruning
// including the current
double[] maximas = new double[colorClasses.size()];
for (int i = 0; i < maximas.length; i++) {
double max = Double.NEGATIVE_INFINITY;
ArrayList<IntDoublePair> list = colorClasses.get(i);
for (int j = 0; j < list.size(); j++) {
if (max < list.get(j).getSecond()) {
max = list.get(j).getSecond();
}
}
maximas[i] = max;
}
double[] maxGainInFuture = new double[maximas.length];
double[][] maxGainSorted = new double[maximas.length][maximas.length];
fillFutureGainEstimations(maximas, maxGainInFuture, maxGainSorted);
int[] positions = new int[colorClasses.size()];
Arrays.fill(positions, -1);
double bestWeight = pruneStart;
final boolean checkTimeout = !(timeout == 0);
long startTime = System.currentTimeMillis();
timedout = false;
int currentClass = 0;
while (currentClass >= 0) {
positions[currentClass]++;
int currentPos = positions[currentClass];
final ArrayList<IntDoublePair> currentColorClass = colorClasses
.get(currentClass);
if (currentPos > 0 && currentPos <= currentColorClass.size() - 1) {
clique.removeLast();
}
// we can stop after trying all the mandatory nodes as start
// positions
if (currentClass == mandatoryClasses && clique.size() == 0) {
break;
}
if (currentPos >= currentColorClass.size()
|| (shouldPrune(clique, currentClass, maxGainInFuture,
maxGainSorted, bestWeight))) {
positions[currentClass] = -1;
currentClass--;
if (checkTimeout
&& System.currentTimeMillis() - startTime > timeout) {
positions[currentClass] = -1;
currentClass = -1000;
stats[stats.length - 1]++;
timedout = true;
}
} else if (clique.potential() == 0) {
if (clique.weight() > bestWeight) {
bestWeight = clique.weight();
bestClique.clear();
clique.copyTo(bestClique);
}
positions[currentClass] = -1;
currentClass--;
} else {
boolean notFound = true;
while (notFound && currentPos < currentColorClass.size()) {
final IntDoublePair candidate = currentColorClass
.get(currentPos);
if (!canJoin(clique, candidate)) {
currentPos++;
} else {
if (candidate != dummy) {
clique.add(candidate);
}
notFound = false;
}
}
positions[currentClass] = currentPos;
currentClass++;
if (currentClass == positions.length) {
/*
* found a clique, check if it is better than the current
* best clique so far
*/
if (clique.weight() > bestWeight) {
bestWeight = clique.weight();
bestClique.clear();
clique.copyTo(bestClique);
}
currentClass--;
}
}
}
TIntArrayList ans = new TIntArrayList();
for (int i = 0; i < bestClique.size(); i++) {
if (bestClique.get(i) != dummy) {
ans.add(bestClique.get(i).getFirst());
}
}
graph = tmp;
return ans;
}
/**
* @param weights
* @param mandatory
* @param color1
* @param amountOfColors1
* @param color2
* @param amountOfColors2
* @param colorClasses
* @return
*/
public int fillColorClasses(double[] weights, TIntArrayList mandatory,
int[] color1, int amountOfColors1, int[] color2,
int amountOfColors2,
ArrayList<ArrayList<IntDoublePair>> colorClasses) {
ArrayList<ArrayList<IntDoublePair>> colorClassesRegular = new ArrayList<ArrayList<IntDoublePair>>();
ArrayList<ArrayList<IntDoublePair>> colorClassesMandatory = new ArrayList<ArrayList<IntDoublePair>>();
// build the colorClasses
fillColorClasses(amountOfColors1, amountOfColors2, color1, color2,
weights, mandatory, colorClassesRegular, colorClassesMandatory);
sortEachClassByWeight(colorClassesMandatory);
sortEachClassByWeight(colorClassesRegular);
Collections.sort(colorClassesRegular, this.classesComparator);
Collections.sort(colorClassesMandatory, this.classesComparator);
// merge the colorClasses
int mandatoryClasses = 0;
for (int i = 0; i < colorClassesMandatory.size(); i++) {
if (colorClassesMandatory.get(i).size() > 0) {
colorClasses.add(colorClassesMandatory.get(i));
mandatoryClasses++;
}
}
for (int i = 0; i < colorClassesRegular.size(); i++) {
if (colorClassesRegular.get(i).size() > 0) {
colorClasses.add(colorClassesRegular.get(i));
}
}
// add dummy to every list
for (int i = 0; i < colorClasses.size(); i++) {
colorClasses.get(i).add(dummy);
}
return mandatoryClasses;
}
/**
* @param maximas
* @param maxGainInFuture
* @param maxGainSorted
*/
public void fillFutureGainEstimations(double[] maximas,
double[] maxGainInFuture, double[][] maxGainSorted) {
maxGainInFuture[maxGainInFuture.length - 1] = maximas[maxGainInFuture.length - 1];
for (int i = maxGainInFuture.length - 2; i >= 0; i--) {
maxGainInFuture[i] = maxGainInFuture[i + 1] + maximas[i];
}
for (int i = 0; i < maxGainSorted.length; i++) {
for (int j = i; j < maxGainSorted.length; j++) {
maxGainSorted[i][j] = maximas[j];
}
Arrays.sort(maxGainSorted[i]);
reverse(maxGainSorted[i]);
}
}
/**
* @return The list of nodes that can be extended in the graph
*/
private TIntArrayList detachExtendableNodes(double[] weights,
TIntArrayList mandatory) {
TIntArrayList list = new TIntArrayList();
TIntArrayList list1 = new TIntArrayList();
TIntArrayList list2 = new TIntArrayList();
for (int u = 0; u < graph.getNodeNum(); u++) {
boolean isFirstMandatory = mandatory != null ? mandatory
.binarySearch(u) >= 0 : false;
list1.resetQuick();
for (int d = 0; d < graph.deg(u); d++) {
list1.add(graph.getNeighbor(u, d));
}
list1.sort();
for (int v = 0; v < graph.getNodeNum(); v++) {
boolean isSecondMandatory = mandatory != null ? mandatory
.binarySearch(v) >= 0 : false;
if (u != v
&& graph.deg(u) <= graph.deg(v)
&& graph.deg(u) > 0
&& ((isFirstMandatory && isSecondMandatory) || (!isFirstMandatory && !isSecondMandatory))
&& !graph.isNeighbours(u, v)
&& weights[u] <= weights[v]) {
// scan the lists
list2.resetQuick();
for (int d = 0; d < graph.deg(v); d++) {
list2.add(graph.getNeighbor(v, d));
}
list2.sort();
boolean contains = true;
for (int x = 0; contains && x < list1.size(); x++) {
if (list2.binarySearch(list1.get(x)) < 0) {
contains = false;
}
}
if (contains) {
list.add(u);
graph.detachNode(u);
v = 2 * graph.getNodeNum();
}
}
}
}
return list;
}
private void reverse(double[] arr) {
int top = arr.length - 1;
int bottom = 0;
double tmp;
while (top > bottom) {
tmp = arr[top];
arr[top] = arr[bottom];
arr[bottom] = tmp;
bottom++;
top--;
}
}
/**
* Update the statistics array
*
* @param mandatory
* the mandatory list
* @param amountOfColors
*/
private void updateStats(TIntArrayList mandatory, int amountOfColors) {
this.stats[0]++;
this.stats[1] += graph.getNodeNum();
this.stats[2] += (2 * (double) graph.getEdgeNum())
/ (graph.getNodeNum() * (graph.getNodeNum() - 1));
this.stats[3] += graph.getMaxDeg();
this.stats[4] += amountOfColors;
this.stats[5] += mandatory == null ? 1.0 : mandatory.size()
/ ((double) graph.getNodeNum());
}
/**
* Calculate the best way to color the graph. the method tries several
* methods of coloring: degeneracy, weight, degree and random.
*
* @param color
* the array that will contain the coloring of the nodes
* @param weights
* the weights of the nodes
* @return the best found coloring of the graph
*/
private int colorGraph(FlexibleUndirectedGraph theGraph, int[] color,
double[] weights) {
nodeOrdering.clear();
theGraph.degeneracy(nodeOrdering);
coloring.setGraph(theGraph);
// color by degeneracy
int amountOfColors = coloring.greedyColoring(nodeOrdering);
coloring.copyColoringTo(color);
int colorBy = 0;
// only try in the case where there is more than two.
if (amountOfColors > 2) {
// check coloring by degree and weight
int amountOfColorsByDegree = coloring.greedyColoringByDegree();
if (amountOfColors > amountOfColorsByDegree) {
amountOfColors = amountOfColorsByDegree;
coloring.copyColoringTo(color);
colorBy = 1;
}
final int tries = theGraph.getNodeNum() / 4;
for (int i = 0; i < tries; i++) {
nodeOrdering.shuffle(rand);
int tmp = coloring.greedyColoring(nodeOrdering);
if (tmp < amountOfColors) {
amountOfColors = tmp;
coloring.copyColoringTo(color);
colorBy = 2;
}
}
}
stats[colorBy + 6]++;
return amountOfColors;
}
/**
* @param amountOfColors
* @param color
* @param weights
* @param mandatory
* @param colorClassesRegular
* @param colorClassesMandatory
*/
private void fillColorClasses(int amountOfColors1, int amountOfColors2,
int[] color1, int[] color2, double[] weights,
TIntArrayList mandatory,
ArrayList<ArrayList<IntDoublePair>> colorClassesRegular,
ArrayList<ArrayList<IntDoublePair>> colorClassesMandatory) {
if (mandatory != null) {
mandatory.sort();
}
colorClassesRegular.clear();
colorClassesMandatory.clear();
for (int i = 0; i < amountOfColors1; i++) {
colorClassesMandatory.add(new ArrayList<IntDoublePair>());
}
for (int i = 0; i < amountOfColors2; i++) {
colorClassesRegular.add(new ArrayList<IntDoublePair>());
}
for (int node = 0; node < graph.getNodeNum(); node++) {
if (mandatory == null || mandatory.binarySearch(node) >= 0) {
colorClassesMandatory.get(color1[node]).add(
new IntDoublePair(node, weights[node]));
} else {
colorClassesRegular.get(color2[node]).add(
new IntDoublePair(node, weights[node]));
}
}
}
private boolean shouldPrune(WeightedClique currentClique, int currentIndex,
double[] maxGainInFuture, double[][] maxGainSorted,
double bestWeight) {
if (currentClique.weight() > bestWeight || currentClique.size() == 0) {
return false;
}
boolean prune = currentClique.weight() + maxGainInFuture[currentIndex] < bestWeight;
if (prune) {
type1++;
} else {
final int amountToAdd = currentClique.potential();
final double[] mg = maxGainSorted[currentIndex];
double maxGain = 0;
if (amountToAdd < mg.length) {
for (int i = 0; i < amountToAdd; i++) {
maxGain += mg[i];
}
prune = currentClique.weight() + maxGain < bestWeight;
if (prune) {
type2++;
}
}
}
return prune;
}
private boolean canJoin(WeightedClique currentClique,
IntDoublePair candidate) {
if (candidate == dummy) {
// can always add dummy
return true;
}
if (graph.deg(candidate.getFirst()) < currentClique.size()) {
return false;
}
return currentClique.canJoin(candidate);
}
/**
* @param amountOfColors
* @param colorClasses
*/
private void sortEachClassByWeight(
ArrayList<ArrayList<IntDoublePair>> colorClasses) {
for (int i = 0; i < colorClasses.size(); i++) {
Collections.sort(colorClasses.get(i), nodeComparator);
}
}
/**
* Set the graph to be used in the algorithm
*
* @param graph
* the input graph for the algorithm
*/
public void setGraph(FlexibleUndirectedGraph graph) {
this.graph = graph;
}
/**
* set the timeout limit for the algorithm
*
* @param timeInMilliseconds
* time limit in milliseconds
*/
public void setTimeout(long timeInMilliseconds) {
if (timeInMilliseconds > 0) {
this.timeout = timeInMilliseconds;
}
}
/**
* @return true if the last call to
* {@link #findMaximumWeightedClique(double[])} or
* {@link #findMaximumWeightedClique(double[], TIntArrayList)} timed
* out (i.e., {@link #timeout} was reached)
*/
public boolean timedout() {
return timedout;
}
/**
* clear the current time out
*/
public void clearTimeout() {
this.timeout = 0;
}
/**
* Returns the statistics of the class
*
* @return A string representation of the statistics
*/
public String statistics() {
StringBuilder sb = new StringBuilder();
sb.append(statsNames[0]);
sb.append(": ");
sb.append((int) stats[0]);
sb.append("\n");
for (int i = 1; i < stats.length - 1; i++) {
sb.append(statsNames[i]);
sb.append(": ");
sb.append(numberFormat.format(stats[i] / stats[0]));
sb.append("\n");
}
sb.append(statsNames[stats.length - 1]);
sb.append(": ");
sb.append((int) stats[stats.length - 1]);
sb.append("\n");
return sb.toString();
}
public long[] typesOfPrunings() {
return new long[] { type1, type2 };
}
public void setClassesComparator(Comparator<ArrayList<IntDoublePair>> comp) {
this.classesComparator = comp;
}
public void setNodeComparator(Comparator<IntDoublePair> comp) {
this.nodeComparator = comp;
}
public ArrayList<Comparator<ArrayList<IntDoublePair>>> getClassesComparators() {
ArrayList<Comparator<ArrayList<IntDoublePair>>> list = new ArrayList<Comparator<ArrayList<IntDoublePair>>>();
list.add(new ClassesComparatorByWeight(false));
list.add(new ClassesComparatorByWeight(true));
list.add(new ClassesComparatorByAverageWeight(false));
list.add(new ClassesComparatorByAverageWeight(true));
list.add(new ClassesComparatorByDegree(false));
list.add(new ClassesComparatorByDegree(true));
return list;
}
private class ClassesComparatorByWeight implements
Comparator<ArrayList<IntDoublePair>> {
private final int mult;
public ClassesComparatorByWeight(boolean increasing) {
this.mult = increasing ? 1 : -1;
}
@Override
public int compare(ArrayList<IntDoublePair> o1,
ArrayList<IntDoublePair> o2) {
if (o1.size() == 0 && o2.size() == 0) {
return 0;
} else if (o1.size() == 0) {
return -1 * mult;
} else if (o2.size() == 0) {
return 1 * mult;
}
return compare(o1, o2, 0);
}
private int compare(ArrayList<IntDoublePair> o1,
ArrayList<IntDoublePair> o2, int index) {
final double diff = o1.get(index).getSecond()
- o2.get(index).getSecond();
if (diff > 0) {
return 1 * mult;
} else if (diff < 0) {
return -1 * mult;
}
index++;
if (o2.size() <= index && o1.size() <= index) {
return 0;
} else if (o2.size() <= index) {
return 1 * mult;
} else if (o1.size() <= index) {
return -1 * mult;
}
return compare(o1, o2, index);
}
}
private class ClassesComparatorByDegree implements
Comparator<ArrayList<IntDoublePair>> {
private final int mult;
public ClassesComparatorByDegree(boolean increasing) {
this.mult = increasing ? 1 : -1;
}
@Override
public int compare(ArrayList<IntDoublePair> o1,
ArrayList<IntDoublePair> o2) {
final int deg1 = maxDeg(o1);
final int deg2 = maxDeg(o2);
return mult * (deg1 - deg2);
}
private int maxDeg(ArrayList<IntDoublePair> list) {
int maxDeg = 0;
for (int i = 0; i < list.size(); i++) {
final int deg = graph.deg(list.get(i).getFirst());
if (deg > maxDeg) {
maxDeg = deg;
}
}
return maxDeg;
}
}
private class ClassesComparatorByAverageWeight implements
Comparator<ArrayList<IntDoublePair>> {
private final int mult;
public ClassesComparatorByAverageWeight(boolean increasing) {
this.mult = increasing ? 1 : -1;
}
@Override
public int compare(ArrayList<IntDoublePair> o1,
ArrayList<IntDoublePair> o2) {
double sum1 = sum(o1) / o1.size();
double sum2 = sum(o2) / o2.size();
if (sum1 - sum2 < 0) {
return -1 * mult;
} else if (sum1 - sum2 > 0) {
return 1 * mult;
} else {
return 0;
}
}
private double sum(ArrayList<IntDoublePair> list) {
double sum = 0;
for (int i = 0; i < list.size(); i++) {
sum += list.get(i).getSecond();
}
return sum;
}
}
private class NodeInformationComparator implements
Comparator<IntDoublePair> {
@Override
public int compare(IntDoublePair o1, IntDoublePair o2) {
double diff = o1.getSecond() - o2.getSecond();
if (diff == 0) {
return 0;
}
if (diff < 0) {
return 1;
}
return -1;
}
}
}