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

Я пытаюсь решить довольно сложную проблему со строками:

данная строка содержит до 100000 символов, состоящих только из двух различных символов " L " и "R". Последовательность "RL" считается "плохой", и такие вхождения должны быть уменьшены путем применения свопов.

однако строка должна считаться круговой, поэтому даже строка "LLLRRR" имеет последовательность "RL", образованную последним " R "и первым "L".

свопы двух последовательных элементов можно сделать. Таким образом, мы можем поменять местами только элементы, которые находятся на позициях Я и i+1, или на позиции 0 и n-1, если n - длина строки (строка 0-индексируется).

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

пример

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

что я пробовал

моя идея состоит в том, чтобы использовать динамическое программирование и вычислить для любого заданного "L", сколько свопов необходимо, чтобы поместить все остальные " L "слева от этого" L "и, альтернативно, справа от этого "L". Для любого " R " я вычисляю то же самое.

этот алгоритм работает в O (N) времени, но это не дает правильного результат.

Он не работает, когда мне нужно поменять местами первый и последний элементы. Что я должен добавить в свой алгоритм, чтобы он работал и для этих свопов?

3 ответов


проблема может быть решена в линейное время.

некоторые наблюдения и определения:

  • цель иметь только одно плохое соединение-это еще один способ сказать, что все буквы L должны быть сгруппированы вместе, а также буквы R (в круговой строке)

  • пусть группа обозначает ряд последующих букв того же рода, которые нельзя сделать больше (из-за окружающих букв, которые отличаются). От комбинируя отдельные свопы, вы можете "переместить" группу с помощью одного или нескольких "шагов". Пример - напишу . вместо L таким образом, это более легко читается:

    RRR...RR....
    

    здесь 4 группы:RRR, ..., RR и ..... Предположим, вы хотите присоединиться к группе из двух "R" с левой группой "R" в приведенной выше строке. Затем вы можете "переместить" эту среднюю группу с 3 шагами влево, выполнив 6 свопов:

    RRR...RR....
    RRR..R.R....
    RRR..RR.....
    RRR.R.R.....
    RRR.RR......
    RRRR.R......
    RRRRR.......
    

    эти 6 свопов что составляет один групповой ход. Стоимость переезда равна 6, и это произведение размера группы (2) и расстояния, которое она проходит (3). Обратите внимание, что этот ход точно такой же, как если бы мы переместили группу с тремя символами "L" (ср. точки) справа.

    Я буду использовать слово "двигаться" в этом смысле.

  • всегда есть решение, которое можно выразить в виде серии групповых ходов, где каждое групповое движение уменьшает количество группы с двумя, т. е. при каждом таком движении две R группы объединяются в одну, а следовательно, и две L группы объединяются. Другими словами, всегда есть решение, когда ни одна из групп не должна разделяться, когда одна ее часть движется влево, а другая вправо. Я не буду приводить здесь доказательства этого утверждения.

  • всегда есть решение, которое имеет одну группу, которая не будет двигаться вообще: все другие группы той же буквы будут двигаться к нему. Вследствие этого существует также группа обратное письмо, что не будет двигаться, где-то на другом конце круга. Опять же, я не буду доказывать это здесь.

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

алгоритм

An алгоритм может выглядеть так:

создать массив целых чисел, где каждое значение представляет размер группы. Массив будет перечислять группы в порядке их появления. Это учитывало бы свойство circular, так что первая группа (с индексом 0) также учитывала бы букву(Ы) В самом конце строки, которые совпадают с первой буквой(АМИ). Таким образом, при четных индексах у вас будут группы, представляющие количество одной конкретной буквы, а при нечетных индексах - будет отсчет другого письма. На самом деле не имеет значения, какую из двух букв они представляют. Массив групп всегда будет иметь четное число элементов. Этот массив-все, что нам нужно для решения проблемы.

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

теперь определите стоимость (количество свопов) для перемещения всех четных групп в среднюю группу.

это может быть или не быть решением, так как выбор средней группы был произвольный.

вышеизложенное пришлось бы повторить для сценариев, где любая из других четных групп была принята за среднюю группу.

теперь суть алгоритма состоит в том, чтобы избежать повторения всей операции при принятии другой группы в качестве средней группы. Оказывается, можно взять следующую четную группу как среднюю (по индексу 2) и скорректировать ранее рассчитанную стоимость в постоянное время (в среднем), чтобы получить стоимость для этого выбора средней группа. Для этого нужно сохранить в памяти несколько параметров: стоимость выполнения ходов в левом направлении и стоимость выполнения ходов в правом направлении. Также сумма размеров четных групп должна поддерживаться для каждого из обоих направлений. И, наконец, сумма размеров нечетных групп также должна поддерживаться для обоих направлений. Каждый из этих параметров может быть скорректирован при принятии следующей четной группы в качестве средней группы. Часто соответствующая разделенная группа должна быть повторно идентифицируется также, но также это может произойти в среднем за постоянное время.

не углубляясь в это, вот рабочая реализация в простом JavaScript:

код

function minimumSwaps(s) {
    var groups, start, n, i, minCost, halfSpace, splitAt, space,
        cost, costLeft, costRight, distLeft, distRight, itemsLeft, itemsRight;
    // 1. Get group sizes 
    groups = [];
    start = 0;
    for (i = 1; i < s.length; i++) {
        if (s[i] != s[start]) {
            groups.push(i - start);
            start = i;
        }
    }
    // ... exit when the number of groups is already optimal
    if (groups.length <= 2) return 0; // zero swaps
    // ... the number of groups should be even (because of circle)
    if (groups.length % 2 == 1) { // last character not same as first
        groups.push(s.length - start);
    } else { // Ends are connected: add to the length of first group
        groups[0] += s.length - start;
    }
    n = groups.length;
    // 2. Get the parameters of the scenario where group 0 is the middle:
    //    i.e. the members of group 0 do not move in that case.
    // Get sum of odd groups, which we consider as "space", while even 
    // groups are considered items to be moved.
    halfSpace = 0;
    for (i = 1; i < n; i+=2) {
        halfSpace += groups[i];
    }
    halfSpace /= 2;
    // Get split-point between what is "left" from the "middle" 
    // and what is "right" from it:
    space = 0;
    for (i = 1; space < halfSpace; i+=2) {
        space += groups[i];
    }
    splitAt = i-2;
    // Get sum of items, and cost, to the right of group 0
    itemsRight = distRight = costRight = 0;
    for (i = 2; i < splitAt; i+=2) {
        distRight += groups[i-1];
        itemsRight += groups[i];
        costRight += groups[i] * distRight;
    }
    // Get sum of items, and cost, to the left of group 0
    itemsLeft = distLeft = costLeft = 0;
    for (i = n-2; i > splitAt; i-=2) {
        distLeft += groups[i+1];
        itemsLeft += groups[i];
        costLeft += groups[i] * distLeft;
    }
    cost = costLeft + costRight;
    minCost = cost;
    // 3. Translate the cost parameters by incremental changes for 
    //    where the mid-point is set to the next even group
    for (i = 2; i < n; i += 2) {
        distLeft += groups[i-1];
        itemsLeft += groups[i-2];
        costLeft += itemsLeft * groups[i-1];
        costRight -= itemsRight * groups[i-1];
        itemsRight -= groups[i];
        distRight -= groups[i-1];
        // See if we need to change the split point. Items that get 
        // at the different side of the split point represent items
        // that have a shorter route via the other half of the circle.
        while (distLeft >= halfSpace) {
            costLeft -= groups[(splitAt+1)%n] * distLeft;
            distLeft -= groups[(splitAt+2)%n];
            itemsLeft -= groups[(splitAt+1)%n];
            itemsRight += groups[(splitAt+1)%n];
            distRight += groups[splitAt];
            costRight += groups[(splitAt+1)%n] * distRight;
            splitAt = (splitAt+2)%n;
        }
        cost = costLeft + costRight;
        if (cost < minCost) minCost = cost;
    }
    return minCost;
}

function validate(s) {
    return new Set(s).size <= 2; // maximum 2 different letters used
}

// I/O
inp.oninput = function () {
    var s, result, start;
    s = inp.value;
    start = performance.now(); // get timing
    if (validate(s)) {
        result = minimumSwaps(s); // apply algorithm
    } else {
        result = 'Please use only 2 different characters';
    }
    outp.textContent = result;
    ms.textContent = Math.round(performance.now() - start);
}

rnd.onclick = function () {
    inp.value = Array.from(Array(100000), _ => 
                    Math.random() < 0.5 ? "L" : "R").join('');
    if (inp.value.length != 100000) alert('Your browser truncated the input!');
    inp.oninput(); // trigger input handler
}

inp.oninput(); // trigger input handler
input { width: 100% }
<p>
    <b>Enter LR series:</b>
    <input id="inp" value="RLLRRL"><br>
    <button id="rnd">Produce random of size 100000</button>
</p><p>
    <b>Number of swaps: </b><span id="outp"></span><br>
    <b>Time used: </b><span id="ms"></span>ms
</p>

Сложность

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

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

поэтому сложность времени O (n).

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


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

LRLLLRLLLRRLLRLLLRRLRLLLRLLRLRLRLRRLLLRRRLRLLRLLRL  

так что мы получаем список, как это:

RRRRRRLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLRRRRRRRRRRRRRR  

или такой:

LLLLLLLLLLLLLLLLLLLLLLLLLRRRRRRRRRRRRRRRRRRRRLLLLL  

где два типа элементов сгруппированы вместе, но точное положение этих двух групп не имеет значения.

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

#L = 30  
#R = 20  

тогда простым решением грубой силы было бы рассматривать каждую позицию в списке как начало L-зоны, начиная с позиции 0, перебирать весь список и подсчитывать, сколько шагов каждый элемент находится от границы зоны, где он должен быть:

LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLRRRRRRRRRRRRRRRRRRRR  <- desired output  
LRLLLRLLLRRLLRLLLRRLRLLLRLLRLRLRLRRLLLRRRLRLLRLLRL  <- input  
 <   <   <<  <   >> >   >  > >< <  <<<   > >> >> >  <- direction to move  

затем мы рассмотрим L-зону, чтобы начать с позиции 1, и снова сделаем весь расчет:

RLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLRRRRRRRRRRRRRRRRRRR  <- desired output  
LRLLLRLLLRRLLRLLLRRLRLLLRLLRLRLRLRRLLLRRRLRLLRLLRL  <- input  
<<   <   <<  <   >> >   >  > >  <  <<<   > >> >> >  <- direction to move  

после вычисления общего количества шагов для каждого положение L-зоны, мы бы знали, какая позиция требует наименьшего количества шагов. Это, конечно, метод с N2 сложности.

если бы мы могли вычислить количество необходимых шагов с L-зоной в позиции X, основываясь на вычислении для L-зоны в позиции X-1 (без повторения по всему списку снова), это могло бы снизить сложность до N.

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

LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLRRRRRRRRRRRRRRRRRRRR  <- desired output  
<<<<<<<<<<<<<<<>>>>>>>>>>>>>>><<<<<<<<<<>>>>>>>>>>  <- half-zones
LRLLLRLLLRRLLRLLLRRLRLLLRLLRLRLRLRRLLLRRLRRLLRLLRL  <- input  
 <   <   <<  <   >> >   >  > >< <  <<<  >  >> >> >  <- direction to move  
        5              6           5         6      <- wrong items
       43             45          25        31      <- required steps  

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

        5              6           5         6      <- wrong items
       38             51          20        37      <- required steps  
, нам нужно проверить четыре пограничных пункта, чтобы увидеть, не переместились ли какие-либо неправильные элементы из одной половины зоны в другую, и соответственно скорректировать количество элементов и шагов.

в Примере L, который был первым элементом L-зоны, теперь стал последним элементом в R-зоне, поэтому мы увеличиваем элемент R> half-zone и количество шагов до 7 и 38.
Кроме того, L, который был первым элементом в R-зоне, стал последним элементом L-зоны, поэтому мы уменьшаем количество элементов для R Кроме того, L в середине R-зоны переместился из R> в R и R и R

RLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLRRRRRRRRRRRRRRRRRRR  <- desired output  
><<<<<<<<<<<<<<<>>>>>>>>>>>>>>><<<<<<<<<<>>>>>>>>>  <- half-zones
LRLLLRLLLRRLLRLLLRRLRLLLRLLRLRLRLRRLLLRRLRRLLRLLRL  <- input  
><   <   <<  <   >> >   >  > >  <  <<<  <  >> >> >  <- direction to move  
         5              6           5         6     <- wrong items
        38             51          30        28     <- required steps  

таким образом, общее количество необходимых шагов, когда L-зона начинается с позиции 0, было 144, и мы вычислили, что когда L-зона начинается с позиции 1, общее число теперь 147, посмотрев на то, что происходит на четырех позициях в списке, вместо того, чтобы повторять весь список снова.


обновление


O(n) решение:

L   L   R   L   L   R   R   R   L   L   R   R   L   R
Number of R's to the next group of L's to the left:
1   1       1   1               3   3           2

NumRsToLeft: [1, 1, 3, 2]

Number of swaps needed, where 0 indicates the static L group, and | represents
 the point to the right of which L's move right, wrapping only when not at the end
 (enough L's must move to their right to replace any R's left of the static group):

  2*0       + 2*1          +      2*(3+1)    +    1*(2+3+1)  |
  2*1       + 2*0          +      2*3     |  +    1*(1+1)

There are not enough L's to place the static group in the third or fourth position.

Variables: 0 1 4 6 |
           1 0 3 | 2

Function: 2*v_1 + 2*v_2 + 2*v_3 + 1*v_4

Coefficients (group sizes): [2, 2, 2, 1]

Change in the total swaps needed when moving the static L group from i to (i+1):

 Subtract: PSum(CoefficientsToBeGoingLeft) * NumRsToLeft[i+1]

 Subtract: c_j * PSum(NumRsToLeft[i+1...j]) for c_j <- CoefficientsNoLongerGoingLeft

 Add: (PSum(CoefficientsAlreadyGoingRight) + Coefficients[i]) * NumRsToLeft[i+1]

 Add: c_j * PSum(NumRsToLeft[j+1...i+1]) for c_j <- NewCoefficientsGoingRight

(PSum can be calculated in O(1) time with prefix sums; and the count of coefficients
 converting from a left move to a right move throughout the whole calculation is not
 more than n. This outline does not include the potential splitting of the last new
 group converting from left move to right move.)