The main challenge in this optimization problem is mathematical in nature.
Your goal, as I can infer from your definition of the gen_abc
method, is to prune your search space by finding bounding intervals for your various variables ($a
, $b
etc.)
The best strategy is to extract as many linear constraints from your full set of constraints, attempt to infer the bounds (using linear programming techniques, see below), then proceed with exhaustive (or non-deterministic) trial-and-error tests against a pruned variable space.
A typical linear programming problem is of the form:
minimize (maximize) <something>
subject to <constraints>
For example, given three variables, a
, b
and c
, and the following linear constraints:
<<linear_constraints>>::
$a < $b
$b > $c
$a > 0
$c < 30
You can find upper and lower bounds for $a
, $b
and $c
as follows:
lower_bound_$a = minimize $a subject to <<linear_constraints>>
upper_bound_$a = maximize $a subject to <<linear_constraints>>
lower_bound_$b = minimize $b subject to <<linear_constraints>>
upper_bound_$b = maximize $b subject to <<linear_constraints>>
lower_bound_$c = minimize $c subject to <<linear_constraints>>
upper_bound_$c = maximize $c subject to <<linear_constraints>>
In Perl you may employ Math::LP to this purpose.
EXAMPLE
A linear constraint is of the form "C eqop C1×$V1 ± C2×$V2 ± C3×$V3 ...
", where
eqop
is one of <
, >
, ==
, >=
, <=
$V1
, $V2
etc. are variables, and
C
, C1
, C2
etc. are constants, possibly equal to 0.
For example, given...
$a < $b
$b > $c
$a > 0
$c < 30
...move all variables (with their coefficients) to the left of the inequality, and the lone constants to the right of the inequality:
$a - $b < 0
$b - $c > 0
$a > 0
$c < 30
...and adjust the constraints so that only =
, <=
and >=
(in)equalities are used (assuming discrete i.e. integer values for our variables):
- '... < C' becomes '... <= C-1'
- '... > C' becomes '... >= C+1'
...that is,
$a - $b <= -1
$b - $c >= 1
$a >= 1
$c <= 29
...then write something like this:
use Math::LP qw(:types); # imports optimization types
use Math::LP::Constraint qw(:types); # imports constraint types
my $lp = new Math::LP;
my $a = new Math::LP::Variable(name => 'a');
my $b = new Math::LP::Variable(name => 'b');
my $c = new Math::LP::Variable(name => 'c');
my $constr1 = new Math::LP::Constraint(
lhs => make Math::LP::LinearCombination($a, 1, $b, -1), # 1*$a -1*$b
rhs => -1,
type => $LE,
);
$lp->add_constraint($constr1);
my $constr2 = new Math::LP::Constraint(
lhs => make Math::LP::LinearCombination($b, 1, $c, -1), # 1*$b -1*$c
rhs => 1,
type => $GE,
);
$lp->add_constraint($constr2);
...
my $obj_fn_a = make Math::LP::LinearCombination($a,1);
my $min_a = $lp->minimize_for($obj_fn_a);
my $max_a = $lp->maximize_for($obj_fn_a);
my $obj_fn_b = make Math::LP::LinearCombination($b,1);
my $min_b = $lp->minimize_for($obj_fn_b);
my $max_b = $lp->maximize_for($obj_fn_b);
...
# do exhaustive search over ranges for $a, $b, $c
Of course, the above can be generalized to any number of variables V1
, V2
, ... (e.g. $a
, $b
, $c
, $d
, ...), with any coefficients C1
, C2
, ... (e.g. -1, 1, 0, 123, etc.) and any constant values C
(e.g. -1, 1, 30, 29, etc.) provided you can parse the constraint expressions into a corresponding matrix representation such as:
V1 V2 V3 C
[ C11 C12 C13 <=> C1 ]
[ C21 C22 C23 <=> C2 ]
[ C31 C32 C33 <=> C3 ]
... ... ... ... ... ...
Applying to the example you have provided,
$a $b $c C
[ 1 -1 0 <= -1 ] <= plug this into a Constraint + LinearCombination
[ 0 1 -1 >= 1 ] <= plug this into a Constraint + LinearCombination
[ 1 0 0 >= 1 ] <= plug this into a Constraint + LinearCombination
[ 0 0 1 <= 29 ] <= plug this into a Constraint + LinearCombination
NOTE
As a side note, if performing non-deterministic (rand
-based) tests, it may or may not be a good idea to keep track (e.g. in a hash) of which ($a,$b,$c)
tuples have already been tested, as to avoid testing them again, if and only if:
- the method being tested is more expensive than a hash lookup (this is not the case with the sample code you provided above, but may or may not be an issue with your real code)
- the hash will not grow to enormous proportions (either all variables are bound by finite intervals, whose product is a reasonable number - in which case checking the hash size can indicate whether you have completely explored the entire space or not -, or you can clear the hash periodically so at least for one time interval at a time you do have some collision detection.)
- ultimately, if you think that the above could apply to you, you can time various implementation options (with and without hash) and see whether it is worth implementing or not.