【问题标题】:Trouble with a complicated recursive algorithm复杂的递归算法的麻烦
【发布时间】:2020-05-29 00:28:14
【问题描述】:

我无法为我正在尝试编写的方法获取正确的逻辑,我认为这可能是一个有趣的问题,让其他人看看。目标语言是 Java,但我只是笼统地介绍它,以免阻止任何人分享他们的想法。

该方法的输入是任意数量的分层数据集,输出是该数据成员组合的列表,其中每个组合都有来自每组分层数据的一个成员。但是,不包括所有可能的组合。构建组合的关键是给定元素是否是层次结构中的叶子成员,并且每个集合的每个元素都知道它的名称以及它是否是叶子。首先,我将举一个例子,然后我将尝试阐明规则。具体而言,我们将考虑输入是以下三个集合:

A1       B1            C1
  A10      B10           C10
  A11         B100       C11
              B101       C12
              B102       C13
              B103
              B104
              B105
           B11
              B110
              B111
              B112
              B113
              B114

相应的输出如下所示:

(1)   A1  B1   C1        (37)  A10 B111 C11    (73)   A11 B103 C10  
(2)   A10 B1   C1        (38)  A10 B111 C12    (74)   A11 B103 C11 
(3)   A10 B10  C1        (39)  A10 B111 C13    (75)   A11 B103 C12 
(4)   A10 B100 C1        (40)  A10 B112 C1     (76)   A11 B103 C13 
(5)   A10 B100 C10       (41)  A10 B112 C10    (77)   A11 B104 C1    
(6)   A10 B100 C11       (42)  A10 B112 C11    (78)   A11 B104 C10 
(7)   A10 B100 C12       (43)  A10 B112 C12    (79)   A11 B104 C11 
(8)   A10 B100 C13       (44)  A10 B112 C13    (80)   A11 B104 C12 
(9)   A10 B101 C1        (45)  A10 B113 C1     (81)   A11 B104 C13 
(10)  A10 B101 C10       (46)  A10 B113 C10    (82)   A11 B11 C1  
(11)  A10 B101 C11       (47)  A10 B113 C11    (83)   A11 B110 C1    
(12)  A10 B101 C12       (48)  A10 B113 C12    (84)   A11 B110 C10 
(13)  A10 B101 C13       (49)  A10 B113 C13    (85)   A11 B110 C11 
(14)  A10 B102 C1        (50)  A10 B114 C1     (86)   A11 B110 C12 
(15)  A10 B102 C10       (51)  A10 B114 C10    (87)   A11 B110 C13 
(16)  A10 B102 C11       (52)  A10 B114 C11    (88)   A11 B111 C1    
(17)  A10 B102 C12       (53)  A10 B114 C12    (89)   A11 B111 C10 
(18)  A10 B102 C13       (54)  A10 B114 C13    (90)   A11 B111 C11 
(19)  A10 B103 C1        (55)  A11 B1 C1       (91)   A11 B111 C12 
(20)  A10 B103 C10       (56)  A11 B10 C1     (92)   A11 B111 C13 
(21)  A10 B103 C11       (57)  A11 B100 C1     (93)   A11 B112 C1    
(22)  A10 B103 C12       (58)  A11 B100 C10    (94)   A11 B112 C10 
(23)  A10 B103 C13       (59)  A11 B100 C11    (95)   A11 B112 C11 
(24)  A10 B104 C1        (60)  A11 B100 C12    (96)   A11 B112 C12 
(25)  A10 B104 C10       (61)  A11 B100 C13    (97)   A11 B112 C13 
(26)  A10 B104 C11       (62)  A11 B101 C1     (98)   A11 B113 C1    
(27)  A10 B104 C12       (63)  A11 B101 C10    (99)   A11 B113 C10 
(28)  A10 B104 C13       (64)  A11 B101 C11    (100)  A11 B113 C11
(29)  A10 B11  C1        (65)  A11 B101 C12    (101)  A11 B113 C12
(30)  A10 B110 C1        (66)  A11 B101 C13    (102)  A11 B113 C13
(31)  A10 B110 C10       (67)  A11 B102 C1     (103)  A11 B114 C1   
(32)  A10 B110 C11       (68)  A11 B102 C10    (104)  A11 B114 C10
(33)  A10 B110 C12       (69)  A11 B102 C11    (105)  A11 B114 C11
(34)  A10 B110 C13       (70)  A11 B102 C12    (106)  A11 B114 C12
(35)  A10 B111 C1        (71)  A11 B102 C13    (107)  A11 B114 C13
(36)  A10 B111 C10       (72)  A11 B103 C1    

虽然我清楚地理解了模式,但我很难清楚地阐明规则,原因与我很难编写方法一样。基本上,如果一个 elemnet 是一个非叶子,它只用于构建与从集合到它的正确的根元素的组合。相反,如果一个元素是一片叶子,它会与它正确的所有可能的组合交叉。显然问题是递归的,我觉得伪代码应该是这样的:

buildCombinations(element, setNumber, numberOfSets)
{
  if (element.IsLeaf( ))
  {
    List list = buildCombinations(elementFromNextList, setNumber++, numberOfSets)
    CrossJon element with list
  }
  else
  {
    IfOnlyIKnew!
  }
}

但我已经盯着它看了两天,我就是无法完全理解它。我怀疑我没有做足够的工作来解释这种情况,所以请随时提出问题以进行澄清。无论如何,这让我觉得这个问题对于比我聪明一点的人来说可能很明显,如果有人能提供任何建议,我会很高兴的。

提前致谢!

【问题讨论】:

  • 什么数据结构保存嵌套列表/树/什么?我觉得伪代码抽象有点混淆了像这样的一些看似重要的细节。最终输出是一个数组/列表?
  • A11 East C1 包含East?
  • 东应该是 B10。我已经修复了帖子。好眼力!
  • 我对ggorlen的问题写了一个详细的回复,但是太长了,被踢了回来。 (仍然在 stackoverfow 中找到我的方法) 简短版本:每个集合都是 MyElement 实例的 ArrayList,其中每个 MyElement 只有两个属性,name 和 isLeaf。并且输出可以是一个字符串数组的 ArrayList,其中每个字符串数组有 3 个元素。

标签: algorithm recursion


【解决方案1】:

您忘记在输出中包含B105。这将在输出中增加 10 行 -

A10 B105 C1
A10 B105 C10
A10 B105 C11
A10 B105 C12
A10 B105 C13

A11 B105 C1
A11 B105 C10
A11 B105 C11
A11 B105 C12
A11 B105 C13

撇开这个错误不谈,这个问题与cartesian product 有一些小区别,cartesian product 有许多众所周知的算法。

关键区别在于leaf 节点永远不会出现在 tree 节点之后。一种简单的算法会生成所有树节点的笛卡尔积,然后删除leafs 出现在trees 之后的那些。

但我们可以做得更好。当遇到tree 节点时,我们可以智能地向前跳过,而不是浪费地访问将从输出中删除的路径 -


JavaScript

这是一个使用 JavaScript 的快速实现 -

const traverse = ([ t, ...more ], r = []) =>
  // base case: empty t
  t === undefined
    ? [ r ]

  // inductive: t.leaf
  : t.leaf
      ? traverse(more, [...r, t.value])

  // inductive: t.tree
  : [ [ ...r, t.value, ...more.map(t => t.value) ] // <--
    , ...t.children.flatMap(c => traverse([ c, ...more ], r))
    ]

traverse 函数的输入是 leaftree 节点的数组 -

const leaf = (value) =>
  ({ leaf, value })

const tree = (value, ...children) =>
  ({ tree, value, children })

鉴于您原始帖子中的树木 -

A1       B1            C1
  A10      B10           C10
  A11         B100       C11
              B101       C12
              B102       C13
              B103
              B104
              B105
           B11
              B110
              B111
              B112
              B113
              B114

我们可以像这样使用treeleaf 来表示它们-

const treeA =
  tree("A1", leaf("A10"), leaf("A11"))

const treeB =
  tree
    ( "B1"
    , tree("B10", leaf("B100"), leaf("B101"), leaf("B102"), leaf("B103"), leaf("B104"), leaf("B105"))
    , tree("B11", leaf("B110"), leaf("B111"), leaf("B112"), leaf("B113"), leaf("B114"))
    )

const treeC =
  tree("C1", leaf("C10"), leaf("C11"), leaf("C12"), leaf("C13"))
const result =
  traverse([ treeA, treeB, treeC ]) // [ [ "A1 B1 C1" ], [ ... ], ... ]
    .map(path => path.join(" "))    // [ "A1 B1 C1", "A10 B1 C1", ... ]

console.log(result.join("\n"))  // "A1 B1 C1\nA10 B1 C1\n..."
console.log(result.length)      // 117

展开下面的sn-p,在浏览器中验证结果-

const leaf = (value) =>
  ({ leaf, value })

const tree = (value, ...children) =>
  ({ tree, value, children })

const traverse = ([ t, ...more ], r = []) =>
  // base case: empty t
  t === undefined
    ? [ r ]

  // inductive: t.leaf
  : t.leaf
      ? traverse(more, [...r, t.value])

  // inductive: t.tree
  : [ [ ...r, t.value, ...more.map(t => t.value) ]
    , ...t.children.flatMap(c => traverse([ c, ...more ], r))
    ]

const treeA =
  tree("A1", leaf("A10"), leaf("A11"))

const treeB =
  tree
    ( "B1"
    , tree("B10", leaf("B100"), leaf("B101"), leaf("B102"), leaf("B103"), leaf("B104"), leaf("B105"))
    , tree("B11", leaf("B110"), leaf("B111"), leaf("B112"), leaf("B113"), leaf("B114"))
    )

const treeC =
  tree("C1", leaf("C10"), leaf("C11"), leaf("C12"), leaf("C13"))

const result =
  traverse([ treeA, treeB, treeC ])
    .map(path => path.join(" "))

console.log(result.join("\n"))
console.log(result.length)

输出 -

A1 B1 C1
A10 B1 C1
A10 B10 C1
A10 B100 C1
A10 B100 C10
A10 B100 C11
A10 B100 C12
A10 B100 C13
A10 B101 C1
A10 B101 C10
A10 B101 C11
A10 B101 C12
A10 B101 C13
A10 B102 C1
A10 B102 C10
A10 B102 C11
A10 B102 C12
A10 B102 C13
A10 B103 C1
A10 B103 C10
A10 B103 C11
A10 B103 C12
A10 B103 C13
A10 B104 C1
A10 B104 C10
A10 B104 C11
A10 B104 C12
A10 B104 C13
A10 B105 C1
A10 B105 C10
A10 B105 C11
A10 B105 C12
A10 B105 C13
A10 B11 C1
A10 B110 C1
A10 B110 C10
A10 B110 C11
A10 B110 C12
A10 B110 C13
A10 B111 C1
A10 B111 C10
A10 B111 C11
A10 B111 C12
A10 B111 C13
A10 B112 C1
A10 B112 C10
A10 B112 C11
A10 B112 C12
A10 B112 C13
A10 B113 C1
A10 B113 C10
A10 B113 C11
A10 B113 C12
A10 B113 C13
A10 B114 C1
A10 B114 C10
A10 B114 C11
A10 B114 C12
A10 B114 C13
A11 B1 C1
A11 B10 C1
A11 B100 C1
A11 B100 C10
A11 B100 C11
A11 B100 C12
A11 B100 C13
A11 B101 C1
A11 B101 C10
A11 B101 C11
A11 B101 C12
A11 B101 C13
A11 B102 C1
A11 B102 C10
A11 B102 C11
A11 B102 C12
A11 B102 C13
A11 B103 C1
A11 B103 C10
A11 B103 C11
A11 B103 C12
A11 B103 C13
A11 B104 C1
A11 B104 C10
A11 B104 C11
A11 B104 C12
A11 B104 C13
A11 B105 C1
A11 B105 C10
A11 B105 C11
A11 B105 C12
A11 B105 C13
A11 B11 C1
A11 B110 C1
A11 B110 C10
A11 B110 C11
A11 B110 C12
A11 B110 C13
A11 B111 C1
A11 B111 C10
A11 B111 C11
A11 B111 C12
A11 B111 C13
A11 B112 C1
A11 B112 C10
A11 B112 C11
A11 B112 C12
A11 B112 C13
A11 B113 C1
A11 B113 C10
A11 B113 C11
A11 B113 C12
A11 B113 C13
A11 B114 C1
A11 B114 C10
A11 B114 C11
A11 B114 C12
A11 B114 C13
117       // results.length

从 JavaScript 到 Java

我不懂 Java,但我可以帮助重写上面的程序,用一种可能更容易翻译的方式 -

JavaScript                           Java
================================================================
Array.prototype.concat               List.AddAll, Stream.concat
Array.prototype.map                  Stream.map
Array.prototype.flatMap              Stream.flatMap
Array.prototype.join                 String.join
(c => ___)                           Lambda (c -> ___)
[ foo, ...bar ]                      (no equivalent, see below)
const traverse = (trees = []) =>
  // call traverseHelper with trees and initial path for all nodes, []
  traverseHelper(trees, [])
function traverseHelper ([ t, ...more ], r = [])
{ // when no t
  if (t === undefined)
    return [ r ]

  // when t.leaf
  else if (t.leaf)
    return traverseHelper(more, [ ...r, t.value ])

  // when t.tree
  else
  { // when t.tree is encountered, do not add t.value to the children paths
    // simply copy the .value of each tree to the path for this node
    const path = r.concat([ t.value ]).concat(more.map(t => t.value))

    // return the path for this node, combined wit the results for
    // the children nodes.
    return [ path ]
      .concat(t.children.flatMap(c => traverseHelper([ c, ...more ], r)))
  }
}

据我所知,Java 不支持spread syntax。这些只是常见数组操作的语法糖 -

function foo(x, y, ...z) {
  console.log("foo x:", String(x))
  console.log("foo y:", String(y))
  console.log("foo z:", String(z))
}

function bar([ x, ...more ]) {
  console.log("bar x:", String(x))
  console.log("bar more:", String(more))
}

const a = [1,2,3]
const b = 4
const c = [5,6]

foo(...a, b, ...c)   // <-- foo(1,2,3,4,5,6)
// x: 1
// y: 2
// z: 3,4,5,6

const d = [...a, b, ...c] // <-- [1,2,3,4,5,6]
bar(d)
// x: 1
// more: 2,3,4,5,6

球拍/方案

Racket 是讨论和分享算法的绝佳语言,因为它几乎没有语法,而且程序由简单的部分组成 -

#lang racket

(define (traverse ts (r null))
  (cond 
    ;; base case: empty
    ((null? ts)  
      (list r))

    ;; inductive: leaf
    ((null? (cdar ts)) 
      (traverse (cdr ts)
                (cons (caar ts) r)))

    ;; inductive: tree
    (else                
      (cons (append (reverse (map car ts)) r)
            (append-map (lambda (c)
                          (traverse (cons c (cdr ts)) r))
                        (cdar ts))))))

这几乎是对 JavaScript 代码的直接翻译,只是路径结果是以相反的顺序构造的。

(define a
  '(a1 (a10) (a11)))

(define b
  '(b1 (b10 (b100) (b101) (b102) (b103) (b104) (b105))
       (b11 (b110) (b111) (b112) (b113) (b114))))

(define c
  '(c1 (c10) (c11) (c12) (c13)))

(define result
  (map reverse (traverse (list a b c))))

result

(map reverse ...) 用于显示使用正确顺序的结果 -

'((a1 b1 c1)
  (a10 b1 c1)
  (a10 b10 c1)
  (a10 b100 c1)
  (a10 b100 c10)
  (a10 b100 c11)
  (a10 b100 c12)
  (a10 b100 c13)
  (a10 b101 c1)
  (a10 b101 c10)
  (a10 b101 c11)
  (a10 b101 c12)
  (a10 b101 c13)
  (a10 b102 c1)
  (a10 b102 c10)
  (a10 b102 c11)
  (a10 b102 c12)
  (a10 b102 c13)
  (a10 b103 c1)
  (a10 b103 c10)
  (a10 b103 c11)
  (a10 b103 c12)
  (a10 b103 c13)
  (a10 b104 c1)
  (a10 b104 c10)
  (a10 b104 c11)
  (a10 b104 c12)
  (a10 b104 c13)
  (a10 b105 c1)
  (a10 b105 c10)
  (a10 b105 c11)
  (a10 b105 c12)
  (a10 b105 c13)
  (a10 b11 c1)
  (a10 b110 c1)
  (a10 b110 c10)
  (a10 b110 c11)
  (a10 b110 c12)
  (a10 b110 c13)
  (a10 b111 c1)
  (a10 b111 c10)
  (a10 b111 c11)
  (a10 b111 c12)
  (a10 b111 c13)
  (a10 b112 c1)
  (a10 b112 c10)
  (a10 b112 c11)
  (a10 b112 c12)
  (a10 b112 c13)
  (a10 b113 c1)
  (a10 b113 c10)
  (a10 b113 c11)
  (a10 b113 c12)
  (a10 b113 c13)
  (a10 b114 c1)
  (a10 b114 c10)
  (a10 b114 c11)
  (a10 b114 c12)
  (a10 b114 c13)
  (a11 b1 c1)
  (a11 b10 c1)
  (a11 b100 c1)
  (a11 b100 c10)
  (a11 b100 c11)
  (a11 b100 c12)
  (a11 b100 c13)
  (a11 b101 c1)
  (a11 b101 c10)
  (a11 b101 c11)
  (a11 b101 c12)
  (a11 b101 c13)
  (a11 b102 c1)
  (a11 b102 c10)
  (a11 b102 c11)
  (a11 b102 c12)
  (a11 b102 c13)
  (a11 b103 c1)
  (a11 b103 c10)
  (a11 b103 c11)
  (a11 b103 c12)
  (a11 b103 c13)
  (a11 b104 c1)
  (a11 b104 c10)
  (a11 b104 c11)
  (a11 b104 c12)
  (a11 b104 c13)
  (a11 b105 c1)
  (a11 b105 c10)
  (a11 b105 c11)
  (a11 b105 c12)
  (a11 b105 c13)
  (a11 b11 c1)
  (a11 b110 c1)
  (a11 b110 c10)
  (a11 b110 c11)
  (a11 b110 c12)
  (a11 b110 c13)
  (a11 b111 c1)
  (a11 b111 c10)
  (a11 b111 c11)
  (a11 b111 c12)
  (a11 b111 c13)
  (a11 b112 c1)
  (a11 b112 c10)
  (a11 b112 c11)
  (a11 b112 c12)
  (a11 b112 c13)
  (a11 b113 c1)
  (a11 b113 c10)
  (a11 b113 c11)
  (a11 b113 c12)
  (a11 b113 c13)
  (a11 b114 c1)
  (a11 b114 c10)
  (a11 b114 c11)
  (a11 b114 c12)
  (a11 b114 c13))

【讨论】:

  • 谢谢!我无法告诉你我有多感激。我实际上曾考虑构建整个交叉产品并修剪结果,但实际代码涉及大量额外处理,包括每次构建组合时的数据库访问,因此为组合支付开销我无论如何都会扔掉没有意义.现在唯一的问题是我不知道任何javascript!但是,弄清楚正在发生的事情不应该做太多的工作。再次感谢,我真的很感激!
  • 很高兴为您提供帮助。如果您遇到任何问题,请告诉我,我会尽力回答后续问题:D
  • 所以,我花了几个小时学习 js 教程,我了解了很多代码,但不是全部。你能解释一下 t.tree 案例中发生了什么吗?具体来说,traverse函数返回什么?
  • @user2801442 与 Java 一样,JavaScript 是一种杂乱但用途广泛的语言。希望你学到了一些有趣的东西!我添加了一些关于将函数转换为 Java 的注释
  • 再次感谢。我终于得到了这个工作。当我在 javascript 中完成我的速成课程时,让我感到困惑的一件事是 ...s 是用作扩展运算符还是用作其余运算符。无论如何,我认为我现在理解了大约 90% 的 js,所以我会满意的!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-10-30
  • 2023-03-10
  • 2021-05-28
  • 2012-05-30
  • 2021-02-12
  • 2013-04-28
  • 2011-02-12
相关资源
最近更新 更多