Lets say there are 10 boys and 10 girls in speed date. 10 boys select 4 girls and rank them. 10 girls select 4 boys and rank them. In the end the winning pairs would be returned.
Finer details such as tie-breakers, rules of input params etc, are well documented. Looking for code review, optimization and best practices.
final class Pair {
private final String male;
private final String female;
public Pair(String male, String female) {
this.male = male;
this.female = female;
}
public String getMale() {
return male;
}
public String getFemale() {
return female;
}
@Override
public String toString() {
return male + " dates: " + female;
}
}
/**
*
* Provides a thread safe solution Solves the speed date problem.
*
* Example:
* --------
* - Lets say there are 10 boys and 10 girls in speed date
* - 10 boys select 4 girls and rank them.
* - 10 girls select 4 boys and rank them.
* - in the end the winning pairs would be returned.
*
* Complexity:
* Time complexity: O(nlogn)
* Space complexity: O(n)
*
*/
public class SpeedDateCompute {
private final Map<String, List<String>> maleChoices;
private final Map<String, List<String>> femaleChoices;
/**
* Does not make a deep copy of maps thus client is not expected to change maleChoice, femaleChoice.
* Expects consistency in males and female maps, else results are unpredictable.
*
* By consistency it means:
* - The number of boys and girls should be the same.
* - Each boy and each girl should make same number of choices
* - Choices should mention "only existing members of opposite sex"
*
*
* @param maleChoices
* @param femaleChoices
*/
public SpeedDateCompute(Map<String, List<String>> maleChoices, Map<String, List<String>> femaleChoices) {
validate(maleChoices, femaleChoices);
this.maleChoices = maleChoices;
this.femaleChoices = femaleChoices;
}
private static class Edge {
String male;
String female;
int malesPreference; // what does male ranks this female ?
int femalePreference; // what does female ranks this male ?
Edge (String male, String female, int malesPreference, int femalePreference) {
this.male = male;
this.female = female;
this.malesPreference = malesPreference;
this.femalePreference = femalePreference;
}
}
private static class EdgeComparator implements Comparator<Edge> {
@Override
public int compare(Edge edge1, Edge edge2) {
int val = rank (edge1.femalePreference - 1, edge1.malesPreference - 1)
- rank (edge2.femalePreference - 1, edge2.malesPreference - 1);
return val;
}
/*
* Ranking algorithm.
*
* Let notation (1,2) mean - femalePrerence is 1 and malePreference is 2.
* (1, 1) means both rank each other as their best so this pair triumps over (2, 2)
* but there is a tie between (1, 2) and (2, 1)
* As a tie breaker we give preference to feelings of women
* thus (1,2) > (2, 1)
*
* Thus the rule is expanded to
*
* (1, 1) > (1, 2) > (2, 1) > (2, 2)
*
* This can be thought of as a matrix of the form
* (lil lazy to retype the whole thing, plz check the link)
* http://math.stackexchange.com/questions/720797/whats-the-name-of-this-sort-of-matrix
* http://codereview.stackexchange.com/questions/44939/a-matrix-with-square-diagonals-and-transpose-being-increments-of-other
*
* @param row the female choice
* @param col the male choice
* @return
*/
private int rank(int row, int col) {
if (row == col) {
return row * row;
}
int rank = (col) * (col) + 1 + (2 * row) ;
if (row < col) {
return rank;
} else {
return rank + 1;
}
}
}
private void validate (Map<String, List<String>> maleChoices, Map<String, List<String>> femaleChoices) {
if (maleChoices == null || femaleChoices == null) {
if (maleChoices != null) {
throw new NullPointerException("female choice map should not be null");
}
if (femaleChoices != null) {
throw new NullPointerException("male choice map should not be null");
}
throw new NullPointerException("no choice map should be null.");
}
if (maleChoices.size() == 0 || femaleChoices.size() == 0) {
if (maleChoices.size() == 0) {
throw new IllegalArgumentException("male choice map should not be empty.");
}
if (femaleChoices.size() == 0) {
throw new IllegalArgumentException("female choice map should not be empty.");
}
throw new IllegalArgumentException("no choice map should be empty.");
}
}
public List<Pair> getPairs() {
final Collection<Edge> edges = parseChoiceMaps();
final List<Edge> sortedEdges = sort(edges);
return createPairs(sortedEdges);
}
/**
* Given a directed bipartite graph, it converts bipartite graph into
* undirected map.
*
* @param choiceMap the bipartite graph of choices
* @return
*/
private Collection<Edge> parseChoiceMaps() {
final Map<String, Edge> combinedMaleChoice = new HashMap<String, Edge>();
synchronized (maleChoices) {
// parsing male choices.
for (Entry<String, List<String>> maleChoice : maleChoices.entrySet()) {
String maleName = maleChoice.getKey();
List<String> females = maleChoice.getValue();
for (int i = 0; i < females.size(); i++) {
// mark female preference as "size" thus assigning the worst rank as default.
combinedMaleChoice.put(maleName + females.get(i), new Edge(maleName, females.get(i), i + 1,
maleChoices.size()));
}
}
}
synchronized (femaleChoices) {
// parsing female choices
for (Entry<String, List<String>> femaleChoice : femaleChoices.entrySet()) {
List<String> males = femaleChoice.getValue();
for (int i = 0; i < males.size(); i++) {
String name = males.get(i) + femaleChoice.getKey();
// check if any male has chosen this female, else skip.
if (combinedMaleChoice.containsKey(name)) {
combinedMaleChoice.get(males.get(i) + femaleChoice.getKey()).femalePreference = i + 1;
}
}
}
}
return combinedMaleChoice.values();
}
private List<Edge> sort(Collection<Edge> edges) {
final List<Edge> edgeList = new ArrayList<Edge>(edges);
Collections.sort(edgeList, new EdgeComparator());
return edgeList;
}
private List<Pair> createPairs(List<Edge> edges) {
final Set<String> hookedUpMales = new LinkedHashSet<String>();
final Set<String> hookedUpFemales = new LinkedHashSet<String>();
for (Edge edge : edges) {
if (hookedUpMales.contains(edge.male) || hookedUpFemales.contains(edge.female)) {
continue;
}
hookedUpMales.add(edge.male);
hookedUpFemales.add(edge.female);
}
return mapSetsToPairs(hookedUpMales, hookedUpFemales);
}
private List<Pair> mapSetsToPairs(Set<String> hookedUpMales, Set<String> hookedUpFemales) {
Iterator<String> maleItr = hookedUpMales.iterator();
Iterator<String> femaleItr = hookedUpFemales.iterator();
final List<Pair> pairs = new ArrayList<Pair>();
while (maleItr.hasNext() && femaleItr.hasNext()) {
pairs.add(new Pair(maleItr.next(), femaleItr.next()));
maleItr.remove(); // just to optimize space
femaleItr.remove(); // just to optimize space
}
return pairs;
}
public static void main(String[] args) {
/*
* Test case 1
*/
Map<String, List<String>> maleChoice1 = new HashMap<String, List<String>>();
Map<String, List<String>> femaleChoice1 = new HashMap<String, List<String>>();
maleChoice1.put("male1", Arrays.asList("female1", "female2"));
maleChoice1.put("male2", Arrays.asList("female2", "female1"));
femaleChoice1.put("female1", Arrays.asList("male1", "male2"));
femaleChoice1.put("female2", Arrays.asList("male2", "male1"));
SpeedDateCompute speedDateCompute = new SpeedDateCompute(maleChoice1, femaleChoice1);
List<Pair> actualValues = speedDateCompute.getPairs();
assertEquals("male1", actualValues.get(0).getMale());
assertEquals("female1", actualValues.get(0).getFemale());
assertEquals("male2", actualValues.get(1).getMale());
assertEquals("female2", actualValues.get(1).getFemale());
/*
* Test case 2
*/
Map<String, List<String>> maleChoice2 = new HashMap<String, List<String>>();
Map<String, List<String>> femaleChoice2 = new HashMap<String, List<String>>();
maleChoice2.put("male1", Arrays.asList("female2", "female1"));
maleChoice2.put("male2", Arrays.asList("female1", "female2"));
maleChoice2.put("male3", Arrays.asList("female1", "female2"));
femaleChoice2.put("female1", Arrays.asList("male2", "male1"));
femaleChoice2.put("female2", Arrays.asList("male1", "male2"));
femaleChoice2.put("female3", Arrays.asList("male1", "male2"));
speedDateCompute = new SpeedDateCompute(maleChoice2, femaleChoice2);
actualValues = speedDateCompute.getPairs();
assertEquals("male1", actualValues.get(0).getMale());
assertEquals("female2", actualValues.get(0).getFemale());
assertEquals("male2", actualValues.get(1).getMale());
assertEquals("female1", actualValues.get(1).getFemale());
assertEquals(2, actualValues.size());
}
}