给定一个由0和1组成的循环字符串,找到使所有1聚集在一起所需的最小相邻交换次数。

3
如果给出一个由0和1组成的循环字符串(意思是你可以交换0和sizeOfString-1),那么展示所有1排在一起所需的最小相邻交换次数的好算法是什么? 1不需要在字符串中的任何特定位置分组,只需要在提供最小相邻交换次数的任何位置进行分组。
例如,如果字符串如下所示:
011010110001
最少的相邻交换次数将是6,因为您会执行以下交换:
交换索引0和11,结果为:111010110000
交换索引3和4,结果为:111100110000
交换索引5和6,结果为:111101010000
交换索引4和5,结果为:111110010000
交换索引6和7,结果为:111110100000
交换索引5和6,结果为:111111000000
有没有一个好的算法可以找到任何1和0字符串的最小相邻交换次数?

从您的描述来看,我会说最少是2而不是6。交换3-7和交换5-11。如果您的示例正确,也许您需要稍微改一下措辞。 - Ralf
当您说1(和0)必须分组时,这是以循环方式吗?换句话说,1000111是一个有效的序列吗? - Damien
它们需要相邻交换,它们必须彼此相邻。 - user14483828
是的,1000111是一个有效的序列。 - user14483828
我认为这个stackoverflow条目中有一个相同的问题和被接受的答案。我不知道它是否对你有帮助? - ibra
2个回答

0

找到一个好的起点,将所有相同价值的东西移动到那个聚类中。

注意:Python 有点生疏,目前没有设置测试(通常使用 Ruby 这些天),但应该非常接近有效的 Python,如果不是有效的话。

def longest_cluster_ending_indexes(array)
  # Scan the array to find the end index of the longest cluster of the same value
  index = 0
  check = array[start]
  best_index = 0
  most_count = 1
  count = 1
  for item in array[1:-1]:
    index += 1
    if item == check:
      count += 1
      if count > most_count:
        best_index = index
        most_count = count
      else:
        check = item
        count = 1
   return (best_index - most_count, best_index)

last_seen_left,last_seen_right = longest_cluster_ending_index(array)
check = array[last_seen_right]
index_left = start_left = last_seen_left
index_right = start_right = last_seen_right
swaps = 0
while true:
  index_left -= 1
  if array[index_left] == check
    diff = (index_left - last_seen_left) - 1
    swaps += diff
    last_seen_left -= 1

  if array[index_right] == check
    diff = (index_right - last_seen_right) - 1
    swaps += diff
    last_seen_right += 1

  break if index_left % len(array) == start_right # Went around the array
  break if index_right % len(array) == start_left # Went around the array

我们想要找到最长的一组连续值,并从该连续值的末尾开始。这可以避免像 10000000000000011101 这样的情况在索引 0 开始时给出错误的结果和糟糕的性能。相反,最好从一组大量零中的最后一个零开始。在这种情况下,您只需要对下一个零进行单个交换即可将其向后移动。

性能为 O(N),其中 N 是所有 1 和 0 的总数。

示例: 111000111000

这里我们有 4 组相等大小的连续值。算法应确定索引2是最佳起点(第一组的结尾)。它将扫描,找到第二组 1 并逐一移动它们。交换次数为 3 + 3 + 3 = 9。

111100011000 3 swaps. index 7 - index 3 - 1 = 3 swaps (right side)
111110001000 3 swaps. index 8 - index 4 - 1 = 3 swaps (right side)
111111000000 3 swaps. index 9 - index 5 - 1 = 3 swaps (right side)

11101100001
11101100001 # Second to last is starting index
11111000001 # index 3 and index 6, cost is 2 swaps (from the left side)

嗯,这个提出了一个很好的观点。算法必须进行调整,从最佳集群双向搜索(而不仅仅向右)。虽然修复了,但现在它会多做大约两倍的工作(它可能会在数组的另一边中途结束,形成一个循环)。
01001001100 => left start = 2 (moving left), right start = 3 (moving right)
10000101100 => Swap left and right (cost = 1 + 1 = 2 swaps)
00000011101 => Swap left and right (cost = 1 + 1 = 2 swaps)
00000011110 => One more swap
So 5 swaps.

011010110001 => Left start at 8, right start and the last index
111011100000 => 2 swaps and 1 swap = 3 swaps
011111100000 => 3 swaps
6 swaps

2
这是否有效取决于find_index_within_the_most_ones_or_zeros,它是算法中最重要的部分,但在此处并没有明确定义... - trincot
1
10000000000000011101 只需要将最后一个1向左移动一位就可以得到 10000000000000011110,然后再将第一个1向左移动,我们就可以得到 00000000000000011111,因此这不是最优解。 - user14483828
@trincot 添加了缺失的函数。同时将其重命名为 longest_group_ending_index,以更准确地描述它所寻找的内容。 - Nuclearman
注意到我们需要从最长的簇向左和向右移动。 - Nuclearman

0

所以我受到了this answer的启发,请先阅读那个答案,它所属的问题基本上与这个问题相同,只是在这个问题中输入字符串是循环的。那个答案中的关键思想是,为了找到将所有1分组所需的最小交换次数,我们只需要得到一个包含所有1索引的数组,该数组的中心始终保持中心索引,然后按照以下算法计算最小交换次数,时间复杂度为O(n)

oneIndices = array of indices of all 1's in the input
middleOfOnesIndices = round(oneIndices.length/2)-1    // index to the center index
minimumSwaps = 0
foreach index i of oneIndices
    minimumSwaps += aboluteValue(oneIndices[middleOfOneIndices]-oneIndices[i])-absoluteValue(middleOfOneIndices-i);

基于这个答案,这个问题的一个快速解决方案是在某个点上打破圆形并将其拉伸成一条直线,假设为str,然后对其应用上述算法,然后我们找到str的最小交换次数。让n为输入循环字符串的长度,则有n个点可以打破圆形,此解决方案的最终时间复杂度将为O(n^2)

因此,我试图提出一个需要O(n)时间复杂度的解决方案。在上面的算法中,决定交换次数的是中间1和其他1之间的相对距离。既然我们已经计算出了str的最小交换次数,那么我们是否可以在其他情况下重复使用它?这样我们就不必在每种情况下进行for循环来计算最小交换次数,那么时间复杂度将为O(n)

在所有可能的断点中,有些是我们不需要的。以输入的循环字符串100011为例,我们在两个不同的点上断开它,一个是在索引01之间,另一个是在12之间,然后我们会得到:

000111  and  001110

在上述算法中,我们只需要计算其他 1 距离中间的 1相对距离。在这两种情况下,决定最小交换次数的是它们包含所有 1公共子串,即为 111。因此,为了避免重复计算同一结果的情况,我们只在每个 1 后立即打破循环。

为了重用上一次的结果,我们按照从左到右的顺序依次在每个 1 后打破循环。以 011010110001 为例,我们首先计算最小交换次数,然后在索引 12 之间打破循环。为了简单起见,在将循环拉成一条直线时,在打破循环点之前仍将数字保持为 0。

011010110001   <---before
00101011000101 <---after break it between indices 1 and 2
^^  <--- these two 0's actually don't exist, but to keep the indices of 1's to it's right 
         as unchanged, so we can reuse last result handily.

the array of all indices of 1's: 
[1,2,4,6,7,11]  <----before
[2,4,6,7,11,13] <----after break it between indices 1 and 2

我的最终解决方案是用JavaScript编写的,时间复杂度为O(n)

function getOneIndices(inputString) {
    var oneIndices = [];
  for (var i=0; i<inputString.length; i++) {
    if (inputString.charAt(i) === "1") {
        oneIndices.push(i);
    }
  }
  return oneIndices;
}

function getSwapInfo(inputString) {
  var oneIndices = getOneIndices(inputString);
  
  if(oneIndices.length < 2) return 0;
  
  var minSwaps = Number.MAX_VALUE;
  var distanceInInput = 0, distanceInOneIndices = 0;
  
  var middleOfOneIndices = Math.round(oneIndices.length/2)-1;
  
  for (var i=0; i<oneIndices.length; i++) {
    distanceInInput += Math.abs(oneIndices[middleOfOneIndices]-oneIndices[i]);
    distanceInOneIndices += Math.abs(middleOfOneIndices-i);
  }
  
  for(var i = 0, lastOneIndexMoved = -1, endIndexInInput = inputString.length - 1; 
                                        i <= oneIndices.length; i++){
    var curSwaps = distanceInInput - distanceInOneIndices;
    if(curSwaps < minSwaps) minSwaps = curSwaps;
    
    var lastMiddleIndex = oneIndices[middleOfOneIndices];
    
    //move the first 1 to the end as we break the circle after it.
    var firstOneIndex = oneIndices[0];
    oneIndices.splice(0, 1);
    var lastOneIndex = endIndexInInput + firstOneIndex - lastOneIndexMoved;
    oneIndices.push(lastOneIndex);
    
    //when we move one step further, the index of the center also move one step further,
   // but the length of the indices array of 1's doesn't change, we don't 
   //need to do middleOfOneIndices++
    var diff = oneIndices[middleOfOneIndices] - lastMiddleIndex;
    
    distanceInInput += middleOfOneIndices * diff 
                  - (oneIndices.length - middleOfOneIndices - 1) * diff
                  - Math.abs(lastMiddleIndex - firstOneIndex)
                  + Math.abs(oneIndices[middleOfOneIndices] - lastOneIndex);
    
    lastOneIndexMoved = firstOneIndex;
    endIndexInInput = lastOneIndex;
  }

    return minSwaps;
}

console.log("minimum swaps for '111111': " + getSwapInfo("111111"));
console.log("minimum swaps for '111011': " + getSwapInfo("111011"));
console.log("minimum swaps for '010001': " + getSwapInfo("010001"));
console.log("minimum swaps for '011010110001': " + getSwapInfo("011010110001"));
console.log("minimum swaps for '10000000000000011101': " + getSwapInfo("10000000000000011101"));
console.log("minmum swaps for '01001001100': " + getSwapInfo("01001001100"));


没有注意到 JavaScript 中 Array.prototype.splice() 的时间复杂度是 O(n),结果我的最终解决方案实际上需要 O(n^2) 的时间复杂度。为了将时间复杂度降至 O(n),我们不会每次都删除 oneIndices 中的第一个索引 1,而是增加数组 oneIndices 的开头,并在获取 firstOneIndex、计算 middleOfOneIndicesdistanceInInput 时将其计入。 - Qiu Zhou

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接