【问题标题】:Counting minimum number of swaps to group characters in string计算最小交换次数以对字符串中的字符进行分组
【发布时间】:2017-10-04 20:17:09
【问题描述】:

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

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

但是,字符串被认为是循环的,所以即使字符串“LLLRRR”也有一个由最后一个“R”和第一个“L”组成的“RL”序列。

可以交换两个连续的元素。所以我们只能交换位置 ii+1 上的元素,或者位置 0 和 n-1 上的元素,如果 n 是字符串的长度(字符串从 0 开始索引)。

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

示例

对于字符串 'RLLRRL',问题可以通过一个交换来解决:交换第一个和最后一个字符(因为字符串是循环的)。因此,该字符串将变为“LLLRRR”,但连接不正确。

我尝试了什么

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

这个算法在 O(N) 时间内有效,但它没有给出正确的结果。

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

【问题讨论】:

  • 我无法理解你的逻辑。如果你能给出递归定义,那会很有帮助。
  • 我怀疑你的解决方案是 O(N),因为 L 或 R 的选择已经是 O(N),然后你仍然需要计算每个选定字母的交换次数,这本身听起来 O(N),所以这将使你的算法 O(N²)。但是没有看到任何代码是不可能的。注意:我已经稍微改写了你的问题,希望它更清楚一点。请检查是否适合您。

标签: string algorithm dynamic-programming


【解决方案1】:

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

一些观察和定义:

  • 只有一个坏连接的目标是另一种说法,即 L 字母和 R 字母(在循环字符串中)应该全部组合在一起

  • 让一个组表示一系列不能变大的相同类型的后续字母(因为周围的字母不同)。通过组合单个交换,您可以用一个或多个“步骤”“移动”一个组。一个例子——我会写.而不是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
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 个字符的字符串的结果是在几分之一秒内计算出来的(参见上面的 sn-p 显示的毫秒数)。

【讨论】:

  • 感谢您的回答和您对算法的详尽解释,您让我觉得如果某件事看起来太难,那并不难。
【解决方案2】:

任务是对循环列表中的项目进行重新排序,如下所示:

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 复杂度的方法。

如果我们可以根据位置 X-1 的 L 区域的计算来计算位置 X 的 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> 半区的项和步数增加到 7 和 38。 此外,作为 R 区第一个项目的 L 已成为 L 区的最后一个项目,因此我们将 R 此外,R 区中间的 L 已从 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,通过查看在 4 处发生的情况列表中的位置,而不必再次遍历整个列表。


更新

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

【讨论】:

  • 如果这可以在代码中实现会很有趣,因此可以对其进行测试。
  • @trincot 我不知道这周我是否有时间。我认为逻辑会起作用,但会有很多繁琐的细节。
【解决方案3】:

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

【讨论】:

    猜你喜欢
    • 2022-01-24
    • 2020-05-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多