Как решить ряд ограничений в Perl? - PullRequest
9 голосов
/ 22 февраля 2009

У меня есть следующий набор ограничений в Perl (просто примерный набор ограничений, а не те, которые мне действительно нужны):

$a < $b
$b > $c
$a is odd => $a in [10..18]
$a > 0
$c < 30

И мне нужно найти список ($a, $b, $c), который соответствует ограничениям. Мое наивное решение -

sub check_constraint {
    my ($a, $b, $c) = @_;
    if !($a < $b) {return 0;}
    if !($b > $c) {return 0;}
    if (($a % 2) && !(10 <= $a && $a <= 18)) {return 0;}
    if !($a > 0) {return 0;}
    if !($c < 30) {return 0;}
    return 1;
}

sub gen_abc {
    my $c = int rand 30;
    my $b = int rand $c;
    my $a = int rand $b;
    return ($a, $b, $c);
}

($a, $b, $c) = &gen_abc();
while (!&check_constraint($a, $b, $c)) {
    ($a, $b, $c) = &gen_abc();
}

Теперь это решение не гарантированно закончится и в целом довольно неэффективно. Есть ли лучший способ сделать это в Perl?

Редактировать: Мне это нужно для генератора случайных тестов, поэтому в решении должны использоваться случайные функции, такие как rand(). Полностью детерминированного решения недостаточно, хотя, если это решение может дать мне список возможных комбинаций, я могу выбрать индекс случайным образом:

@solutions = &find_allowed_combinations(); # solutions is an array of array references
$index = int rand($#solutions);
($a, $b, $c) = @$solution[$index];

Редактировать 2: Ограничения здесь легко решить с помощью грубой силы. Однако, если имеется много переменных с большим диапазоном возможных значений, грубая сила не подходит.

Ответы [ 6 ]

14 голосов
/ 23 февраля 2009

Основная проблема в этой задаче оптимизации имеет математический характер.

Ваша цель, как я понимаю из вашего определения метода gen_abc, состоит в том, чтобы сократить пространство поиска, найдя ограничивающие интервалы для ваших различных переменных ($a, $b и т. Д.)

Лучшая стратегия состоит в том, чтобы извлечь как можно больше линейных ограничений из вашего полного набора ограничений, попытаться вывести границы (используя линейное программирование методы, см. Ниже), а затем перейти к исчерпывающие (или недетерминированные) тесты проб и ошибок по сокращенному пространству переменных.

Типичная задача линейного программирования имеет вид:

minimize (maximize) <something>
subject to <constraints>

Например, для трех переменных a, b и c и следующих линейных ограничений:

<<linear_constraints>>::
  $a < $b
  $b > $c
  $a > 0
  $c < 30

Вы можете найти верхнюю и нижнюю границы для $a, $b и $c следующим образом:

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>>

В Perl для этой цели вы можете использовать Math :: LP .


Пример

Линейное ограничение имеет вид "C eqop C1×$V1 ± C2×$V2 ± C3×$V3 ...", где

  • eqop является одним из <, >, ==, >=, <=
  • $V1, $V2 и т. Д. Являются переменными, а
  • C, C1, C2 и т. Д. Являются константами, возможно равными 0.

Например, учитывая ...

$a < $b
$b > $c
$a > 0
$c < 30

... переместите все переменные (с их коэффициентами) влево от неравенства, а константы-одиночки справа от неравенства:

$a - $b       <  0
     $b - $c  >  0
$a            >  0
          $c  < 30

... и настройте ограничения таким образом, чтобы использовались только =, <= и >= (in) равенства (при условии дискретных, то есть целочисленных значений для наших переменных):

  • '...
  • '...> C' становится '...> = C + 1'

... то есть

$a - $b       <= -1
     $b - $c  >=  1
$a            >=  1
          $c  <= 29

... тогда напишите что-то вроде этого:

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

Конечно, вышеприведенное можно обобщить на любое количество переменных V1, V2, ... (например, $a, $b, $c, $d, ...), с любыми коэффициентами C1, C2, ... (например, -1, 1, 0, 123 и т. д.) и любыми постоянными значениями C (например, -1, 1, 30, 29 и т. д.), предоставленными Вы можете проанализировать выражения ограничения в соответствующем матричном представлении, таком как:

   V1  V2  V3     C
[ C11 C12 C13 <=> C1 ]
[ C21 C22 C23 <=> C2 ]
[ C31 C32 C33 <=> C3 ]
... ... ... ... ... ...

Применительно к предоставленному вами примеру,

  $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

Примечание

В качестве примечания: при выполнении недетерминированных (на основе rand) тестов может быть хорошей идеей отслеживать (например, в хэше), какие кортежи ($a,$b,$c) уже были протестированы. , чтобы избежать их повторного тестирования, тогда и только тогда, когда :

  • тестируемый метод дороже, чем поиск по хешу (это не относится к примеру кода, который вы предоставили выше, но может быть или не быть проблемой с вашим реальным кодом)
  • хеш не увеличится до огромных размеров (либо все переменные связаны конечными интервалами, произведение которых является разумным числом - в этом случае проверка размера хеша может указать, полностью ли вы исследовали все пространство или нет), или Вы можете периодически очищать хеш, чтобы, по крайней мере, в течение одного промежутка времени вы могли обнаруживать столкновения.)
    • в конечном счете, если вы считаете, что вышесказанное может относиться к вам, вы можете рассчитать различные варианты реализации (с хешем и без него) и посмотреть, стоит ли его реализовывать.
2 голосов
/ 23 февраля 2009

Я использую Данные :: Ограничение . Вы пишете небольшие подпрограммы, которые реализуют отдельные ограничения, затем последовательно применяете все необходимые ограничения. Я немного об этом говорю в Мастеринг Perl в главе «Динамические подпрограммы».

use Data::Constraint;

Data::Constraint->add_constraint(
    'a_less_than_b',
    run         => sub { $_[1] <  $_[2] },
    description => "a < b",
    );

Data::Constraint->add_constraint(
    'b_greater_than_c',
    run         => sub { $_[2] >  $_[3] },
    description => "b > c",
    );

Data::Constraint->add_constraint(
    'a_greater_than_0',
    run         => sub { $_[1] > 0 },
    description => "a > 0",
    );

Data::Constraint->add_constraint(
    'c_less_than_30',
    run         => sub { $_[3] < 30 },
    description => "c < 30",
    );

Data::Constraint->add_constraint(
    'a_is_odd_between_10_18',
    run         => sub { 
        return 1 if( $_[1] < 10 or $_[1] > 18);
        return 0 unless $_[1] % 2,
        },
    description => "a is odd between 10 and 18",
    );

for ( 1 .. 10 )
    {
    my( $a, $b, $c ) = gen_abc(); 
    print "a = $a | b = $b | c = $c\n";

    foreach my $name ( Data::Constraint->get_all_names )
        {
        print "\tFailed $name\n"
            unless Data::Constraint->get_by_name( $name )->check( $a, $b, $c ),
        }
    }

sub gen_abc {
    my $c = int rand 30;
    my $b = int rand $c;
    my $a = int rand $b;
    return ($a, $b, $c);
}

Выполнение этого означает, что легко проверить результат, чтобы увидеть, что не удалось вместо общего отказа:

a = 2 | b = 4 | c = 5
    Failed a_less_than_b
    Failed b_greater_than_c
a = 0 | b = 0 | c = 2
    Failed a_greater_than_0
    Failed a_less_than_b
    Failed b_greater_than_c
a = 0 | b = 0 | c = 2
    Failed a_greater_than_0
    Failed a_less_than_b
    Failed b_greater_than_c
a = 7 | b = 14 | c = 25
    Failed a_less_than_b
    Failed b_greater_than_c
a = 0 | b = 0 | c = 29
    Failed a_greater_than_0
    Failed a_less_than_b
    Failed b_greater_than_c
a = 0 | b = 0 | c = 20
    Failed a_greater_than_0
    Failed a_less_than_b
    Failed b_greater_than_c
a = 0 | b = 4 | c = 22
    Failed a_greater_than_0
    Failed a_less_than_b
    Failed b_greater_than_c
a = 4 | b = 16 | c = 28
    Failed a_less_than_b
    Failed b_greater_than_c
a = 0 | b = 22 | c = 26
    Failed a_greater_than_0
    Failed a_less_than_b
    Failed b_greater_than_c
a = 0 | b = 3 | c = 6
    Failed a_greater_than_0
    Failed a_less_than_b
    Failed b_greater_than_c

Если вы хотите что-то более хардкорное, мой модуль Brick обрабатывает деревья ограничений, включая сокращение и ветвление. Эти вещи имеют смысл для больших систем, где вы будете смешивать и сопоставлять различные ограничения для разных ситуаций, поскольку большая часть кода настраивает объекты ограничений. Если у вас есть только одна ситуация, вы, вероятно, просто хотите придерживаться того, что имеете.

Удачи,:)

1 голос
/ 22 февраля 2009

Я не уверен, что вы найдете простой ответ на этот вопрос (хотя я бы хотел оказаться неправым!).

Похоже, что ваша проблема хорошо подходит для генетического алгоритма . Фитнес Функция должна быть легко написана, просто наберите 1 балл за каждое выполненное ограничение, иначе 0. AI :: Genetic кажется модулем, который может помочь вам как написать код, так и понять, что вам нужно написать.

Это должно быть быстрее, чем метод грубой силы.

1 голос
/ 22 февраля 2009

«Реальный» ответ потребует анализа выражений и рассуждений об отношениях. Если не считать этого, я бы предложил использовать систематический обход пространства значений, а не просто пробовать значения наугад. Например,

my $count = 0;
for (my $c = 0; $c < 30 && $count < $SOMELIMIT; ++$c) {
    # check all other constraints on only $c here
    # next if any fail
    for (my $b = $c + 1; $b < $UPPERLIMIT && $count < $SOMELIMIT; ++$b) {
        # check all other constraints on only $b and $c here
        # next if any fail
        for (my $a = 1; $a < $b && $count < $SOMELIMIT; ++$a) {
            #check all remaining constraints on $a, $b, and $c here
            # next if any fail
            # now use surviving combinations
            ++$count;
        }
    }
}

Я бы поместил переменную с наиболее индивидуальными ограничениями на самый внешний уровень, затем на следующий, наиболее ограниченный, и т. Д.

По крайней мере, с этой структурой вы не будете тестировать одну и ту же комбинацию несколько раз (как это может сделать случайная версия), и если вы посмотрите, как она работает, вы можете увидеть появление шаблонов, позволяющих сократить выполнение.

0 голосов
/ 22 февраля 2009

Я бы вместо этого создал алгоритм, который генерирует набор действительных списков, генерируемых случайным образом или нет (это должно быть тривиально), записывает их в файл, а затем использует этот файл для подачи в тестовую программу, чтобы он мог произвольно выбирать любой список, который он хочет.

0 голосов
/ 22 февраля 2009

кажется, что Simo :: Constrain - это то, что вы хотите

...