【问题标题】:Variant on Sequence-Based enumeration of permutations基于序列的排列枚举的变体
【发布时间】:2019-02-07 02:15:48
【问题描述】:

这是对this thread 的跟进,主要问题是迭代数组的所有排列,即给定["a","b","c"],使用迭代器获得["bca","acb".. etc]

感谢Martin R 的见解,以及他在另一个thread 中的输入,我想出了使用迭代器的“基于序列的排列枚举”问题的另一种可能的解决方案。问题是我不确定我是否拥有所有排列,尽管有很好的迹象表明它们都在那里。该算法保证提供 n!最多排列,没有重复。

这种方法背后的想法如下,假设有一个数组a=["a","b","c"...],大小为n。列出所有排列可以看作是从包中挑选元素:

■
■
■
■       ■  
■       ■  ■
■ ...   ■  ■  ■

0 ...  n-3 n-2 n-1

所以算法采用初始数组,并删除一行,递归传递它,直到没有剩下的行。此时,如果可以找到迭代器,则可以独立处理所有单独的排列。迭代器隐藏在下面的FactorialSequence 中,其中next() 方法允许从相邻点移动。

public struct FactorialSequence : Sequence, IteratorProtocol {

    private var current: [Int]

    public init(size:Int) {
        self.current = Array(repeating: 0, count: size)
    }

    public mutating func next() -> [Int]? {
        return self.__next();
    }

    private mutating func __next() -> [Int]? {
        var next = current
        defer {
            current = next;
        }

        for i in self.current.indices.reversed() {
            if next[i] < current.count - i - 1  {
                next[i] += 1
                return next;
            }
            next[i] = 0
        }
        return nil
    }
}

func permute(seq:[String],at:[Int]) -> String? {
    if seq.count > 0 {
        var ss = seq;
        let uu = seq[at.first!]
        var cc = at;
        _ = ss.remove(at: cc.first!)
        _ = cc.remove(at: 0);
        return uu + (permute(seq:ss,at:cc) ?? "")
    }
    return nil ;
}

permute() 函数被称为传递从 FactorialSequence 计算的迭代器(一个数组):

var fs = FactorialSequence(size: 3)
print("\(fs.current):\(permute(seq:["a","b","c"], at: fs.current)!)")

while let uu = fs.next() {
    print("\(uu):\(permute(seq:["a","b","c"], at: uu)!)")
}

并给出(扁平字符串格式):

   [-0.000][-0.000][171]    [0, 0, 0]:abc
   [0.0009][0.0009][174]    [0, 1, 0]:acb
   [0.0016][0.0007][174]    [1, 0, 0]:bac
   [0.0024][0.0008][174]    [1, 1, 0]:bca
   [0.0032][0.0008][174]    [2, 0, 0]:cab
   [0.0040][0.0008][174]    [2, 1, 0]:cba

关于“无重复”的注意事项:由于使用数组(迭代器)访问排列,如果两个迭代器相差一个元素,则它们指向两个不同的排列。虽然有点薄,但我认为这是没有重复的论据。

剩下的唯一问题是“他们都在那里吗?”。可以说有 n!指向给定排列的不同数组,但我不太确定该论点的有效性,因为它来自“绘图”......欢迎指针。

我没有彻底清理 SO 来检查这是否已经以这种方式或类似方式制定(尽管原始线程中的链接使用其他方法)。抱歉,如果这样做了。

【问题讨论】:

  • 那么,您得到的结果数量是否正确,它们是唯一的吗?这应该不难检查。
  • 到目前为止,它们已达到我测试的极限,但这几乎不能证明。这篇文章的目的是询问是否有任何通用证明,因为我怀疑上述方法肯定是以前使用过的。我只是找不到在哪里。

标签: arrays swift algorithm permutation


【解决方案1】:

对于给定大小的NFactorialSequence 生成所有数组的序列

[ i.0, i.1, ..., i.(N-1) ]

这样

0 <= i.0 < N, 0 <= i.1 < N-1, ..., 0 <= i.(N-1) < 1

正是

N * (N-1) * ... * 1 = N!

元素。然后permute() 函数选择索引为i.0 的元素 从带有N 元素的给定数组,然后是带有i.1 的元素 剩余的N-1 元素,以此类推。

所以是的,这确实产生了数组的所有可能排列。

但是,代码可以简化一点。一、FactorialSequence 是否返回初始数组[ 0, ..., 0 ],对应于 恒等排列。似乎还有单独的__next() 方法 没必要。

如果我们把代码改成

public struct FactorialSequence : Sequence, IteratorProtocol {

    private var current: [Int]
    private var firstIteration = true

    public init(size:Int) {
        self.current = Array(repeating: 0, count: size)
    }

    public mutating func next() -> [Int]? {
        if firstIteration {
            firstIteration = false
            return current
        }

        for i in self.current.indices.reversed() {
            if current[i] < current.count - i - 1  {
                current[i] += 1
                return current;
            }
            current[i] = 0
        }
        return nil
    }
}

然后返回所有排列(包括初始标识),并且 defer 语句不再需要。

permute() 函数可以稍微简化一下,变成通用的:

func permute<E>(array: [E], at: [Int]) -> [E] {
    if at.isEmpty { return [] }
    var at = at
    var array = array
    let firstIndex = at.remove(at: 0)
    let firstElement = array.remove(at: firstIndex)
    return [firstElement] + permute(array: array, at: at)
}

现在

let array = ["a", "b", "c"]
let fs = FactorialSequence(size: 3)
for p in fs {
    print(permute(array: array, at: p).joined())
}

产生预期的输出。

但是请注意,permute() 会产生很多中间数组, 因此我假设它比其他方法效率低 你引用的那个。

另一种方法是将选择的元素交换到它的新元素 地方,这避免了递归和临时数组:

func permute<E>(array: [E], at: [Int]) -> [E] {
    var result = array
    for (i, j) in at.enumerated() {
        result.swapAt(i, i + j)
    }
    return result
}

(不过,它以不同的顺序产生排列。)

【讨论】:

  • 感谢您的反馈,非常感谢。是的,它的效率要低得多(至少比您提到的方法慢两倍),并且会创建大量中间数组(准确地说是 n!,而一次只有 n 处于活动状态),这就是我最初制作的原因字符串格式的排列。不过这个方法更容易理解。我想知道是否没有办法创建一个主数组,从中获取所有子数组?确实是内存分配扼杀了算法......
  • 实际上,我很确定有更好的方法......如果我们看一下算法,它会在排列和迭代器之间产生双射。在步骤“i”它告诉数组的“哪个”元素要跳过,因为该元素在步骤“i - 1”中被“删除”,以及选择哪个元素来填充排列。因此,如果我们能找到一种方法将这两条信息从一个步骤“携带”到下一步,我认为不需要中间数组。这 + 一个 LinkedList 可以完成 IMO 的工作。你怎么看?
  • @Alex:我添加了一个不需要临时数组的变体。我确信存在更多好的方法。
  • 天啊……这个算法以前有没有被编码过?如果不是,我认为你应该给它起一个醒目的名字,并将其发布在代码审查上。也许有一些更“明显”的东西,但我怀疑更优化。您的代码需要 ~nn!查找所有排列的操作,而 Knuth 需要 ~3nn! (最坏的情况)
  • @Alex:我确信这不是新的。 – 另请注意,如果数组元素并非全部不同,则 Knuth 方法与此方法不同:对于 [1, 1, 2],Knuth 方法仅产生“不同”排列 [1, 1, 2], [1, 2 , 1], [2, 1, 1],而此方法产生所有 6 个排列。 – 我还建议对大型数组进行一些基准测试,以确定哪种方法更有效。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-12-31
  • 2019-05-12
  • 1970-01-01
  • 2017-07-21
  • 1970-01-01
  • 2019-02-01
  • 2015-05-02
相关资源
最近更新 更多