Use the multinomial theorem (https://en.wikipedia.org/wiki/Multinomial_theorem) to expand the expression. * The expression must be a power of a sum of terms. * The power must be a positive integer. * - expr = '(a + b)^2' * -> 'a^2 + 2ab + b^2' * - expr = '(a + b)^3' * -> 'a^3 + 3a^2b +
(base: Expression, exp: number)
| 66 | * -> 'a^3 + 3a^2b + 3ab^2 + b^3' |
| 67 | */ |
| 68 | |
| 69 | function expandPower(base: Expression, exp: number): Expression | null { |
| 70 | const ce = base.engine; |
| 71 | if (exp < 0) { |
| 72 | const expr = expandPower(base, -exp); |
| 73 | return expr ? expr.inv() : null; |
| 74 | } |
| 75 | if (exp === 0) return ce.One; |
| 76 | if (exp === 1) return expand(base); |
| 77 | if (isFunction(base, 'Negate')) { |
| 78 | if (Number.isInteger(exp)) { |
| 79 | const sign = exp % 2 === 0 ? 1 : -1; |
| 80 | const result = expandPower(base.op1, exp); |
| 81 | if (result === null) return null; |
| 82 | return sign > 0 ? result : result.neg(); |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | // Subtract is non-canonical, so we don't expect to see it here. |
| 87 | console.assert(base.operator !== 'Subtract'); |
| 88 | |
| 89 | // We can expand only if the expression is a power of a sum. |
| 90 | if (!isFunction(base, 'Add')) return null; |
| 91 | |
| 92 | // Apply the multinomial theorem |
| 93 | // https://en.wikipedia.org/wiki/Multinomial_theorem |
| 94 | // (a + b + c)^n = sum_{k1 + k2 + ... + km = n} (n choose k1, k2, ..., km) a^k1 b^k2 ... c^km |
| 95 | // where the sum is over all non-negative integers k1, k2, ..., km such that k1 + k2 + ... + km = n |
| 96 | // and (n choose k1, k2, ..., km) = n! / (k1! k2! ... km!) |
| 97 | // For example, (a + b)^3 = (a + b)^2 (a + b) = (a^2 + 2ab + b^2) (a + b) = a^3 + 3a^2b + 3ab^2 + b^3 |
| 98 | // The multinomial theorem is a generalization of the binomial theorem. |
| 99 | // For example, (a + b)^2 = a^2 + 2ab + b^2 |
| 100 | // (a + b + c)^2 = (a + b + c) (a + b + c) = a^2 + b^2 + c^2 + 2ab + 2ac + 2bc |
| 101 | // (a + b + c)^3 = (a + b + c) (a + b + c) (a + b + c) = a^3 + b^3 + c^3 + 3a^2b + 3a^2c + 3b^2a + 3b^2c + 3c^2a + 3c^2b + 6abc |
| 102 | |
| 103 | const terms = base.ops; |
| 104 | const it = powers(terms.length, exp); |
| 105 | |
| 106 | const result: Expression[] = []; |
| 107 | const deadline = ce._deadline; |
| 108 | let count = 0; |
| 109 | for (const val of it) { |
| 110 | // A high power of a sum enumerates a multinomial with `C(exp+m-1, m-1)` |
| 111 | // terms — e.g. `(a+b+c)^350` is ~6·10⁴ — so this loop can run for many |
| 112 | // seconds. Checkpoint the engine deadline (stride-amortized) so a runaway |
| 113 | // expansion honors `ce.timeLimit` instead of stalling (e.g. the compiler's |
| 114 | // symbolic-antiderivative attempt on `(trinomial)^p`). |
| 115 | if ((++count & 0xff) === 0) checkDeadline(deadline); |
| 116 | const product = [ce.number(multinomialCoefficient(val))]; |
| 117 | for (let i = 0; i < val.length; i += 1) { |
| 118 | if (val[i] !== 0) { |
| 119 | if (val[i] === 1) product.push(terms[i]); |
| 120 | else product.push(terms[i].pow(val[i])); |
no test coverage detected