In a post, the following question was posed:
We can select unique values \(\displaystyle\frac{1}{i}\) for \(i=1,\dots,n\). Find all combinations that add up to 1.
A complete enumeration scheme was slow even for \(n=10\). Can we use a MIP model for this or something related?
A single solution is easily found using the model:
Mathematical Model |
---|
\[ \begin{align} & \sum_{i=1}^n \frac{1}{i} \cdot \color{darkred}x_i = 1 \\ & \color{darkred}x_i \in \{0,1\} \end{align}\] |
- Use a Constraint Programming (CP) tool. Most CP solvers have built-in facilities to return more than just a single solution. However, they also typically work with integer data. Few CP solvers allow fractional data. Here, we have fractional data by design. Converting to an integer problem can be done by scaling the constraint.
- Use a no-good constraint. Solve, add a constraint (cut) to the model that forbids the current solution, and repeat until things become infeasible.
- Some MIP solvers have what is called a solution pool which can enumerate solutions. These methods are often very fast.
- \(n=12\). This has three solutions.
- \(n=25\). This gives us 41 solutions.
Constraint programming
n=12 coefficients:[1.0, 0.5, 0.3333333333333333, 0.25, 0.2, 0.16666666666666666, 0.14285714285714285, 0.125, 0.1111111111111111, 0.1, 0.09090909090909091, 0.08333333333333333] number of solutions: 2 ['1/1'] ['1/2', '1/3', '1/6']
n=12, lcm=27720 coefficients:[27720, 13860, 9240, 6930, 5544, 4620, 3960, 3465, 3080, 2772, 2520, 2310] number of solutions: 3 ['1/1'] ['1/2', '1/3', '1/6'] ['1/2', '1/4', '1/6', '1/12']
n=25, lcm=26771144400number of solutions: 41 ['1/1'] ['1/2', '1/3', '1/6'] ['1/2', '1/3', '1/8', '1/24'] ['1/2', '1/3', '1/9', '1/18'] ['1/2', '1/3', '1/10', '1/15'] ['1/2', '1/4', '1/5', '1/20'] ['1/2', '1/4', '1/6', '1/12'] ['1/2', '1/4', '1/8', '1/12', '1/24'] ['1/2', '1/4', '1/9', '1/12', '1/18'] ['1/2', '1/4', '1/10', '1/12', '1/15'] ['1/2', '1/5', '1/6', '1/12', '1/20'] ['1/2', '1/5', '1/8', '1/12', '1/20', '1/24'] ['1/2', '1/5', '1/9', '1/12', '1/18', '1/20'] ['1/2', '1/5', '1/10', '1/12', '1/15', '1/20'] ['1/2', '1/6', '1/8', '1/9', '1/18', '1/24'] ['1/2', '1/6', '1/8', '1/10', '1/15', '1/24'] ['1/2', '1/6', '1/9', '1/10', '1/15', '1/18'] ['1/2', '1/8', '1/9', '1/10', '1/15', '1/18', '1/24'] ['1/3', '1/4', '1/5', '1/6', '1/20'] ['1/3', '1/4', '1/5', '1/8', '1/20', '1/24'] ['1/3', '1/4', '1/5', '1/9', '1/18', '1/20'] ['1/3', '1/4', '1/5', '1/10', '1/15', '1/20'] ['1/3', '1/4', '1/6', '1/8', '1/12', '1/24'] ... ['1/4', '1/5', '1/6', '1/9', '1/10', '1/15', '1/18', '1/20'] ['1/4', '1/5', '1/8', '1/9', '1/10', '1/15', '1/18', '1/20', '1/24'] ['1/4', '1/6', '1/8', '1/9', '1/10', '1/12', '1/15', '1/18', '1/24'] ['1/5', '1/6', '1/8', '1/9', '1/10', '1/12', '1/15', '1/18', '1/20', '1/24']
No-good cuts
- Solve the model
- If infeasible: STOP
- Record the solution
- Add the no-good cut to the model
- Go to step 1
---- 75 PARAMETER trace results for each iteration1/11/21/31/41/61/12 sum coeff 1.0000.5000.3330.2500.1670.083 iter1 1.0001.000 iter2 1.0001.0001.0001.000 iter3 1.0001.0001.0001.0001.000
Cplex solution pool
--- Dumping 42 solutions from the solution pool...
Conclusion
- The constraint solver wants integer data, so I scaled the constraint by the LCM.
- The solve-loop with no-good constraints requires some attention. After scaling the constraint by 1000, I get the correct set of solutions
- The Cplex solution pool required me to change the integer feasibility tolerance.
References
- Python-constraint, https://pypi.org/project/python-constraint/
- Chess and Solution Pool, https://yetanothermathprogrammingconsultant.blogspot.com/2018/11/chess-and-solution-pool.html
Appendix: Python constraint code
import constraint as cp import math n = 25 r = range(1,1+n) lcm = math.lcm(*list(r)) print(f'n={n}, lcm={lcm}') # coefficients p = [lcm // i for i in r] if n <= 15: print(f'coefficients:{p}') problem = cp.Problem() problem.addVariables(r, [0,1]) problem.addConstraint(cp.ExactSumConstraint(lcm,p)) sol = problem.getSolutions() print(f'number of solutions: {len(sol)}') for k in sol: print([f'1/{i}'for i in k.keys() if k[i]==1])
Appendix: GAMS model
$onText
Enumerate feasible solutions 1. using no-good constraints 2. using Cplex solution pool $offText
*---------------------------------------------------------------- * data *----------------------------------------------------------------
set dummy 'for ordering of elements' /'coeff'/ i 'numbers 1/i to use' /'1/1'*'1/25'/ k0 'iterations (superset)' /iter1*iter100/ ;
parameter p(i) 'coefficients'; p(i) = 1/ord(i);
*---------------------------------------------------------------- * dynamic set + parameters to hold cut data *----------------------------------------------------------------
set k(k0) 'iterations performed (and cut data index)'; k(k0) = no;
parameter cutcoeff(k0,*) 'coefficients for cuts'; cutcoeff(k0,i) = 0;
*---------------------------------------------------------------- * model * no-good cuts are added after each solve *----------------------------------------------------------------
binary variable x(i); variable z 'obj'; Equations e 'sum to one' cuts 'no-good cuts' obj 'dummy objective' ;
scalar scale /1000/; e.. sum(i, scale*p(i)*x(i)) =e= scale; cuts(k).. sum(i,cutcoeff(k,i)*x(i)) =l= cutcoeff(k,'rhs'); obj.. z=e=0;
model m /all/; m.solprint = %solprint.Silent%; m.solvelink = %solveLink.Load Library%; option threads = 0;
*---------------------------------------------------------------- * algorithm *---------------------------------------------------------------- parameter trace(*,*) 'results for each iteration';
loop(k0,
solve m minimizing z using mip;
break$(m.modelstat <> 1 and m.modelstat <> 8); x.l(i) = round(x.l(i)); trace(k0,i) = x.l(i); cutcoeff(k0,i) = 2*x.l(i)-1; cutcoeff(k0,'rhs') = sum(i,x.l(i))-1; k(k0) = yes; );
trace('coeff',i)$sum(k,trace(k,i)) = p(i); trace(k,'sum') = sum(i,p(i)*trace(k,i)); display trace;
*---------------------------------------------------------------- * solution pool approach *----------------------------------------------------------------
model m2 /obj,e/; m2.optfile=1; solve m2 minimizing z using mip;
$onecho > cplex.opt * solution pool options solnpoolintensity 4 solnpoolpop 2 populatelim 10000 solnpoolmerge solutions.gdx
* tighten tolerances epint 0 $offecho
* * load the gdx file with all solutions * set id /soln_m2_p1*soln_m2_p1000/; parameter xsol(id,i); execute_load "solutions.gdx",xsol=x; display xsol;
|