Однажды на проводимом мной практическом занятии [по ЯП] я, скучая, разглядывал список студентов группы. Глаз зацепился за знак ударения в фамилии Лемзекóв, который я поставил [для себя] после того, как произнёс фамилию этого студента неправильно. Я мысленно прочёл эту фамилию по слогам, и тут у меня возник вопрос: «а по какому алгоритму мозг разбивает слова по слогам?» Почему-то интуитивно получается "Лем-зе-ков", а не "Ле-мзе-ков" или "Лем-зек-ов". Я выписал ещё несколько примеров, и разглядывая их размышлял о том, как перевести это в алгоритм.

Алгоритм получился такой (привожу практически тот самый Python-код, который я написал карандашом тогда на занятии).
slog_start = 0
i = 0
while i < len(word):
    if word[i] in vowels_set:
        vowel_pos = i
        i += 1
        while i < len(word):
            if word[i] in vowels_set:
                if i - vowel_pos == 1:
                    hyphens.append(i)
                elif i - vowel_pos == 2:
                    hyphens.append(i - 1)
                else:
                    hyphens.append(vowel_pos + 2)

На этом месте меня осенило: достаточно просто знать расстояние между соседними гласными буквами (пусть они будут в позициях a и b) — если оно равно 1, тогда вставляем перенос в позиции b, если равно 2, то в позиции b − 1, иначе [т.е. когда расстояние больше 2] в позиции a + 2.

Получается такой Python-код:
word = input()

vowels_set = set('аеёиоуыэюяАЕЁИОУЫЭЮЯ')
vowels = []
for i in range(len(word)):
    if word[i] in vowels_set:
        vowels.append(i)

import collections
hyphens = collections.deque()
for i in range(1, len(vowels)):
    a, b = vowels[i-1], vowels[i]
    if b - a == 1:
        hyphens.append(b)
    elif b - a == 2:
        hyphens.append(b - 1)
    else:
        hyphens.append(a + 2)

for i in range(len(word)):
    if len(hyphens) and hyphens[0] == i:
        print('-', end = '')
        hyphens.popleft()
    print(word[i], end = '')
Можно оптимизировать данный код, избавившись от вспомогательного массива `vowels`:
word = input()

vowels_set = set('аеёиоуыэюяАЕЁИОУЫЭЮЯ')
prev_vowel = len(word)
for i in range(len(word)):
    if word[i] in vowels_set:
        prev_vowel = i
        break

import collections
hyphens = collections.deque()
for i in range(prev_vowel + 1, len(word)):
    if word[i] in vowels_set:
        a, b = prev_vowel, i
        if b - a == 1:
            hyphens.append(b)
        elif b - a == 2:
            hyphens.append(b - 1)
        else:
            hyphens.append(a + 2)
        prev_vowel = i

for i in range(len(word)):
    if len(hyphens) and hyphens[0] == i:
        print('-', end = '')
        hyphens.popleft()
    print(word[i], end = '')

Осталось только добавить поддержку букв «й», «ь» и «ъ».
Для этого нужно слегка модифицировать цепочку условий начинающуюся с if b - a == 1::
for i ...:
    ...
    if b - a == 1:
        hyphens.append(b)
    else:
        for j in reversed(range(a + 1, b)):
            if word[j] in specials_set: # specials_set = set('йьъЙЬЪ')
                hyphens.append(j + 1)
                break
        else:
            if b - a == 2:
                hyphens.append(b - 1)
            else:
                hyphens.append(a + 2)

Ещё одна оптимизация (отказ от `hyphens`), и вот что получается в итоге:
word = input()

vowels_set = set('аеёиоуыэюяАЕЁИОУЫЭЮЯ')
specials_set = set('йьъЙЬЪ')

prev_vowel = len(word)
for i in range(len(word)):
    if word[i] in vowels_set:
        prev_vowel = i
        break

pos = 0
for i in range(prev_vowel + 1, len(word)):
    if word[i] in vowels_set:
        a, b = prev_vowel, i
        if b - a == 1:
            print(word[pos:b], end = '-')
            pos = b
        else:
            for j in reversed(range(a + 1, b)):
                if word[j] in specials_set:
                    print(word[pos:j + 1], end = '-')
                    pos = j + 1
                    break
            else:
                if b - a == 2:
                    print(word[pos:b - 1], end = '-')
                    pos = b - 1
                else:
                    print(word[pos:a + 2], end = '-')
                    pos = a + 2
        prev_vowel = i
print(word[pos:])

В заключение отвечу на возможный вопрос «и к чему всё это?», ведь есть же алгоритм П. Христова в модификации Дымченко и Варсанофьева, который, к тому же, применяется не только для русского, но и для английского языка. Ну, во-первых, по факту для английского он не подходит из-за особенностей этого языка. Во-вторых, некоторые правила в нём довольно сомнительны, например правило "гсс-ссг" приводит к неверной разбивке слова "отстранять". Ну и в-третьих, предложенный мной алгоритм существенно быстрее.

P. S. Кстати, буду признателен если кто даст ссылочку на оригинальный алгоритм П. Христова, т.к. интересно какие именно модификации сделали Дымченко и Варсанофьев.

P. P. S. Поиском по списку всех русских слов обнаружилось не очень большое количество слов с 5-ю подряд идущими согласными буквами {например: агентство, ангстрем, бодрствование, интеллигентство}. В таких случаях (т.е. когда расстояние между соседними гласными буквами равно 6) следует вставлять перенос в позиции a + 3 или [что то же самое] b − 3.
Также можно объединить случаи, когда расстояние между гласными равно 1 или 2: в обоих этих случаях перенос вставляется в позиции a + 1.
Итоговый код выглядит так:
vowels_set = set('аеёиоуыэюяАЕЁИОУЫЭЮЯ')
specials_set = set('йьъЙЬЪ')

word = input()

prev_vowel = len(word)
for i in range(len(word)):
    if word[i] in vowels_set:
        prev_vowel = i
        break

pos = 0
for i in range(prev_vowel + 1, len(word)):
    if word[i] in vowels_set:
        a, b = prev_vowel, i
        for j in reversed(range(a + 1, b)):
            if word[j] in specials_set:
                npos = j + 1
                break
        else:
            if b - a <= 2:
                npos = a + 1
            elif b - a >= 6:
                npos = b - 3
            else:
                npos = a + 2
        print(word[pos:npos], end = '-')
        pos = npos
        prev_vowel = i
print(word[pos:])

Комментарии (27)


  1. apachik
    31.05.2023 21:49
    +4

    печально, что не выложили тесты (а есть ли они вообще?)
    без них "итоговый код" не выглядит "полным"


    1. Terimoun
      31.05.2023 21:49
      +1

      Полностью согласен, итоговый код не помешал бы


  1. Paulus
    31.05.2023 21:49

    В прошлом веке, когда python ещё не было, что что ТеХ, что Word обходились мягкими переносами. Проще некуда и все языки поддерживались ;)


  1. Wesha
    31.05.2023 21:49
    +4

    Если я что-то в этой жизни усвоил, так это то, что "у любой задачи есть простое, элегантное, неправильное решение" (c)

    Примеры, на которых Ваш код работает неверно: агентство, контрстрахование


    1. alextretyak Автор
      31.05.2023 21:49
      -2

      Почему неверно?
      У меня разделяет так: а-гент-ство, контр-стра-хо-ва-ни-е.
      Вы точно использовали последнюю версию кода (которая в самом конце статьи под P. P. S. внутри спойлера)?


      1. Wesha
        31.05.2023 21:49

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


      1. domix32
        31.05.2023 21:49

        опредеённо ему не очень нравится много согласных


        1. Wesha
          31.05.2023 21:49

          Я это сразу понял и именно поэтому искал такие слова, где их много (агентство, контрвзбзднуть)


    1. kryvichh
      31.05.2023 21:49

      Контргайка, законтрить?


      1. Wesha
        31.05.2023 21:49

        "Последняя версия кода" выдаёт: кон-тргай-ка, за-кон-трить, кон-тра-ген-тский

        Кстати, с хохмами тоже не очень

        контрвзб-зднуть


    1. osmanpasha
      31.05.2023 21:49

      И есть ещё слова, где небольшое число согласных между гласными (1), но граница слогов проходит по границе частей слова, а не после гласной: под-одеяльник, пред-усилитель


      1. vesper-bot
        31.05.2023 21:49
        +1

        Опа, а это для меня новость. Откуда инфа?


        1. osmanpasha
          31.05.2023 21:49
          +1

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

          Процитирую http://new.gramota.ru/spravka/buro/search-answer?s=249871

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

          Тем не менее наиболее распространены в современной русистике две теории слога. Они связаны с именами двух выдающихся советских языковедов – Р. И. Аванесова (Московская фонологическая школа) и Л. В. Щербы (Ленинградская фонологическая школа). Правила деления на слоги в этих двух теориях несколько различаются (так, слово кошка следует делить на слоги согласно теории Аванесова так: ко-шка, согласно теории Щербы так: кош-ка). Поэтому в разных учебниках правила слогоделения могут быть сформулированы по-разному, в зависимости от того, позицию какой фонологической школы разделяет автор учебника.

          А вот правила переноса более строго кодифицированы, и в текущей редакции предпочтительно "под-одеяльник", но допуситмо и "по-додеяльник". Ранее правильным был только первый вариант.


  1. vesper-bot
    31.05.2023 21:49

    По правилам русского языка, запомненным мной во втором классе, точка разделения слогов находится перед слиянием (согласная+гласная) следующего слога, или между двумя гласными, если слияния в следующем слоге нет. Всё.


    1. Hardcoin
      31.05.2023 21:49

      Во втором классе не проходят слово "агентство". Оно по этому правилу поделится неверно, но для второклассника не страшно.


      1. vesper-bot
        31.05.2023 21:49

        Кстати, "агентство" делится на слоги как раз с длинным хвостом из согласных, "а-гентст-во". Мы с учителем заспорили по поводу слова "уползла", у которого тоже длинный (для тогдашних примеров, две против максимум одной) хвост во втором слоге, и я спор успешно выиграл, она делила как "у-пол-зла".


        1. Hardcoin
          31.05.2023 21:49

          А есть подтверждение, кроме мнения? Я полагаю, что делится а·ге́нт-ство. Ссылка есть выше в комментариях.


          1. Wesha
            31.05.2023 21:49

            Я полагаю, что делится а·ге́нт-ство.

            Воистину так.


      1. Wesha
        31.05.2023 21:49

        Во втором классе не проходят слово "агентство".

        Это зависит от того, где мама работает!


    1. alextretyak Автор
      31.05.2023 21:49

      точка разделения слогов находится перед слиянием (согласная+гласная) следующего слога

      Тогда получится отст-ра-нять.
      И такое правило не учитывает присутствие в слове твердого или мягкого знаков, а также буквы «й» (подъезд, бульон, майор).


      1. vesper-bot
        31.05.2023 21:49
        +1

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

        Апд: про Й был неправ, а вот про всё остальное, похоже, сами правила сменились, причем кардинально, но если честно, что в старой, что в новой системе правил даже на втором уровне есть вариативность, что мягко скажем нелогично для процесса, который априори должен быть детерминирован.


        1. alextretyak Автор
          31.05.2023 21:49

          То есть, в этом случае разделение на слоги будет перед гласной.

          Перед гласной, говорите? А как же: дяденька, большой, скользкий?

          правильно как раз будет ма-йор, а не май-ор.

          А здесь утверждается наоборот.


          1. vesper-bot
            31.05.2023 21:49

            А как же: дяденька, большой, скользкий?

            Слияние есть - фиг с мягким знаком, по нему и делим. Вот если нет, там грабли. Но в соседней ветке напомнили контрпример (кстати), по которому приставки запихиваются в один слог (пред-усилитель), и я склонен с этим согласиться, слова с приставками следует рассматривать так, как если бы граница слога уже стояла по разделу между приставкой и корнем (или приставками, в словах с несколькими приставками). То есть тот же "подъезд" прекрасно делится по приставке, "подъ-езд", и заморочек с обходом твердого знака не требуется. Вопрос тогда в слове "контракт", его как делить, "контр-акт" или "конт-ракт", или вообще "кон-тракт"?


            1. Wesha
              31.05.2023 21:49

              дяденька, большой, скользкий?

              ...одна штука!

              (Простите, погорячился не удержался.)


          1. alcanoid
            31.05.2023 21:49
            +1

            Это правила переноса, и они не полностью совпадают с разбиением по слогам.


          1. osmanpasha
            31.05.2023 21:49
            +1

            По ссылке, кстати, правила переноса слов, а не слогоделения. Судя по всему, именно слогоделение пользователям языка не так важно, интересно только с академической точки зрения и до сих пор является дискуссионной темой.


  1. emptyTuple
    31.05.2023 21:49

    Также неправильно работает со словом “взбзднуть”