【问题标题】:In-place interleaving of the two halves of a string字符串两半的就地交错
【发布时间】:2013-04-06 10:51:17
【问题描述】:

给定一个 even 大小的字符串,比如说:

abcdef123456

我将如何交错两半,这样 same 字符串会变成这样:

a1b2c3d4e5f6

我尝试尝试开发一种算法,但没有成功。有人能给我一些关于如何进行的提示吗?我需要这样做而不创建额外的字符串变量或数组。一两个变量就可以了。

我只是不想要一个工作代码(或算法),我需要开发一个算法并在数学上证明它的正确性。

【问题讨论】:

  • 这不是一个有效的解决方案,但它是一个开始。解决前两个字符a1 的算法怎么样,修复数组,然后用较小的数组重复。显然它不是高效的,因为将abcdef123456 转换为a1bcdef23456 效率不高(除非字符串表示为链表?)。你对运行时有什么要求?还是您只是在寻找一种聪明的算法?
  • 你的字符串是数组还是链表?
  • 看起来好像是某种自相似的构建序列,两半越长。我必须去教堂,但我在这里有我的笔记:pastebin.com/nBQf4q90 主要是观察随着长度的变化你需要做出的动作如何变化。我想如果你把它延长一点,关系会变得更清晰。
  • 来自 ctheory 帖子中的 cmets:arxiv.org/abs/0805.1598
  • @Nawaz,这项工作arxiv.org/pdf/0805.1598v1.pdf。他们解决了您的问题的微小变化(但应该清楚如何从您的问题转移到他们的问题,反之亦然)。这只是交换第一个和第二个块,应该清楚为 O(n)。

标签: java c++ c algorithm math


【解决方案1】:

这是一个算法和工作代码。它就位,O(N),概念上很简单。

  1. 穿过阵列的前半部分,将项目交换到位。
    • 从左半部分开始的项目将被交换到右半部分 在我们需要它们之前,所以我们使用一个技巧来确定它们在哪里 被换成了。
  2. 当我们到达中点时,整理已交换到右侧的未放置的左侧项目。
    • 使用相同技巧的变体来找到正确的解扰顺序。
  3. 对剩余的半个数组重复。

这会通过阵列进行不超过 N+N/2 次交换,并且不需要临时存储。

诀窍是找到交换项目的索引。左边的项目在放置时被交换到由右边的项目腾出的交换空间中。交换空间按以下顺序增长:

  • 在末尾添加一个项目(进入由右项目腾出的空间)
  • 将一个项目与最旧的现有(左)项目交换。

按顺序添加项目 1..N 给出:
1 2 23 43 435 465 4657 ...
每一步改变的索引为:
0 0 1 0 2 1 3 ...

这个序列正好是OEIS A025480,可以在O(1)的摊销时间内计算出来:

def next_index(n):
    while n&1: n=n>>1
    return n>>1

一旦我们在交换 N 个项目后到达中点,我们就需要解读。交换空间将包含 N/2 个项目,其中应位于偏移量 i 的项目的实际索引由 next_index(N/2+i) 给出。我们可以通过交换空间前进,将物品放回原处。唯一的复杂之处在于,随着我们的前进,我们最终可能会找到一个位于目标索引左侧的源索引,因此已经在其他地方进行了交换。但是我们可以通过再次查找之前的索引来找出它在哪里。

def unscramble(start,i):
        j = next_index(start+i)
        while j<i: j = next_index(start+j)
        return j

请注意,这只是索引计算,而不是数据移动。在实践中,对next_index 的调用总数对于所有 N 来说

这就是完整实现所需的全部内容:

def interleave(a, idx=0):
    if (len(a)<2): return
    midpt = len(a)//2 

    # the following line makes this an out-shuffle.
    # add a `not` to make an in-shuffle
    base = 1 if idx&1==0 else 0

    for i in range(base,midpt):
        j=next_index(i-base)
        swap(a,i,midpt+j)

    for i in range(larger_half(midpt)-1):
        j = unscramble( (midpt-base)//2, i);
        if (i!=j):
            swap(a, midpt+i, midpt+j)

    interleave(a[midpt:], idx+midpt)

最后的尾递归可以很容易地被循环替换。 Python 的数组语法就不那么优雅了。另请注意,对于此递归版本,输入必须是 numpy 数组而不是 python 列表,因为标准列表切片会创建未传播备份的索引副本。

这是一个验证正确性的快速测试。 (一副 52 张牌的 8 次完美洗牌将其恢复到原始顺序)。

A = numpy.arange(52)
B = A.copy()
C =numpy.empty(52)

for _ in range(8):
    #manual interleave
    C[0::2]=numpy.array(A[:26])
    C[1::2]=numpy.array(A[26:])
    #our interleave
    interleave(A)
    print(A)
    assert(numpy.array_equal(A,C))

assert(numpy.array_equal(A, B))

【讨论】:

  • C 实现和另一个解释尝试here.
【解决方案2】:

你也许可以在 O(N*log(N)) 时间内完成:

Want: abcdefgh12345678 -> a1b2c3d4e5f6g7h8

a b c d e f g h
  1 2 3 4 5 6 7 8

  4 1-sized swaps:

a 1 c 3 e 5 g 7
  b 2 d 4 f 6 h 8

a1  c3  e5  g7
    b2  d4  f6  h8

  2 2-sized swaps:

a1  b2  e5  f6
    c3  d4  g7  h8

a1b2  e5f6
      c3d4  g7h8

  1 4-sized swap:

a1b2  c3d4
      e5f6  g7h8

a1b2c3d4
        e5f6g7h8

在 C 中的实现:

#include <stdio.h>
#include <string.h>

void swap(void* pa, void* pb, size_t sz)
{
  char *p1 = pa, *p2 = pb;
  while (sz--)
  {
    char tmp = *p1;
    *p1++ = *p2;
    *p2++ = tmp;
  }
}

void interleave(char* s, size_t len)
{
  size_t start, step, i, j;

  if (len <= 2)
    return;

  if (len & (len - 1))
    return; // only power of 2 lengths are supported

  for (start = 1, step = 2;
       step < len;
       start *= 2, step *= 2)
  {
    for (i = start, j = len / 2;
         i < len / 2;
         i += step, j += step)
    {
      swap(s + i,
           s + j,
           step / 2);
    }
  }
}

char testData[][64 + 1] =
{
  { "Aa" },
  { "ABab" },
  { "ABCDabcd" },
  { "ABCDEFGHabcdefgh" },
  { "ABCDEFGHIJKLMNOPabcdefghijklmnop" },
  { "ABCDEFGHIJKLMNOPQRSTUVWXYZ0<({[/abcdefghijklmnopqrstuvwxyz1>)}]\\" },
};

int main(void)
{
  unsigned i;

  for (i = 0; i < sizeof(testData) / sizeof(testData[0]); i++)
  {
    printf("%s -> ", testData[i]);
    interleave(testData[i], strlen(testData[i]));
    printf("%s\n", testData[i]);
  }

  return 0;
}

输出(ideone):

Aa -> Aa
ABab -> AaBb
ABCDabcd -> AaBbCcDd
ABCDEFGHabcdefgh -> AaBbCcDdEeFfGgHh
ABCDEFGHIJKLMNOPabcdefghijklmnop -> AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPp
ABCDEFGHIJKLMNOPQRSTUVWXYZ0<({[/abcdefghijklmnopqrstuvwxyz1>)}]\ -> AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz01<>(){}[]/\

【讨论】:

  • +1。好的。很不错。现在,如何证明它的正确性?是否可以进一步改进?
  • 我不明白为什么交换多个字符被认为是O(1) 操作。为什么不考虑将整个字符集转换为O(1),然后线性解决方案就变得微不足道了? abcd1234 => a_bcd234O(1)?不要小看这个解决方案,因为它很聪明,但我认为运行时分析没有多大意义。
  • 我认为,归纳应该可以解决问题。外行人可以观察它在 4、8、16、32 和 64 的长度上工作并停在那里。我不知道这是否可以进一步改进。我实际上怀疑这种特殊方法能否得到显着改进。
  • @roliu 我不认为交换多个字符组为 O(1)。看看 cmets:4 1-sized swaps, ..., 1 4-sized swap。请注意,我确实提到了交换的大小和每个级别的交换数量。它们的乘积是常数,并且在字符串的length 中是线性。但是,只有log2(length) 级别。这会给你length*log2(length) 操作或时间。
  • 你是对的。我太累了。我想到了另一种在同一运行时使用交换的算法。而不是在外部向中间工作,它只是将角色移动到它们应该在的位置:abcdefgh12345678 =&gt; abcd 1234 efgh 5678 =&gt; ab 12 cd 34 ef 56 gh 78 =&gt; a 1 b 2 c 3 d 4 e 5 f 6 g 7 h 8
【解决方案3】:

这里是 J. Ellis 和 M. Markov 的解决方案。完美shuffle,原位稳定合并。 计算机杂志。 43(1):40-53, (2000)。

另请参阅此处的各种讨论:

  1. https://cs.stackexchange.com/questions/332/in-place-algorithm-for-interleaving-an-array/400#400
  2. https://cstheory.stackexchange.com/questions/13943/linear-time-in-place-riffle-shuffle-algorithm.

【讨论】:

  • "我需要这样做而不创建额外的字符串变量或数组"
  • 它是就地交错。所以没有额外的内存/字符串/数组。此外,递归有效地使用了额外的内存(它只是一种创建大量变量的聪明方法,这是不允许的。)
【解决方案4】:

好的,让我们重新开始。下面是我们要做的:

def interleave(string):
    i = (len(string)/2) - 1
    j = i+1

    while(i > 0):
        k = i
        while(k < j):
            tmp = string[k]
            string[k] = string[k+1]
            string[k+1] = tmp
            k+=2 #increment by 2 since were swapping every OTHER character
        i-=1 #move lower bound by one
        j+=1 #move upper bound by one

这是程序将要执行的操作的示例。我们将使用变量i,j,kij 将分别是下限和上限,k 将是我们交换的索引。

示例

`abcd1234`

i = 3 //got this from (length(string)/2) -1

j = 4 //this is really i+1 to begin with

k = 3 //k always starts off reset to whatever i is 

swap d and 1
increment k by 2 (k = 3 + 2 = 5), since k > j we stop swapping

result `abc1d234` after the first swap

i = 3 - 1 //decrement i
j = 4 + 1 //increment j
k= 2 //reset k to i

swap c and 1, increment k (k = 2 + 2 = 4), we can swap again since k < j
swap d and 2, increment k (k = 4 + 2 = 6), k > j so we stop
//notice at EACH SWAP, the swap is occurring at index `k` and `k+1`

result `ab1c2d34`

i = 2 - 1
j = 5 + 1
k = 1

swap b and 1, increment k (k = 1 + 2 = 3), k < j so continue
swap c and 2, increment k (k = 3 + 2 = 5), k < j so continue
swap d and 3, increment k (k = 5 + 2 = 7), k > j so were done

result `a1b2c3d4`

至于证明程序的正确性,请参见link。它解释了如何通过循环不变量来证明这是正确的。

粗略的证明如下:

  1. 初始化:在循环的第一次迭代之前,我们可以看到i 设置为 (length(string)/2) - 1。我们可以在进入循环之前看到 i
  2. 维护。每次迭代后,i 会递减 (i = i-1, i=i-2,...),并且必须有一个点 i&lt;length(string)
  3. 终止:由于i 是正整数的递减序列,循环不变量i &gt; 0 最终将等于false,循环将退出。

【讨论】:

  • 请阅读问题。它说“就地”交错。使用额外的字符串变量,问题很简单,我没有问。
  • 我的错,这也容易,让我来解决
  • 另外,解释一下你的解决方案。
  • 请稍等,我没有正确阅读问题,正在修复问题
  • 顺便说一句,您的粗略证明似乎只能证明您的算法终止。它没有显示正确性的证据。
【解决方案5】:

好的,这是一个粗略的草稿。你说你不只是想要一个算法,但你正在接受提示,所以把这个算法当作一个提示:

长度为 N。

k = N/2 - 1.

1) 从中间开始,将位置 N/2 k 处的元素向左移动(通过连续交换相邻的对元素)(第一次:'1' 移动到位置 1)。

2) --k. k==0吗?退出。

3) 将元素在 N/2 处(第一次:'f' 移动到位置 N-1)处向右移动(通过交换)k 个位置。

4) --k.

编辑:上面的算法是正确的,如下面的代码所示。实际上证明它是正确的已经超出了我的能力范围,不过这是一个有趣的小问题。

#include <iostream>
#include <algorithm>

int main(void)
{
    std::string s("abcdefghij1234567890");
    int N = s.size();
    int k = N/2 - 1;
    while (true)
    {

        for (int j=0; j<k; ++j)
        {
            int i = N/2 - j;
            std::swap(s[i], s[i-1]);
        }

        --k;

        if (k==0) break;

        for (int j=0; j<k; ++j)
        {
            int i = N/2 + j;
            std::swap(s[i], s[i+1]);
        }

        --k;
    }

   std::cout << s << std::endl;

   return 0;
}

【讨论】:

  • 您可以在 O(length²) 操作中移动元素,还是您正在这样做?
【解决方案6】:

一般来说,这个问题是相当困难的——它归结为寻找置换循环。根据长度的不同,它们的数量和长度变化很大。

第一个和最后一个循环总是退化的; 10 个条目数组有 2 个长度为 6 和 2 的循环,12 个条目数组有一个长度为 10 的循环。

有一个循环:

 for (i=j; next=get_next(i) != j; i=next) swap(i,next);

尽管函数 next 可以实现为一些相对简单的 N 公式,但问题被推迟到对已交换的索引进行账面核算。在左侧 10 个条目的情况下,应该 [快速] 找到循环的起始位置(例如 1 和 3)。

【讨论】:

    猜你喜欢
    • 2011-10-11
    • 1970-01-01
    • 2022-07-07
    • 1970-01-01
    • 2015-01-27
    • 2015-12-02
    • 1970-01-01
    • 2023-04-02
    • 2012-04-19
    相关资源
    最近更新 更多