При работе с текстом часто возникает потребность корректно расставить переносы. Задача на первый взгляд не такая уж очевидная, нужно учитывать особенности каждого языка, чтобы решить, в каком месте разорвать слово. Как правильно формализовать такие требования, и как потом применить их в алгоритме? Одно из самых распространенных на сей день решений предложил Франклин Марк Лян, студент известного профессора Дональда Кнута. Алгоритм так и называется – «Алгоритм Ляна-Кнута», он применяется в издательской системе TeX, автор которой опять же Д. Кнут.
Алгоритм основан на сравнении исходного слова с набором правил (шаблонов). Чем больше правил и чем качественнее они составлены, тем лучше будут расставляться переносы. В пакете TeX можно найти готовые бесплатные наборы правил для многих языков, нужно только внимательно смотреть на условия использования и распространения.
Каждое правило состоит из букв и цифр между ними, а также цифр в начале и в конце. Цифру 0 обычно опускают. Например, первое правило должно пониматься как 0п0р0и1. Последовательность букв – это часть слова, для которой определяется перенос, т.е. эта последовательность должна присутствовать в слове. Цифры называют «уровнем», они задают приоритет между правилами и возможность переноса в соответствующей позиции. Четные цифры, включая 0, запрещают перенос. Нечетные – разрешают. Точка в начале правила означает, что правило применяется, только если последовательность находится в начале слова. Аналогично с точкой в конце – слово должно заканчиваться этой последовательностью. Если точка есть и в начале и в конце, то правило содержит слово целиком.
Исходное слово: алгоритм
Набор правил (взяты из TeX):
Сопоставим слово со всеми правилами и выберем наибольшие уровни:
В позициях с уровнем 1 можно смело ставить перенос. Получаем результат «ал-го-ритм».
Теперь реализуем этот алгоритм на языке С++. Мне нужен был рабочий алгоритм для использования в iOS, поэтому я сделал все в виде Си-шного интерфейса. Модуль написан без привязки к какой-либо локали или платформе, можно использовать где угодно.
Правило будем хранить так:
Будем преобразовывать каждое правило в вид «чистая последовательность символов» + «набор уровней», чтобы удобно было применять в дальнейшем.
Набор правил:
Код для выдергивания уровней из правил простой, его можно посмотреть в полных исходниках по ссылке в конце статьи.
После того как мы заполним список правил, его нужно отсортировать, чтобы обеспечить правильную и эффективную работу алгоритма. Напишем свою функцию less и применим стандартный алгоритм сортировки:
Теперь непосредственно алгоритм нахождения переносов:
В строку word_string мы помещаем исходное слово, с добавленными символами '.' по краям, чтобы автоматически подбирались правила, содержащие указания о своей позиции в слове. Теперь в исходном слове для каждого символа с i=0 до N перебираем все подстроки начинающиеся с i и длиной от 1 до N-i. Ищем каждую подстроку в векторе правил стандартным алгоритмом std::lower_bound. Исходим из того, что правила отсортированы нужным нам образом и нет необходимости на каждом шаге перебирать все заново. Когда находим совпадение, берем вектор уровней и применяем его к текущему результату, т.е. если уровень для текущей позиции в правиле выше, запоминаем его вместо старого.
В векторе levels скапливаются максимальные значения уровней для каждой позиции. Осталось проверить его на нечетные значения.
Примеры работы алгоритма для набора правил из TeX:
Готовый код на С++ вместе с приведенными примерами можно скачать здесь. Тестовый пример в файле main.c (кодировка Windows-1251), правила в файле patterns.h.
Алгоритм основан на сравнении исходного слова с набором правил (шаблонов). Чем больше правил и чем качественнее они составлены, тем лучше будут расставляться переносы. В пакете TeX можно найти готовые бесплатные наборы правил для многих языков, нужно только внимательно смотреть на условия использования и распространения.
Пример правил:
при1 при3в 2и1ве .по3ж2
Каждое правило состоит из букв и цифр между ними, а также цифр в начале и в конце. Цифру 0 обычно опускают. Например, первое правило должно пониматься как 0п0р0и1. Последовательность букв – это часть слова, для которой определяется перенос, т.е. эта последовательность должна присутствовать в слове. Цифры называют «уровнем», они задают приоритет между правилами и возможность переноса в соответствующей позиции. Четные цифры, включая 0, запрещают перенос. Нечетные – разрешают. Точка в начале правила означает, что правило применяется, только если последовательность находится в начале слова. Аналогично с точкой в конце – слово должно заканчиваться этой последовательностью. Если точка есть и в начале и в конце, то правило содержит слово целиком.
Основные этапы работы алгоритма:
- Выбрать все правила, подходящие к выбранному слову и для каждой позиции в слове получить набор уровней (сколько правил пришлось на одну позицию, столько и уровней получим).
- В каждой позиции выбрать максимальный уровень. Если он четный – здесь переносить нельзя, если нечетный – допустимое место переноса.
- Отсечь очевидно недопустимые переносы (например, одна буква в начале или в конце).
Посмотрим работу алгоритма на примере:
Исходное слово: алгоритм
Набор правил (взяты из TeX):
лго1 1г о1ри и1т и2тм тм2
Сопоставим слово со всеми правилами и выберем наибольшие уровни:
В позициях с уровнем 1 можно смело ставить перенос. Получаем результат «ал-го-ритм».
Реализация
Теперь реализуем этот алгоритм на языке С++. Мне нужен был рабочий алгоритм для использования в iOS, поэтому я сделал все в виде Си-шного интерфейса. Модуль написан без привязки к какой-либо локали или платформе, можно использовать где угодно.
Правило будем хранить так:
struct pattern_t
{
std::basic_string<unichar> str;
std::vector<unsigned char> levels;
};
Будем преобразовывать каждое правило в вид «чистая последовательность символов» + «набор уровней», чтобы удобно было применять в дальнейшем.
Набор правил:
struct pattern_list_t
{
std::vector<pattern_t*> list;
};
Код для выдергивания уровней из правил простой, его можно посмотреть в полных исходниках по ссылке в конце статьи.
После того как мы заполним список правил, его нужно отсортировать, чтобы обеспечить правильную и эффективную работу алгоритма. Напишем свою функцию less и применим стандартный алгоритм сортировки:
bool pattern_compare(const pattern_t* a, const pattern_t* b)
{
bool first = a->str.size() < b->str.size();
size_t min_size = first ? a->str.size() : b->str.size();
for (size_t i = 0; i < min_size; ++i)
{
if (a->str[i] < b->str[i])
return true;
else if (a->str[i] > b->str[i])
return false;
}
return first;
}
void sort_pattern_list(pattern_list_t* pattern_list)
{
if (!pattern_list) return;
std::sort(pattern_list->list.begin(), pattern_list->list.end(), pattern_compare);
}
Теперь непосредственно алгоритм нахождения переносов:
std::vector<unsigned char> levels;
levels.assign(word_string.size(), 0);
for (size_t i = 0; i < word_string.size()-2; ++i)
{
std::vector<pattern_t*>::const_iterator pattern_iter = pattern_list->list.begin();
for (size_t count = 1; count <= word_string.size()-i; ++count)
{
pattern_t pattern_from_word;
pattern_from_word.str = word_string.substr(i, count);
if (pattern_compare(&pattern_from_word, *pattern_iter))
continue;
pattern_iter = std::lower_bound(pattern_iter, pattern_list->list.end(), &pattern_from_word, pattern_compare);
if (pattern_iter == pattern_list->list.end())
break;
if (!pattern_compare(&pattern_from_word, *pattern_iter))
{
for (size_t level_i = 0; level_i < (*pattern_iter)->levels.size(); ++level_i)
{
unsigned char l = (*pattern_iter)->levels[level_i];
if (l > levels[i+level_i])
levels[i+level_i] = l;
}
}
}
}
В строку word_string мы помещаем исходное слово, с добавленными символами '.' по краям, чтобы автоматически подбирались правила, содержащие указания о своей позиции в слове. Теперь в исходном слове для каждого символа с i=0 до N перебираем все подстроки начинающиеся с i и длиной от 1 до N-i. Ищем каждую подстроку в векторе правил стандартным алгоритмом std::lower_bound. Исходим из того, что правила отсортированы нужным нам образом и нет необходимости на каждом шаге перебирать все заново. Когда находим совпадение, берем вектор уровней и применяем его к текущему результату, т.е. если уровень для текущей позиции в правиле выше, запоминаем его вместо старого.
В векторе levels скапливаются максимальные значения уровней для каждой позиции. Осталось проверить его на нечетные значения.
mask_size = levels.size()-2;
mask = new unsigned char[mask_size];
for (size_t i = 0; i < mask_size; ++i)
{
if (levels[i+1] % 2 && i)
mask[i] = 1;
else
mask[i] = 0;
}
Примеры работы алгоритма для набора правил из TeX:
про-грам-мист ки-бер-не-ти-ка вопль ин-ту-и-ци-я до-сто-при-ме-ча-тель-ность при-вет
Готовый код на С++ вместе с приведенными примерами можно скачать здесь. Тестовый пример в файле main.c (кодировка Windows-1251), правила в файле patterns.h.