Как найти лексикографически наименьшую строку, перевернув подстроку?

У меня есть строка S, состоящая из a и b. Выполните описанную ниже операцию один раз. Цель состоит в том, чтобы получить лексикографически наименьшую строку.

Операция: инвертировать ровно одну подстроку S

e.g.

  1. если S = abab, то Output = aabb (обратное ba строки S)
  2. если S = abba, то Output = aabb (обратное bba строки S)

Мой подход

Случай 1: если все символы входной строки совпадают, то на выходе будет сама строка.

Случай 2: если S имеет вид aaaaaaa....bbbbbb...., тогда ответом будет само S.

иначе: найдите первое вхождение b в S, скажем, позиция i. Строка S будет выглядеть так

aa...bbb...aaaa...bbbb....aaaa....bbbb....aaaaa...
     |
     i   

Чтобы получить лексикографически наименьшую строку, подстрока, которая будет инвертирована, начинается с индекса i. См. ниже возможные концовки j.

aa...bbb...aaaa...bbbb....aaaa....bbbb....aaaaa...
     |           |               |               |
     i           j               j               j

Переверните подстроку S[i:j] для каждого j и найдите наименьшую строку. Сложность алгоритма будет O(|S|*|S|), где |S| — длина строки.

Есть ли лучший способ решить эту проблему? Возможно O(|S|) решение.

Что я думаю, если мы сможем выбрать правильное j за линейное время, тогда мы закончим. Мы выберем тот j, где количество a максимально. Если максимум один то мы решили задачу, а если нет? Я много пробовал. Пожалуйста помоги.


person cryptomanic    schedule 15.09.2017    source источник
comment
Разве ответ не всегда состоит в том, чтобы сначала писать все «а», а затем все «б»?   -  person Abdennacer Lachiheb    schedule 15.09.2017
comment
@AbdenaceurLichiheb вы можете выполнить операцию ровно один раз. Необязательно, чтобы вы всегда могли привести все а в начале и все б в конце. например S = ababba output=aabbab   -  person cryptomanic    schedule 15.09.2017
comment
да, я знаю, представьте, что ответ строки abababa - aaaabbb, вам нужно только подсчитать количество «a» и «b», а затем написать все «a», а затем все «b».   -  person Abdennacer Lachiheb    schedule 15.09.2017
comment
Я не думаю, что существует линейный алгоритм, хотя должен быть линейно-логарифмический алгоритм.   -  person Jared Goguen    schedule 15.09.2017
comment
@AbdenaceurLichiheb: Какую подстроку вы перевернули, чтобы перейти от abababa к aaaabbb?   -  person Mark Peters    schedule 15.09.2017
comment
@JaredGoguen Я думаю, что нашел алгоритм O (n). (По крайней мере, для строк разумной длины; для более длинных строк потребуются BigInteger.)   -  person m69 ''snarky and unwelcoming''    schedule 16.09.2017
comment
@m69 Я видел, красиво   -  person Jared Goguen    schedule 16.09.2017
comment
@JaredGoguen Спасибо; Я не знал, что голос был за тобой :-)   -  person m69 ''snarky and unwelcoming''    schedule 16.09.2017
comment
Откуда эта проблема? Я почти уверен, что у меня есть решение O (n), не ограниченное двоичными строками, но я хотел бы узнать больше о контексте.   -  person rici    schedule 19.09.2017
comment
@rici мое любопытство убивает меня   -  person גלעד ברקן    schedule 19.09.2017
comment
@גלעדברקן: Я понимаю, но у меня все еще есть сомнения относительно моего решения. Хотя он проходит все тесты, которые я мог придумать, моя попытка доказать его правильность выявила возможность ошибки, из-за которой он может быть O (n log n) вместо O (n). Однако я не могу вызвать ошибку. В любом случае, черновик ответа, который я написал прошлой ночью, потерялся, когда мой ноутбук сломался, поэтому его придется подождать. Извиняюсь.   -  person rici    schedule 20.09.2017
comment
@rici Я тоже с нетерпением жду этого. Кстати, мне это кажется самодостаточным упражнением по кодированию; Я не думаю, что есть какой-то реальный контекст.   -  person m69 ''snarky and unwelcoming''    schedule 21.09.2017


Ответы (2)


Итак, я придумал алгоритм, который кажется более эффективным, чем O(|S|^2), но я не совсем уверен в его сложности. Вот примерный план:

  1. Полоса ведущего a's, хранящаяся в переменной start.
  2. Сгруппируйте оставшуюся часть строки в фрагменты букв.
  3. Найдите индексы групп с самыми длинными последовательностями a's.
  4. Если остался только один index, перейдите к 10.
  5. Отфильтруйте эти индексы так, чтобы длина [первой] группы b's после разворота была минимальной.
  6. Если остался только один index, перейдите к 10.
  7. Отфильтруйте эти индексы так, чтобы длина [первой] группы a's (не включая ведущую a's) после разворота была минимальной.
  8. Если остался только один index, перейдите к 10.
  9. Вернитесь к пункту 5, но на этот раз осмотрите [вторую/третью/...] группы a's и b's.
  10. Возвратите start плюс перевернутые группы до index плюс оставшиеся группы.

Поскольку любая инвертируемая подстрока начинается с b и заканчивается на a, никакие два гипотетических реверсирования не являются палиндромами, и, следовательно, два реверсирования не приведут к одному и тому же результату, гарантируя, что существует единственное оптимальное решение и что алгоритм будет прекращено.

Моя интуиция подсказывает, что этот подход, вероятно, O(log(|S|)*|S|), но я не слишком уверен. Пример реализации (хотя и не очень хороший) на Python приведен ниже.

from itertools import groupby

def get_next_bs(i, groups, off):
    d = 1 + 2*off
    before_bs = len(groups[i-d]) if i >= d else 0
    after_bs = len(groups[i+d]) if i <= d and len(groups) > i + d else 0
    return before_bs + after_bs

def get_next_as(i, groups, off):
    d = 2*(off + 1)
    return len(groups[d+1]) if i < d else len(groups[i-d])

def maximal_reversal(s):
    # example input: 'aabaababbaababbaabbbaa'

    first_b = s.find('b')
    start, rest = s[:first_b], s[first_b:] 
    # 'aa', 'baababbaababbaabbbaa'

    groups = [''.join(g) for _, g in groupby(rest)]
    # ['b', 'aa', 'b', 'a', 'bb', 'aa', 'b', 'a', 'bb', 'aa', 'bbb', 'aa']

    try:
        max_length = max(len(g) for g in groups if g[0] == 'a')
    except ValueError:
        return s # no a's after the start, no reversal needed

    indices = [i for i, g in enumerate(groups) if g[0] == 'a' and len(g) == max_length]
    # [1, 5, 9, 11]

    off = 0
    while len(indices) > 1:
        min_bs = min(get_next_bs(i, groups, off) for i in indices)
        indices = [i for i in indices if get_next_bs(i, groups, off) == min_bs]
        # off 0: [1, 5, 9], off 1: [5, 9], off 2: [9]

        if len(indices) == 1:
            break

        max_as = max(get_next_as(i, groups, off) for i in indices)
        indices = [i for i in indices if get_next_as(i, groups, off) == max_as]
        # off 0: [1, 5, 9], off 1: [5, 9]

        off += 1

    i = indices[0]
    groups[:i+1] = groups[:i+1][::-1]

    return start + ''.join(groups)
    # 'aaaabbabaabbabaabbbbaa'
person Jared Goguen    schedule 15.09.2017
comment
Что делать, если есть несколько вхождений такой подстроки? Какой выбрать? - person cryptomanic; 15.09.2017
comment
@cryptomanic Пропустил этот момент, я написал новый алгоритм, решающий эту проблему. - person Jared Goguen; 15.09.2017

TL;DR: вот алгоритм, который перебирает строку только один раз (с O(|S|)-ой сложностью для строк ограниченной длины). Пример, который я объясню ниже, немного многословен, но алгоритм действительно довольно прост:

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

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

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

function reverseSubsequence(str) {
    var reverse = 0, max = 0, first, last, value, len = 0, unit = 1;
    
    for (var pos = 0; pos < str.length; pos++) {
        var digit = str.charCodeAt(pos) - 97;                   // read next digit
        if (digit == 0) {
            if (first == undefined) continue;                   // skip leading zeros
            if (++len > max || len == max && reverse < value) { // better endpoint found
                max = len;
                last = pos;
                value = reverse;
            }
        } else {
            if (first == undefined) first = pos;                // end of leading zeros
            len = 0;
        }
        reverse += unit * digit;                                // update reverse value
        unit <<= 1;
        value = value * 2 + digit;                              // update endpoint value
    }
    return {from: first || 0, to: last || 0};
}
var result = reverseSubsequence("aaabbaabaaabbabaaabaaab");
document.write(result.from + "&rarr;" + result.to);

(Код можно упростить, сравнивая reverse и value всякий раз, когда встречается ноль, а не только когда встречается конец максимально длинной последовательности нулей.)


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

Рассмотрим эту строку в качестве примера:

aaabbaabaaabbabaaabaaab

или, записанный с нулями и единицами для простоты:

00011001000110100010001   

Мы перебираем ведущие нули, пока не найдем первый:

0001
   ^

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

reverse = 1, unit = 1  

Затем на каждом шаге мы удваиваем единицу и обновляем обратное число:

0001        reverse = 1
00011       unit = 2; reverse = 1 + 1 * 2 = 3
000110      unit = 4; reverse = 3 + 0 * 4 = 3
0001100     unit = 8; reverse = 3 + 0 * 8 = 3

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

endpoint = {position = 6, value = 3} 

Затем мы продолжаем перебирать строку, но на каждом этапе мы обновляем значение возможной конечной точки, но уже как обычное (от старшего до младшего бита) двоичное число:

00011001      unit = 16; reverse = 3 + 1 * 16 = 19
              endpoint.value *= 2 + 1 = 7
000110010     unit = 32; reverse = 19 + 0 * 32 = 19
              endpoint.value *= 2 + 0 = 14
0001100100    unit = 64; reverse = 19 + 0 * 64 = 19
              endpoint.value *= 2 + 0 = 28
00011001000   unit = 128; reverse = 19 + 0 * 128 = 19
              endpoint.value *= 2 + 0 = 56

В этот момент мы обнаруживаем, что у нас есть последовательность из 3 нулей, которая длиннее, чем текущий максимум из 2, поэтому мы отбрасываем конечную точку, которая у нас была до сих пор, и заменяем ее текущей позицией и обратным значением:

endpoint = {position = 10, value = 19}  

И затем мы продолжаем перебирать строку:

000110010001         unit = 256; reverse = 19 + 1 * 256 = 275
                     endpoint.value *= 2 + 1 = 39
0001100100011        unit = 512; reverse = 275 + 1 * 512 = 778
                     endpoint.value *= 2 + 1 = 79
00011001000110       unit = 1024; reverse = 778 + 0 * 1024 = 778
                     endpoint.value *= 2 + 0 = 158
000110010001101      unit = 2048; reverse = 778 + 1 * 2048 = 2826
                     endpoint.value *= 2 + 1 = 317
0001100100011010     unit = 4096; reverse = 2826 + 0 * 4096 = 2826
                     endpoint.value *= 2 + 0 = 634
00011001000110100    unit = 8192; reverse = 2826 + 0 * 8192 = 2826
                     endpoint.value *= 2 + 0 = 1268
000110010001101000   unit = 16384; reverse = 2826 + 0 * 16384 = 2826
                     endpoint.value *= 2 + 0 = 2536

Здесь мы обнаруживаем, что у нас есть еще одна последовательность с 3 нулями, поэтому мы сравниваем текущее обратное значение со значением конечной точки и обнаруживаем, что сохраненная конечная точка имеет меньшее значение:

endpoint.value = 2536  < reverse = 2826  

поэтому мы сохраняем конечную точку в позиции 10 и продолжаем перебирать строку:

0001100100011010001      unit = 32768; reverse = 2826 + 1 * 32768 = 35594
                         endpoint.value *= 2 + 1 = 5073  
00011001000110100010     unit = 65536; reverse = 35594 + 0 * 65536 = 35594
                         endpoint.value *= 2 + 0 = 10146
000110010001101000100    unit = 131072; reverse = 35594 + 0 * 131072 = 35594
                         endpoint.value *= 2 + 0 = 20292
0001100100011010001000   unit = 262144; reverse = 35594 + 0 * 262144 = 35594
                         endpoint.value *= 2 + 0 = 40584

И мы находим еще одну последовательность из 3 нулей, поэтому мы сравниваем эту позицию с сохраненной конечной точкой:

endpoint.value = 40584 > reverse = 35594  

и мы обнаруживаем, что оно имеет меньшее значение, поэтому мы заменяем возможную конечную точку текущей позицией:

endpoint = {position = 21, value = 35594}  

И затем мы перебираем последнюю цифру:

00011001000110100010001   unit = 524288; reverse = 35594 + 1 * 524288 = 559882  
                          endpoint.value *= 2 + 1 = 71189

Итак, в конце мы обнаруживаем, что позиция 21 дает нам наименьшее значение, поэтому это оптимальное решение:

00011001000110100010001  ->  00000010001011000100111
   ^                 ^
start = 3         end = 21

Вот версия C++, в которой вместо целых чисел используется вектор bool. Он может анализировать строки длиннее 64 символов, но сложность, вероятно, квадратична.

#include <vector>

struct range {unsigned int first; unsigned int last;};

range lexiLeastRev(std::string const &str) {
    unsigned int len = str.length(), first = 0, last = 0, run = 0, max_run = 0;
    std::vector<bool> forward(0), reverse(0);
    bool leading_zeros = true;

    for (unsigned int pos = 0; pos < len; pos++) {
        bool digit = str[pos] - 'a';
        if (!digit) {
            if (leading_zeros) continue;
            if (++run > max_run || run == max_run && reverse < forward) {
                max_run = run;
                last = pos;
                forward = reverse;
            }
        }
        else {
            if (leading_zeros) {
                leading_zeros = false;
                first = pos;
            }
            run = 0;
        }
        forward.push_back(digit);
        reverse.insert(reverse.begin(), digit);
    }
    return range {first, last};
}
person m69 ''snarky and unwelcoming''    schedule 15.09.2017
comment
Как это может быть O(n), если длина чисел, которыми вы манипулируете, обычно зависит от n? - person algrid; 16.09.2017
comment
Я не понимаю логики пункта 3, т.е. сравнения endpoint.value и reverse. Почему это сработает? Кроме того, все кристально чисто. - person cryptomanic; 16.09.2017
comment
@algrid Вы, вероятно, правы, что это не O (n) в самом строгом математическом смысле; скажем, это линейно в пределах целочисленного диапазона конкретной реализации. - person m69 ''snarky and unwelcoming''; 16.09.2017
comment
@cryptomanic В любой момент reverse содержит значение строки, как если бы она была перевернута до текущей позиции, тогда как value условно оптимальной конечной точки содержит значение строки, если бы она была перевернута только до этой конечной точки. Если reversed меньше, это означает, что строка, перевернутая до текущей точки, лексикографически меньше, чем строка, в которой перевернута только часть до конечной точки, и поэтому текущая позиция становится новой оптимальной конечной точкой. - person m69 ''snarky and unwelcoming''; 16.09.2017
comment
Если количество битов в каждом сравнении зависит от n, оно было бы линейным, если бы количество сравнений было постоянным. Однако в этом случае количество сравнений зависит от n. Тем не менее, это отличный ответ. - person גלעד ברקן; 16.09.2017
comment
@cryptomanic Мы можем сравнить reverse и value в любой момент, не анализируя всю строку; если вы сохранили и обновили обе конечные точки и дождались конца строки, чтобы сравнить их, вы бы заметили, что они оба были умножены и добавлены к одним и тем же числам, поэтому, если одно меньше другого, это не изменится. при разборе остальной части строки. - person m69 ''snarky and unwelcoming''; 16.09.2017
comment
@גלעדברקן Привет еще раз! (Кстати, я тоже не понял ответа Ричи, но боялся спросить :-) Думаю, можно сказать, что он линейный в пределах определенного диапазона бит; т. е. если строка состоит из 20 символов, она занимает 20 шагов (и максимум 10 возможных конечных точек), если она состоит из 30 символов, имеется 30 шагов (и максимум 15 возможных конечных точек). Если вы переходите к длине, которая требует большего целочисленного размера или BigInteger, тогда, конечно, происходит скачок в сложности. Как бы вы формально обозначили сложность этого? - person m69 ''snarky and unwelcoming''; 16.09.2017
comment
Похоже, вы говорите, что это линейно, если n ограничено. Но вся идея сложности состоит в том, чтобы учитывать влияние переменной n (длины ввода). - person גלעד ברקן; 16.09.2017
comment
Да, я знаю. Я не очень хорошо знаком с математическими деталями нотации O(), возможно, мне следует воздержаться от ее использования. Я предполагаю, что смысл моего ответа в том, что вам нужно только один раз перебрать строку, чтобы получить результат, даже с несколькими сериями нулей одинаковой длины, что и искал спрашивающий. На данный момент я изменил свою оценку сложности на O(n)-ish для ограниченных размеров строк. - person m69 ''snarky and unwelcoming''; 16.09.2017
comment
Кажется, я понял идею Ричи - ясно, что начало разворота - это первая b слева, назовем ее b_1. И что нам нужно перевернуть, так это наименьший суффикс суффикса, начинающегося с b_1, перевернутого. - person גלעד ברקן; 16.09.2017
comment
@גלעדברקן О, мило. Тем не менее, я надеюсь, что он расширит свой ответ, потому что сейчас он довольно загадочен. - person m69 ''snarky and unwelcoming''; 16.09.2017
comment
@גלעדברקן Означает ли проблема, обнаруженная Ричи в вашем алгоритме, что у вас может быть более двух кандидатов одновременно? Первоначально я начал писать свой ответ как метод, который требовал отслеживания нескольких кандидатов и выбора в конце, какой из них был лучшим; Я только понял, что это можно решить сразу, прорабатывая пример. - person m69 ''snarky and unwelcoming''; 18.09.2017
comment
Да, он не мог эффективно справляться с повторяющимися циклами в строке. - person גלעד ברקן; 18.09.2017