Начинаем определение класса, как полагается, с команды
Perlpackage Polyomino;
Типовой конструктор принимает кодирующую строку и возвращает ссылку на неё,
выполнив предварительно bless:
Perlsub new($) { my $class=shift; my $string=shift; return bless \$string, $class; }
Определения этих методов настолько банальны, что вряд ли нуждаются в пояснениях:
Perlsub toString() { my $self=shift; return $$self; } sub perimeter() { my $self=shift; return length $$self; }
При программировании метода canonize выберем следующую
стратегию: в переменную $str отправим кодирующую строку. Её
получим как выражение $$self ($self — сам
объект). Такое же значение первоначально получит и переменная
$strMin, в которую в конце концов попадёт канонический
представитель класса эквивалентности кодирующей строки. Затем будем в цикле
подвергать строку преобразованиям, отражающим всевозможные повороты
и всевозможные способы выбора начальной точки контура. Среди этих строк найдём
минимальную (в лексикографическом смысле) и отправим её в переменную
$strMin, которую сразу после выхода из цикла сделаем
кодирующей строкой полимино.
Естественно, нет никакой нужды накапливать в памяти весь список эквивалентных кодирующих строк, чтобы потом выбрать из них минимальную. Будем искать минимальную строку индуктивно, по мере появления новых строк из класса эквивалентности.
А получать эти строки мы будем в двойном цикле. Внешний цикл будет перебирать разные способы выбора начальной точки контура, переставляя один символ из начала кодирующей строки в её конец. Можно использовать для этой цели регулярное выражение и оператор замены:
Perl$str=~s/^(.)(.*)$/$2$1/;
Или то же самое сделать при помощи substr:
Perl$str=substr($str, 1).substr($str, 0, 1);
(мы подозреваем, что второй способ будет работать быстрее).
Во внутреннем цикле кодирующая строка будет подвергаться поворотам на против часовой стрелки, то есть
тотальной замене символов R, U,
L, D на U,
L, D, R
соответственно. Для таких операций над строками идеален оператор
tr///:
Perl$str=~tr/RULD/ULDR/;
Получившаяся строка $str сравнивается со строкой
$strMin, и, если окажется меньше, становится на место
последней.
В итоге получаем следующий код:
Perlsub canonize() { my $self=shift; my $str=my $strMin=$$self; for(my $k=0; $k<$self->perimeter; $k++) { $str=~s/^(.)(.*)$/$2$1/; for(my $j=0; $j<4; $j++) { $str=~tr/RULD/ULDR/; $strMin=$str if $strMin gt $str; } } $$self=$strMin; }
Параметр $n конструктора offshut
содержит номер стороны фигуры. Первым делом создаётся
новый объект Polyomino с той же
кодирующей строкой, что и данный объект (для которого вызывался метод
offshut). Новый объект отправляется в переменную
$polyomino.
Затем извлекается $n-й символ из кодирующей строки этого
объекта и заменяется на соответствующую трёхсимвольную строку. Проще всего
использовать готовую таблицу соответствий в анонимном ассоциативном массиве:
тогда символ $c заменяется на {R=>'DRU', L=>'ULD', U=>'RUL',
D=>'LDR'}->{$c}.
Вспомним, что появление отростка не всегда проходит гладко. Во-первых, на новой
фигуре могут появиться разрезы глубиной один или два. Их лечение,
соответственно, проводится в один или два этапа по одной и той же схеме:
устранение двухсимвольных сочетаний RL,
UD, DU, LR
в кодирующей строке (здесь мы представляем кодирующую строку свёрнутой
в кольцо). Но строка, увы, не свёрнута в кольцо, и парные, подлежащие
сокращению символы могут оказаться в начале и в конце строки; не забудем про
этот особый случай. Так что будем устранять эти неприятности в цикле, пока
хотя бы одна из них встречается:
Perlwhile ( $$polyomino=~s/RL|UD|DU|LR//g or $$polyomino=~s/^R(.*)L$/$1/ or $$polyomino=~s/^L(.*)R$/$1/ or $$polyomino=~s/^U(.*)D$/$1/ or $$polyomino=~s/^D(.*)U$/$1/ ) {}
Тело цикла пустое, поскольку устранение неприятностей происходит одновременно
с их обнаружением: каждое из выражений $$polyomino=~s/…/…/ возвращает истинное значение
в случае обнаружения и устранения разреза того или иного сорта. Обратите
внимание на модификатор g в самом первом операторе замены: он
вынуждает удалять все встретившиеся в кодирующей строке буквосочетания,
подлежащие сокращению. Это нужно для того, чтобы быстро и за один присест
уничтожить оба разреза первого сорта, а их при добавлении
отростка может оказаться больше одного (но не больше двух). Без модификатора
второй разрез будет вылечен при следующем проходе цикла while,
но строку придётся просматривать лишний раз, сопоставляя с регулярным
выражением.
После лечения разрезов нас может подстерегать ещё одна беда: наложение отростка
на другие клетки полимино, или касание с другими клетками — потеря
односвязности. Эта опасность грозит фигурам, которые содержат или больше клеток, и поэтому могут
охватить клетку, не принадлежащую фигуре. Признаком всех этих осложнений
является наличие самопересечений контура, то есть наличие хотя бы двух
совпадающих вершин. Этот изъян не лечится; такая ситуация означает, что на
$n-й стороне отросток построить невозможно (или возможно, но
полученная фигура окажется бракованной, непригодной для дальнейшей работы).
Проверку простоты контура (отсутствия самопересечений) мы поручаем методу
isSimple, о котором речь пойдёт ниже. Брак после
проверки уничтожается, а если контроль качества пройден успешно, новая фигура
канонизируется и возвращается из конструктора.
Всё сказанное отражено в определении конструктора offshut:
Perlsub offshut($) { my $self=shift; my $n=shift; my $polyomino=__PACKAGE__->new($$self); my $c=substr($$polyomino, $n, 1); substr($$polyomino, $n, 1, {R=>'DRU', L=>'ULD', U=>'RUL', D=>'LDR'}->{$c}); while ( $$polyomino=~s/RL|UD|DU|LR//g or $$polyomino=~s/^R(.*)L$/$1/ or $$polyomino=~s/^L(.*)R$/$1/ or $$polyomino=~s/^U(.*)D$/$1/ or $$polyomino=~s/^D(.*)U$/$1/ ) {} undef $polyomino unless $polyomino->isSimple; $polyomino->canonize if defined $polyomino; return $polyomino; }
Извлечение части контура, начинающуюся с заданной вершины и имеющую заданную
длину, мы возлагаем на метод subpath, а проверку
всевозможных частей — на метод isSimple. При
обнаружении первой же петли последний возвращает ложное значение, а поиск
других петель больше не производится. Если все отрезки контура перебраны,
а петель среди них не обнаружено, метод isSimple
возвращает истинное значение.
Итак, метод subpath принимает два параметра — номер
вершины ($k), с которой начинается отрезок контура, и длину
отрезка $l. В типичном случае соответствующий этому отрезку
кусок кодирующей строки возвращается выражением substr($$self, $k, $l) (если извлекаемый отрезок не
содержит начальную точку контура). Если же содержит, потребуются особые
манипуляции. Приведём без пояснений код, в котором учтено данное
обстоятельство; укажем лишь, что типичному случаю соответствует условие $k+$l<=$self->perimeter:
Perlsub subpath($$) { my $self=shift; my $k=shift; my $l=shift; return substr($$self, $k, $l) if $k+$l<=$self->perimeter; my $h=$self->perimeter-$k; return substr($$self, $k, $h).substr($$self, 0, $l-$h); }
Конечно же предполагается, что $l меньше периметра фигуры.
Уже после опубликования этой версии метода subpath наш
ученик Александр Волков предложил гораздо более изящное
(и более эффективное) решение. Александр заметил, что если даже $k+$l и больше периметра фигуры, то не больше удвоенного
периметра. Таким образом проблема выхода за пределы кодирующей строки при
вызове substr($$self, $k, $l) полностью снимается,
если «удвоить» кодирующую строку: substr($$self x 2, $k,
$l). В итоге отпадает потребность и в условном операторе, и в вызове
метода perimeter, да и в переменных
$k и $l, которые теперь использовались
лишь однократно.
Perlsub subpath($$) { my $self=shift; return substr($$self x 2, shift, shift); }
Это маленькое улучшение ускоряет нашу программу при на 15 %, а при — на 21 %. Очень неплохо.
Теперь нам осталось перебирать все отрезки контура (с началами во всевозможных вершинах и всевозможными допустимыми длинами) и проверять, не является ли хотя бы один из них петлёй.
В соответствии с соображениями, высказанными в разделе «Утрата односвязности», организуем перебор участков границы, которые могут претендовать на роль петель. Подсчёт символов проводим при помощи оператора tr///. Первая же найденная петля делает ненужным дальнейший перебор.
Perlsub isSimple() { my $self=shift; my $perimeter=$self->perimeter; for(my $k=1; $k<$perimeter; $k++) { for(my $l=4; $l<=$perimeter-12; $l+=2) { my $subpath=$self->subpath($k, $l); return 0 if ($subpath=~tr/R//)==($subpath=~tr/L//) and ($subpath=~tr/U//)==($subpath=~tr/D//) } } return 1; }
Теперь, когда метод subpath стал совсем простым,
необходимость в нём отпадает, а ту единственную строчку, где он вызывается,
Perlmy $subpath=$self->subpath($k, $l);
мы заменим на
Perlmy $subpath=substr($$self x 2, $k, $l);
Тем самым мы дополнительно ускорим программу, поскольку вызов метода и передача
параметров при вызове — операция, требующая времени. С учётом этого
быстродействие программы по сравнению с первоначальной версией выросло при
на 29 %, а при
—
на 40 %. Очевидно, метод subpath был слабым местом
в программе.
Метод boundingBox будет играть вспомогательную роль.
Он будет вызываться в методах toSVG
и toASCII. Алгоритм вычисления координат левого
нижнего ($llx и $lly) и правого верхнего
($urx и $ury) углов прямоугольника,
ограничивающего фигуру, очень прост. В цикле перебираются символы кодирующей
строки и вычисляются координаты $x и $y
очередной вершины. Наименьшее и наибольшее значения абсциссы
$x отправятся соответственно в переменные
$llx и $urx, а наименьшее и наибольшее
значения ординаты $y — в переменные $lly
и $ury. Найденные четыре значения возвращаются в массиве.
Perlsub boundingBox() { my $self=shift; my ($llx, $lly, $urx, $ury)=(0, 0, 0, 0); my ($x, $y)=(0, 0); for(split //, $$self) { if($_ eq 'R') { $x++; } elsif($_ eq 'U') { $y++; } elsif($_ eq 'L') { $x--; } elsif($_ eq 'D') { $y--; } $llx=$x if $llx>$x; $lly=$y if $lly>$y; $urx=$x if $urx<$x; $ury=$y if $ury<$y; } return ($llx, $lly, $urx, $ury); }
Метод toSVG возвращает строку, содержащую документ
SVG с изображением фигуры полимино. Документ содержит
элемент svg:svg — контейнер для рисунка, внутри
которого размещается единственный элемент svg:path.
Содержательная часть кода предназначена для вычисления значений нужных
атрибутов этих двух элементов:
Perlsub toSVG() { my $self=shift; вычислить значения атрибутов return <<__SVG__; <svg:svg version="1.1" viewBox="@viewBox" width="$width" height="$height" > <svg:path d="M 0 0 ${path}z"/> </svg:svg> __SVG__ }
В элементе svg:svg атрибут version принимает значение 1.1 (это версия языка SVG). Атрибут
viewBox должен содержать четвёрку чисел,
разделённых пробелами. Первые два из них — координаты левого нижнего угла
прямоугольника, ограничивающего рисунок (элементы изображения могут выходить за
пределы прямоугольника, но показано будет только то, что попало
в прямоугольник). Два оставшихся числа — это ширина и высота ограничивающего
прямоугольника.
Атрибуты width и height задают «внешние» размеры рисунка, то есть
размеры того прямоугольника, на который будет отображён прямоугольник viewBox. Размеры (и даже пропорции) этих двух
прямоугольников не обязаны совпадать. В случае несовпадения размеров
прямоугольник viewBox может растягиваться или
сжиматься по горизонтали и вертикали до нужной ширины и высоты, либо
с сохранением пропорций (тогда при необходимости остаются поля), либо без
сохранения (тогда изображение сжимается или растягивается). Этим поведением
можно управлять при помощи дополнительных атрибутов, однако нам это не
понадобится: мы укажем в точности ту же ширину и высоту, что и у viewBox.
Размеры viewBox легко вычисляются через значения,
возвращаемые методом boundingBox. Однако
boundingBox возвращает размеры в клетках фигуры,
а требуются размеры в пикселах. Мы выбрали для клетки фигуры приемлемый размер
пикселов. Поскольку фигуры на рисунке будут очерчены чёрной линией, потребуется
дополнительные поля вокруг фигуры (мы выбрали половину размера клетки).
С учётом сказанного, если $llx, $lly,
$urx, $ury — числа, возвращённые при
вызове boundingBox, то ширина и высота viewBox даются выражениями $urx-$llx+1 и $ury-$lly+1
(добавочная единица возникла из-за полей). Абсцисса левого нижнего угла
получается как $llx-.5. Что касается ординаты, мы
используем не $ury-.5, а -$ury-.5. Это связано с тем, что
в SVG применяются декартовы координаты с осью ординат,
направленной вниз. После того как значения @viewBox
вычислены в клетках, умножим их на
— размер клетки в пикселах.
Perlmy ($llx, $lly, $urx, $ury)=$self->boundingBox; my @viewBox=($llx-.5, -$ury-.5, $urx-$llx+1, $ury-$lly+1); my $cellSize=36; $_*=$cellSize for @viewBox; my ($width, $height)=@viewBox[2, 3];
Дальше вычисляется значение атрибута d элемента
svg:path. Это значение всегда имеет вид M 0 0 … z: путь начинается из начала координат
и оканчивается замыканием. На место многоточия можно было бы вставить команды
сдвигов по горизонтали h 36 и h -36 для букв R
и L кодирующей строки и команды сдвигов по вертикали v -36 и v 36 для
U и D (вверх в наших координатах — это
вниз в координатах SVG). Но такое решение было бы следствием
исключительного малодушия. Если в кодирующей строке идут подряд несколько
одинаковых букв, скажем, …RRRR…, то получится … h 36 h 36 h 36 h 36 …, хотя можно было бы написать
короче: … h 144 …. Примем во внимание, что язык
SVG не отличается лаконичностью, и нельзя пренебрегать
никакой возможностью сократить размер документа.
Напрашивается такое решение: разбить строку на фрагменты, состоящие из
одинаковых букв. В зависимости от буквы формируется команда сдвига:
h для буквы R, h - для
L, v - для U
и v для D. После команды и возможного
минуса записывается размер клетки, умноженный на длину фрагмента.
И как же найти в строке фрагменты, состоящие из одинаковых букв? Конечно, с помощью регулярных выражений:
Perlwhile($$self=~m/((.)\2*)/g) { $path.={R=>'h ', U=>'v -', L=>'h -', D=>'v '}->{$2} .($cellSize*length $1).' '; }
При вычислении условия цикла $$self=~m/((.)\2*)/g
во вторую (внутреннюю) группу захвата попадает символ. Первая, внешняя группа
должна захватить, помимо этого символа, ещё ноль или больше таких же символов.
Содержимое второй группы, $2, позволяет выбрать команду
сдвига, если использовать его как ключ в анонимном ассоциативном массиве.
Длина строки $1, умноженная на размер клетки, дает величину
сдвига.
Первая часть тела метода toASCII посвящена заполнению
двумерного массива @intersections. В этом массиве -й элемент — список абсцисс вертикальных отрезков на контуре полимино,
лежащих в -й полосе. Список
размещается в элементе массива @intersections как ссылка на
анонимный массив. Система координат
выбрана из-за того, что координаты клеток фигуры в ней всегда неотрицательны,
что, как правило, неверно для координат
.
Отрицательные координаты невозможно использовать как индексы в массиве.
Значения добавляются в массив @intersections в порядке
обхода вершин контура. Обход начинается с начальной точки контура
.
Итак, начальные координаты сначала берутся как
-координаты
левого нижнего угла ограничивающего прямоугольника (первые два значения из
списка, возвращаемого при вызове boundingBox), а затем
у них меняется знак:
Perlsub toASCII() { my $self=shift; my ($i, $j)=$self->boundingBox; ($i, $j)=(-$i, -$j); …
Затем в цикле перебираются символы кодирующей строки. Для горизонтальных
участков (R или L) просто увеличивается
или уменьшается абсцисса $i, а для вертикальных
(U или D) увеличивается или уменьшается
ордината $j, и, кроме того, значение текущей абсциссы
добавляется в список абсцисс из $j-й полосы:
Perlfor my $c(split //, $$self) { if($c eq 'R') { $i++; } elsif($c eq 'U') { push @{$intersections[$j++]}, $i; } elsif($c eq 'L') { $i--; } elsif($c eq 'D') { push @{$intersections[--$j]}, $i; } } …
Обратите внимание, на то, что при движении по контуру вверх абсцисса
добавляется в список до того, как увеличится ордината ($j++). При движении вниз сначала уменьшается ордината
(--$j), и только после этого происходит добавление
абсциссы. Объяснение содержится на рисунке 42.2. «Преобразование контурного описания полимино в клеточное». Там видно, что стрелки, отвечающие
вертикальным сторонам фигуры и попавшие в -ю полосу, начинаются на нижнем краю полосы
(отмечен пунктиром) при движении вверх, и заканчиваются там при движении вниз.
Поскольку нас интересуют пересечения вертикальных сторон именно с нижним краем
полосы, при движении вниз сначала выполняется сдвиг, и уж затем добавляется
абсцисса в -й список для нового,
уменьшенного на единицу, значения .
Вторая и заключительная часть метода формирует в строке
$string изображение фигуры. Клетки фигуры изображаются как
блочные символы ▒, пустоты как пробелы. Каждый ряд
завершается символом конца строки, так что если вывести такую переменную на
экран с новой строки, получится изображение фигуры.
Поскольку ряды фигуры нумеруются снизу вверх, а выводиться должны, по замыслу, сверху вниз, нам потребуется организовать циклический перебор рядов в обратном порядке:
Perl… my $string=''; for(reverse @intersections) { … } return $string; }
Цикл посвящён построению значения переменной $string.
В теле цикла список абсцисс, доступный в массиве @$_,
сортируется в арифметическом порядке, и попадает в массив
@row. Затем следует преобразовать ряд в строку
$rowString, состоящую из блоков и пробелов. Эта строка будет
добавлена к переменной $string вместе с символом конца
строки, завершающим ряд:
Perlmy @row=sort { $a<=>$b } @$_; my $rowString=''; while(@row) { my ($start, $stop)=splice @row, 0, 2; $rowString.=(' ' x ($start-length $rowString)); $rowString.=('▒' x ($stop-$start)); } $string.="$rowString\n";
Во внутреннем цикле пара абсцисс $start,
$stop извлекается из отсортированного по возрастанию массива
@row строка $rowString сначала
дополняется пробелами до длины $start, а затем дополняется
блочными символами в количестве $stop-$start. По
завершении цикла (когда @row опустеет) строка
$rowString вместе с символом конца строки добавляется, как
и было обещано, к строке $string.