Skip to content
Snippets Groups Projects
Commit ebffca83 authored by Paul A. Rubin's avatar Paul A. Rubin
Browse files

Initial commit.

parent 1f8d0e14
No related branches found
No related tags found
No related merge requests found
package setgame;
import ilog.concert.IloAnd;
import ilog.concert.IloConstraint;
import ilog.concert.IloException;
import ilog.concert.IloIntVar;
import ilog.cp.IloCP;
/**
* CP implements a constraint programming model to find all set.
* @author Paul A. Rubin (rubin@msu.edu)
*/
public final class CP implements AutoCloseable {
private final IloCP model; // the CP model
private final IloIntVar[] cards; // the index of the card in each slot
private final IloIntVar[][] values; // the value of each feature (row) in
// each slot (column)
/**
* Constructs the CP model.
* @param game the game to solve
* @throws IloException if CPO objects to something
*/
public CP(final Set game) throws IloException {
model = new IloCP();
cards = new IloIntVar[Set.SETSIZE];
values = new IloIntVar[Set.NFEATURES][Set.SETSIZE];
// Define the variables.
for (int slot = 0; slot < Set.SETSIZE; slot++) {
cards[slot] = model.intVar(0, Set.NCARDS - 1, "card_in_slot_" + slot);
for (int feature = 0; feature < Set.NFEATURES; feature++) {
values[feature][slot] =
model.intVar(0, Set.NVALUES, "slot_" + slot + "_feature_" + feature);
}
}
// Constraint: No card can be used twice.
model.add(model.allDiff(cards));
// Constraint: To avoid finding all possible permutations of all possible
// hands, the slots must be filled in ascending card order.
for (int slot = 1; slot < Set.SETSIZE; slot++) {
model.add(model.lt(cards[slot - 1], cards[slot]));
}
// Constraint: The value of a feature in a slot is inherited from the
// card in that slot.
for (int feature = 0; feature < Set.NFEATURES; feature++) {
int[] v = game.getValues(feature);
for (int slot = 0; slot < Set.SETSIZE; slot++) {
model.add(model.eq(values[feature][slot],
model.element(v, cards[slot])));
}
}
// Constraint: For each feature, either all slots have the same value or
// all slots are different.
for (int feature = 0; feature < Set.NFEATURES; feature++) {
IloConstraint different = model.allDiff(values[feature]);
IloAnd same = model.and();
for (int slot = 0; slot < Set.SETSIZE - 1; slot++) {
same.add(model.eq(values[feature][slot], values[feature][slot + 1]));
}
model.add(model.or(different, same));
}
// Suppress output.
model.setOut(null);
}
/**
* Solves the model and returns the number of solutions found.
* @return the solution count
* @throws IloException if CPO pitches a fit
*/
public int solve() throws IloException {
int found = 0;
model.startNewSearch();
while (model.next()) {
found += 1;
}
return found;
}
/**
* Closes the model.
*/
@Override
public void close() {
model.end();
}
}
package setgame;
import ilog.concert.IloException;
import ilog.concert.IloLinearNumExpr;
import ilog.concert.IloNumVar;
import ilog.concert.IloRange;
import ilog.cplex.IloCplex;
import java.util.HashSet;
/**
* IP implements an integer programming model to find all sets.
* @author Paul A. Rubin (rubin@msu.edu)
*/
public final class IP implements AutoCloseable {
private static final double HALF = 0.5; // used for rounding
private final IloCplex model; // the MIP model
private final IloNumVar[] include; // indicator to include a card
private final IloNumVar[][] appears; // appears[i][j] = 1 if value j for
// feature i appears in the set
private final IloNumVar[] different; // 1 if feature values all differ,
// 0 if they are all the same
private final HashSet<HashSet<Integer>> found; // solutions found
/**
* Constructor.
* @param game the game to solve
* @throws IloException if CPLEX fails to build the model
*/
public IP(final Set game) throws IloException {
model = new IloCplex();
found = new HashSet<>();
// Define the variables.
include = new IloNumVar[Set.NCARDS];
appears = new IloNumVar[Set.NFEATURES][Set.NVALUES];
different = new IloNumVar[Set.NFEATURES];
for (int c = 0; c < Set.NCARDS; c++) {
include[c] = model.boolVar("include_" + c);
}
for (int f = 0; f < Set.NFEATURES; f++) {
different[f] = model.boolVar("all_different_" + f);
for (int v = 0; v < Set.NVALUES; v++) {
appears[f][v] = model.boolVar("feature_" + f + "_value_" + v);
}
}
// We use the trivial objective (minimize 0).
// Constraint: the set must have the correct size.
model.addEq(model.sum(include), Set.SETSIZE);
// Constraint: the number of values for any feature appearing in the set
// must be either 1 (all the same) or the set size (all different).
for (int f = 0; f < Set.NFEATURES; f++) {
model.addEq(model.sum(appears[f]),
model.sum(1.0, model.prod(2.0, different[f])));
}
// Constraint: a value of a feature appears in the set if and only if
// any card with that value is selected.
for (int f = 0; f < Set.NFEATURES; f++) {
for (int v = 0; v < Set.NVALUES; v++) {
IloLinearNumExpr expr = model.linearNumExpr();
for (int c : game.findCards(f, v)) {
model.addGe(appears[f][v], include[c]);
expr.addTerm(1.0, include[c]);
}
model.addLe(appears[f][v], expr);
}
}
// Suppress output.
model.setOut(null);
// Avoid warm starts (which do not work).
model.setParam(IloCplex.Param.Advance, 0);
}
/**
* Solves the model.
* @return true if the model is feasible (hence automatically optimal)
* @throws IloException if CPLEX blows up
*/
public boolean solve() throws IloException {
// Allow parallel threads.
model.setParam(IloCplex.Param.Threads, 0);
model.solve();
if (model.getStatus() == IloCplex.Status.Optimal) {
// A solution was found. Recover it.
HashSet<Integer> set = toSet(model.getValues(include));
// Add a constraint to rule the solution out.
model.add(exclude(set));
return true;
} else {
return false;
}
}
/**
* Converts the values of "include" in a solution to a card set.
* @param x the solution
* @return the corresponding set
*/
private HashSet<Integer> toSet(final double[] x) {
HashSet<Integer> s = new HashSet<>();
for (int c = 0; c < Set.NCARDS; c++) {
if (x[c] > HALF) {
s.add(c);
}
}
return s;
}
/**
* Solves a single model using a callback to exclude previous solutions.
* Note: To avoid having to synchronize methods (and test whether multiple
* threads found the same solution) we throttle the solver to a single
* thread.
* @return the number of solutions found.
* @throws IloException if CPLEX blows up
*/
public int solve2() throws IloException {
// Clear the solution accumulator.
found.clear();
// Attach a callback to reject solutions.
model.use(new Callback(), IloCplex.Callback.Context.Id.Candidate);
// Force single threading.
model.setParam(IloCplex.Param.Threads, 1);
// Solve the model (stopping due to infeasibility).
model.solve();
// Return the solution count.
return found.size();
}
/**
* Creates a constraint to exclude an identified set.
* @param set the set to exclude
* @return a range constraint that excludes the set
* @throws IloException if CPLEX gets snippy
*/
private IloRange exclude(final HashSet<Integer> set) throws IloException {
IloLinearNumExpr expr = model.linearNumExpr();
for (int c : set) {
expr.addTerm(1.0, include[c]);
}
return model.le(expr, Set.SETSIZE - 1);
}
/**
* Closes the model.
*/
@Override
public void close() {
model.close();
}
/**
* Callback implements a generic callback to add no good constraints whenever
* a set is found.
*/
private final class Callback implements IloCplex.Callback.Function {
/**
* Constructor (which does nothing).
*/
Callback() { };
/**
* Rejects the current candidate solution using a no-good constraint.
* @param context the context information
* @throws IloException if CPLEX gets snippy
*/
@Override
public void invoke(final IloCplex.Callback.Context context)
throws IloException {
// Get the candidate set.
double[] x = context.getCandidatePoint(include);
// Convert it to a set.
HashSet<Integer> s = toSet(x);
// Add the set to the pool of solutions.
found.add(s);
// Turn the set into a range constraint.
IloRange r = exclude(s);
// Reject the solution.
context.rejectCandidate(r);
}
}
}
package setgame;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
/**
* Set is the container for a game instance.
* @author Paul A. Rubin (rubin@msu.edu)
*/
public final class Set {
/** NCARDS is the number of cards in the game. */
public static final int NCARDS = 81;
/** NFEATURES is the number of features on a card. */
public static final int NFEATURES = 4;
/** NVALUES is the number of values a feature can have. */
public static final int NVALUES = 3;
/** SETSIZE is the number of cards in a set. */
public static final int SETSIZE = 3;
private final int[][] cards; // the card deck
private final HashMap<Integer, HashMap<Integer, HashSet<Integer>>>
hasFeature;
private int found; // number of valid sets found
/**
* Constructor.
*/
public Set() {
cards = new int[NCARDS][NFEATURES];
int[] divisors = new int[NFEATURES + 1];
divisors[0] = 1;
for (int i = 1; i <= NFEATURES; i++) {
divisors[i] = NVALUES * divisors[i - 1];
}
// Fill in the cards.
for (int c = 0; c < NCARDS; c++) {
cards[c][0] = c % divisors[1];
for (int f = 1; f < NFEATURES; f++) {
cards[c][f] = (c % divisors[f + 1]) / divisors[f];
}
}
// Fill in the feature possession map.
hasFeature = new HashMap<>();
for (int f = 0; f < NFEATURES; f++) {
HashMap<Integer, HashSet<Integer>> m = new HashMap<>();
for (int v = 0; v < NVALUES; v++) {
HashSet<Integer> s = new HashSet<>();
for (int c = 0; c < NCARDS; c++) {
if (cards[c][f] == v) {
s.add(c);
}
}
m.put(v, s);
}
hasFeature.put(f, m);
}
}
/**
* Gets the set of cards with a particular value of a particular feature.
* @param feature the feature
* @param value the target value
* @return the set of cards with that value
*/
public Collection<Integer> findCards(final int feature, final int value) {
return new HashSet<>(hasFeature.get(feature).get(value));
}
/**
* Prints a set.
* @param set the set to print
*/
public void printSet(final Collection<Integer> set) {
ArrayList<Integer> list = new ArrayList<>(set);
Collections.sort(list);
for (int c : list) {
System.out.println(Arrays.toString(cards[c]) + " (" + c + ")");
}
}
/**
* Computes all valid sets by brute force (using recursion).
* @return the number of sets found
*/
public int findAllSets() {
found = 0;
HashMap<Integer, HashSet<Integer>> map = new HashMap<>();
for (int f = 0; f < NFEATURES; f++) {
map.put(f, new HashSet<>());
}
enumerate(1, 0, map);
return found;
}
/**
* Recursively enumerates all valid sets.
* @param level the level (1 ... SETSIZE) at which the function is called
* @param start the initial card index
* @param values the accumulated values of all features
*/
private void enumerate(final int level, final int start,
final HashMap<Integer, HashSet<Integer>> values) {
// Check for vacuous cases (level or start index too high).
if (level > SETSIZE || start == NCARDS) {
return;
}
for (int c = start; c < NCARDS; c++) {
// Clone the values map, adding the value of each feature in card c.
HashMap<Integer, HashSet<Integer>> v = new HashMap<>();
for (int f = 0; f < NFEATURES; f++) {
HashSet<Integer> s = new HashSet<>(values.get(f));
s.add(cards[c][f]);
v.put(f, s);
}
if (level == SETSIZE) {
// Final level: check for a valid set and if valid count it.
boolean valid = true;
for (int f = 0; f < NFEATURES; f++) {
if (v.get(f).size() == 2) {
valid = false;
break;
}
}
if (valid) {
found += 1;
}
} else {
// Call enumerate at the next level.
enumerate(level + 1, c + 1, v);
}
}
}
/**
* Gets the value of a particular feature for every card.
* @param feature the feature
* @return the vector of feature values
*/
public int[] getValues(final int feature) {
int[] v = new int[NCARDS];
for (int c = 0; c < NCARDS; c++) {
v[c] = cards[c][feature];
}
return v;
}
}
package setgame;
import ilog.concert.IloException;
/**
* SetGame attempts to find all possible "sets" from the game Set.
*
* This addresses a question on OR Stack Exchange:
* https://or.stackexchange.com/questions/10820/how-to-generate-feasible
* -sets-with-a-mip
*
* @author Paul A. Rubin (rubin@msu.edu)
*/
public final class SetGame {
/** Dummy constructor. */
private SetGame() { }
/**
* Solves for all possible sets in the game.
* @param args the command line arguments (ignored)
*/
@SuppressWarnings({ "checkstyle:magicnumber" })
public static void main(final String[] args) {
// Generate the card deck.
Set deck = new Set();
// Try brute force.
long clock = System.currentTimeMillis();
int count = deck.findAllSets();
clock = System.currentTimeMillis() - clock;
System.out.println("Brute force found " + count + " sets in "
+ (0.001 * clock) + " seconds.");
// Try the IP model using a callback.
System.out.println("Trying the IP model with callback ...");
clock = System.currentTimeMillis();
try (IP ip = new IP(deck)) {
int found = ip.solve2();
clock = System.currentTimeMillis() - clock;
System.out.println("Number of sets found = " + found);
System.out.println("Time required = " + (0.001 * clock) + " seconds.");
} catch (IloException ex) {
System.out.println(ex.getMessage());
}
// Try the basic IP model.
System.out.println("Trying the basic IP model ...");
clock = System.currentTimeMillis();
try (IP ip = new IP(deck)) {
int found = 0;
while (ip.solve()) {
found += 1;
}
clock = System.currentTimeMillis() - clock;
System.out.println("Number of sets found = " + found);
System.out.println("Time required = " + (0.001 * clock) + " seconds.");
} catch (IloException ex) {
System.out.println(ex.getMessage());
}
// Try the CP model.
System.out.println("Trying the CP model ...");
clock = System.currentTimeMillis();
try (CP cp = new CP(deck)) {
int found = cp.solve();
clock = System.currentTimeMillis() - clock;
System.out.println("Number of sets found = " + found);
System.out.println("Time required = " + (0.001 * clock) + " seconds.");
} catch (IloException ex) {
System.out.println(ex.getMessage());
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment