简化表达式树

5

我希望编写一个简化数学表达式的程序。

我已经编写了一个将字符串转换为二叉树的解析器。例如,(1+2)*x 会变成:

    *
   / \
  +   x
 / \
1   2

我简化此类树的想法如下: 存储一组树及其简化版本。 例如:
    *                       +
   / \                     / \
  a   +        and        *   *
     / \                 / \ / \
    b   c               a  b a  c

(其中a、b、c可以是任何子树) 如果我找到与存储的树之一匹配的子树,我将用其简化版本替换它。
必要时,我将重复此过程,直到树完全简化为止。
这种方法的问题在于,在某些情况下无法“合并类似项”。例如,如果我尝试存储以下树:
      +                *
     / \      and     / \
    x   x            2   x

接下来,当我尝试简化表达式 x+y+x 时,可以使用以下树形结构:

    +
   / \
  x   +
     / \
    y   x  

它不会被简化为2x+y,因为子树存在

      +     
     / \     
    x   x      

该元素未包含在树中,因此树将不会被简化。

我尝试编写一个显式算法来合并类似项,但需要考虑的情况太多了。

请问有谁能帮我找到解决这个问题的方法吗?


处理这个问题最常见的方法是手动编写一堆启发式规则来简化 +*,但正确的方法可能是允许回溯,并比较表达式的可优先性,如果找到两个或更多简化形式。您将希望能够枚举从初始表达式到简化形式的可能路径,允许备份并沿着不适用任何内容的不同路径向下走(这是回溯部分)。 - Robert Dodier
并非每个操作都会导致结果;在没有指导(即启发式)的情况下,您所能做的就是尝试所有已知的规则,并查看是否在几步之后有任何不同。枚举+回溯+优先级评估的目的是将该过程形式化。 - Robert Dodier
2个回答

3
这是计算机代数系统中使用的基本思想之一。
对于像Plus(+)和Times(*)这样的运算符,您可以定义属性,例如Flat结合律)和Orderless交换律)。还应将PlusTimes定义为“多参数”运算符,而不是“二元”运算符。
因此,输入如下:
Plus(x,Plus(y,x))

在第一步中,可以通过Flat属性进行转换(展平)。

Plus(x,y,x)

在下一步中,由于Orderless属性的存在,它可以被转换(排序)为

Plus(x,x,y)

在你的“评估”步骤中,现在可以浏览参数并将表达式“简化”为:
Plus(Times(2,x),y)

这种方法的优点是,具有“结构相等”的表达式存储在同一“规范形式”中,例如可以更容易地在使用的编程语言中进行“对象相等”比较。


我最终采用了类似的方法。我按照你的建议创建了“非二叉树”,但是我没有对操作数进行排序并将树转换回二叉树,而是编写了一个算法,在“非二叉树”中显式地合并相似项。你的方法更好吗? - Freud
我编辑了“方法的优势”答案。 - axelclk

1

我们可以考虑多项式。

对于在X中的两个多项式,我们可以得到'+'-reducer'*'-reducer

现在,在树中,我们可以考虑一个“不可约”多项式,而不是将标量或x作为节点考虑。

然后,如果节点运算符是*,我们应用'*'-reducer,否则应用'+'-reducer,这两者都将两个不可约多项式转换为新的不可约多项式。

例如,其中P_aP_b是两个多项式。

P_a = {
  x0: 1 // term of degree 0 idem 1
  x1: 2 // 2x
  x3: 4 // 4x^3
}

并且 P_b = {x1: 3}

我们得到的求和结果为:P_a + P_b = {x0: 1, x1: 5, x3: 4}

(因此树 ['+', P_a, P_b] 的简化形式为 {x: 0, x1: 5, x3: 4})

我们得到的乘积结果为:P_a * P_b = {x1: 3, x2: 6, x3: 12}

最终,我们得到了一个在 X 中不可约的多项式。

我们可以将该多项式写成二叉树的形式(也就是简化后的树):

对于每个单项式(在 X^i 中),写出其关联的二叉树(该二叉树仅包含 * 运算符) 例如:5x^3 => ['*', ['*', ['*', x, x], x], 5] 然后相加 例如:1 + x + x^2 => ['+', 1, ['*', x, 1], ['*', x, x]

同样的想法(实现'+'-reducer/'*'-reducer)可以应用于具有XYZ多项式的表达式,或者任何其他表达式(因此在您的情况下,xy)。

下面是一个实现示例(您可以取消注释并使用nodejs通过测试)

// a, b are polynomes of form {monomialKey: scalar, monomialKey2, scalar2, ...}
// a monomial key is e.g x1y2z2
const add = (a, b) => {
  const out = Object.assign({}, a)
  Object.entries(b).forEach(([monomialKey, scalar]) => {
    out[monomialKey] = (out[monomialKey] || 0) + scalar
    if (out[monomialKey] === 0) {
      delete out[monomialKey]
    }
  })
  return out
}
// transforms x1y2z2 to {x: 1, y: 2, z: 2}
const parseKey = s => s.match(/[a-z]+\d+/g).reduce((o, kv) => {
  const [,varname,deg] = kv.match(/([a-z]+)(\d+)/)
  o[varname] = parseInt(deg)
  return o
}, {})
const writeKey = o => Object.entries(o).reduce((s, [varname, deg]) => s + varname+deg, '')

// simplify monomial, e.g x1y3*x1 => x2y3
const timesMonomialKey = (iA, iB) => {
  const a = parseKey(iA)
  const b = parseKey(iB)
  const out = {}
  ;[a,b].forEach(x => Object.entries(x).forEach(([varname, deg]) => {
    if (deg === 0) return
    out[varname] = (out[varname] || 0) + deg
  }))
  if (Object.keys(out).length === 0) return writeKey({ x: 0 })
  return writeKey(out)
}
// a, b both polynomes
const times = (a, b) => {
  const out = {}
  Object.entries(a).forEach(([monimalKeyA, sA]) => {
    Object.entries(b).forEach(([monimalKeyB, sB]) => {
      const key = timesMonomialKey(monimalKeyA, monimalKeyB)
      out[key] = (out[key] || 0) + sA * sB
      if (out[key] === 0) {
        delete out[key]
      }
    })
  })
  return out
}


const reduceTree = t => { // of the form [operator, left, right] or val
  if (!Array.isArray(t)) {
    return typeof(t) === 'string'
      ? { [writeKey({ [t]: 1 })]: 1 } // x => {x1: 1}
      : { [writeKey({ x: 0 })]: t }  // 5 => {x0: 5}
  }
  const [op, leftTree, rightTree] = t
  const left = reduceTree(leftTree)
  const right = reduceTree(rightTree)
  return op === '+' ? add(left, right) : times(left, right)
}
const writePolynomial = o => {
  const writeMonomial = ([key, s]) => {
    const a = parseKey(key)
    const factors = Object.entries(a).flatMap(([varname, deg]) => {
      return Array.from({length: deg}).fill(varname)
    }).concat(s !== 1 ? s : [])
    return factors.reduce((t, next) => ['*', t, next])
  }
  if (Object.keys(o).length === 0) return 0
  return Object.entries(o).map(writeMonomial).reduce((t, next) => ['+', t, next])
}
console.log(writePolynomial(reduceTree(['+', ['+', 'x', 'y'], 'x'])))
//const assert = require('assert')
//assert.deepEqual(parseKey('x0y2z3'), { x: 0, y: 2, z: 3 })
//assert.deepEqual(writeKey({ x: 0, y: 2, z: 3 }), 'x0y2z3')
//assert.deepEqual(timesMonomialKey('x1y2', 'x3z1'), 'x4y2z1')
//assert.deepEqual(timesMonomialKey('x0y0', 'z0'), 'x0')
//assert.deepEqual(timesMonomialKey('x0y0', 'z0x1'), 'x1')
//assert.deepEqual(add({x0: 3, x1: 2}, {x0: 4, x3: 5}), {x0: 7, x1: 2, x3: 5})
//assert.deepEqual(add({x0: 3, y1: 2}, {x0: 4, y2: 5}), {x0: 7, y1: 2, y2: 5})
//assert.deepEqual(add({x0: 1}, {x0: -1}), {})
//assert.deepEqual(times({x0: 3, x1: 2}, {x0: 4, x1: 5}), {x0: 12, x1: 23, x2: 10})
//assert.deepEqual(times(
//  {x1y0: 3, x1y1: 2},
//  {x1y0: 4, x1y1: 5}),
//  {x2: 12, x2y1: 23, x2y2: 10}
//)
//assert.deepEqual(reduceTree('x'), {x1: 1})
//assert.deepEqual(reduceTree(['*', 2, 'x']), {x1: 2})
//assert.deepEqual(reduceTree(['+', 2, 'x']), {x0: 2, x1: 1})
//assert.deepEqual(reduceTree(['+', 'x', ['+', 'y', 'x']]), {x1: 2, y1: 1})
//assert.deepEqual(writePolynomial({ x1y1:1, x1y2: 2}), ['+', ['*', 'x', 'y'], ['*', ['*', ['*', 'x', 'y'], 'y'], 2]])
//assert.deepEqual(writePolynomial(reduceTree(['*', ['*', 'x', 'y'], 0])), 0)
//assert.deepEqual(writePolynomial(reduceTree(['+', ['*', ['*', 'x', 'y'], 0], 2])), 2)
//
//// finally your example :)
//assert.deepEqual(writePolynomial(reduceTree(['+', ['+', 'x', 'y'], 'x'])), ['+', ['*', 'x', 2], 'y'])


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