计算字符串

时间:2017-05-06 12:17:17

标签: string algorithm dynamic-programming

我正在尝试用字符串解决一个非常复杂的问题:

给定的字符串最多包含100000个字符,仅包含两个不同的字符“L”和“R”。序列“RL”被认为是“坏”,并且必须通过应用交换来减少这种情况。

然而,该字符串被认为是循环的,因此即使字符串'LLLRRR'也有一个由前一个'R'和第一个'L'组成的'RL'序列。

可以进行两个连续元素的交换。因此,我们只能交换 i i + 1 位置的元素,或者位置0和 n-1 的元素,如果 n 是字符串的长度(字符串是0索引的)。

目标是找到在字符串中只留下一个错误连接所需的最小交换次数。

实施例

对于字符串'RLLRRL',只需一次交换就可以解决问题:交换第一个和最后一个字符(因为字符串是圆形的)。因此,字符串将成为“LLLRRR”,连接不良。

我尝试了什么

我的想法是使用动态编程,并计算任何给定的'L'需要多少次交换才能将所有其他'L'留给那个'L',或者,在这个'L'的右侧。对于任何'R',我计算相同。

此算法适用于 O(N)时间,但不能提供正确的结果。

当我必须交换第一个和最后一个元素时,它不起作用。我应该在算法中添加什么才能使其适用于那些交换?

3 个答案:

答案 0 :(得分:6)

问题可以在线性时间内解决。

一些观察和定义:

  • 只有一个不良连接的目标是说L字母应该全部组合在一起的另一种方式,以及R字母(在圆形字符串中)

  • 让一组表示一系列随后不能变大的字母(因为周围的字母不同)。通过组合单个交换,您可以“移动”#34;具有一个或多个"步骤"的组。一个例子 - 我会写.而不是L,因此它更易于阅读:

    RRR...RR....
    

    此处有4个小组:RRR...RR....。假设你想加入两个组合" R"用左边的" R"上面字符串中的组。然后你可以"移动"通过执行6次交换,中间组左侧有3个步骤:

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

    这6次掉期构成了一次集体行动。移动的成本是6,并且是组(2)的大小和它行进的距离(3)的乘积。请注意,此移动与我们将组移动三个" L"完全相同。右边的字符(参见点)。

    我将使用"移动"就是这个意思。

  • 总有一种解决方案可以表示为一系列组移动,其中每组移动减少了具有两组的组数,即每次移动时,两个R组合并为一组,因此还合并了两个L组。换句话说,总有一种解决方案,其中没有一个组必须分开,其中一部分向左移动而另一部分向右移动。我不会在这里提供这种说法的证明。

  • 总会有一个解决方案,其中一个组根本不会移动:同一个字母的所有其他组将向它移动。因此,还有一组相反的字母不会移动,在圆圈的另一端。再说一次,我不会在这里证明这一点。

  • 这个问题等同于最小化代表两个字母之一的组的移动的总成本(交换)(所有组的一半)。另外一半的组同时移动,如上例所示。

算法

算法可以是这样的:

创建一个整数数组,其中每个值代表一个组的大小。该数组将按照它们出现的顺序列出组。这会考虑循环属性,因此第一个组(索引为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
&#13;
input { width: 100% }
&#13;
<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>
&#13;
&#13;
&#13;

时间复杂度

预处理(创建groups数组等),以及第一组是中间组时的成本计算,都包含最多 n 次迭代的非嵌套循环,所以这部分是 O(n)

当中间组是任何其他偶数组时,计算成本包括一个循环(用于选择中间组),以及另一个用于调整拆分组选择的内循环。即使这个内部循环可以迭代多次外部循环的一次迭代,总的来说这个内部循环将没有比 n 更多的迭代,所以这个外部循环的总执行时间仍然是 O(n)的

因此,时间复杂度为 O(n)

请注意,100,000个字符的字符串的结果是在几分之一秒内计算出来的(请参阅上面代码段显示的毫秒数)。

答案 1 :(得分:3)

任务是重新排序圆形列表中的项目,如下所示:

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区的每个位置的总步数之后,我们将知道哪个位置需要最少的步数。这当然是具有N 2 复杂性的方法。

如果我们可以计算位置X处L区的所需步数,根据位置X-1的L区计算(不再遍历整个列表),这可能会带来复杂性到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&gt;半区的项目和步数统计为7和38 此外,作为R区中第一项的L已成为L区的最后一项,因此我们减少R&lt;半区到4.
此外,R区中间的L已从R> R移动。到R&lt;半区,所以我们递减并递增R>的项目计数。和R&lt;到6和5,并且将步数减少并增加10(R>和R <半区的长度)到28和30.

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,通过查看四处发生的情况列表中的位置,而不是必须再次遍历整个列表。

更新

在考虑如何实现这一点时,我意识到在一个区域内移动的错误物品的数量必须与在另一个区域中移动的错误物品的数量相同;否则区域之间的边界最终会在错误的位置。这意味着L和R区域不会被分成两个长度相等的半区域,并且&#34; mid&#34;区域中的点根据其左侧和右侧的错误项目移动。我仍然认为可以将其转换为具有O(N)效率的工作代码,但它可能不像我最初描述的那样简单。

答案 2 :(得分:1)

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.)