我们正在写一个“计算器”。 第二部分 解决方程式,在LaTeX中渲染,加速函数以使其超轻

你好

因此,在第一部分中,我们已经做好了派生,简化和编译的工作。 那么,我们的简单计算器还能做什么呢? 好吧,至少等式的形式

xb tan sinx23 tan sinx+c=0

一定要决定。 提起皮套也很美,那就好了! 走吧





免责声明
尽管代码是在C#中给出的,但在这里却是如此之小,以至于不了解该语言或讨厌它不会使阅读复杂化。


编译加速


在最后一部分中,我们对函数进行了编译,以便可以对其进行一次编译,然后在将来运行多次。
所以,我们介绍一下功能

 sinx2+ cosx2+x2+ sinx2


在第一部分的末尾,此功能的速度如下:
方法时间(以纳秒为单位)
代替6800
我们的编译功能650
这是直接在代码中编写的函数。430

什么啊
替代-一种计算表达式值的经典方法,例如
var x = MathS.Var("x"); var expr = x + 3 * x; Console.WriteLine(expr.Substitute(x, 5).Eval()); >>> 20 

我们的编译函数是在执行相同的操作之后,但是在编译之后
 var x = MathS.Var("x"); var expr = x + 3 * x; var func = expr.Compile(x); Console.WriteLine(func.Substitute(5)); 

直接用代码编写的函数是
 static Complex MyFunc(Complex x) => x + 3 * x; 


我们可以看到,此功能中有重复的部分,例如, x2 ,最好将它们缓存起来。

为此,我们再介绍两个指令PULLCACHE和TOCACHE。 第一个将把高速缓存中的数字压入我们传递给堆栈的地址。 第二个也会堆栈中的最后一个数字复制stack.Peek() )到缓存中,同样位于特定地址。

剩下的就是要创建一个表,在编译期间我们将在其中编写用于缓存的函数。 我们不会缓存什么? 好吧,首先,一旦发生。 访问缓存的额外指令不好。 其次,过于简单的操作也无济于事。 好吧,例如,访问变量或数字。

在解释指令列表时,我们将有一个预先创建的用于缓存的数组。 现在,此功能的说明如下

 PUSHCONST (2, 0) PUSHVAR 0 CALL powf TOCACHE 0 #   ,         , -  . CALL sinf TOCACHE 1 #      PULLCACHE 0 # ,   .    PULLCACHE 0 CALL cosf PULLCACHE 1 CALL sumf CALL sumf CALL sumf 


之后,我们得到了明显更好的结果:
方法时间(以纳秒为单位)
替代6800
我们的编译功能330 (原为650)
这是直接在代码中编写的函数。430

在萝卜中,指令的编译和解释都在此处实现。

乳胶


这是一种众所周知的格式,用于记录数学公式(尽管不仅如此!),还可以将其渲染成精美的图片。 它也位于中心,我编写的所有公式都以此格式编写。

拥有表达式树使在乳胶中渲染非常简单。 在逻辑上该怎么做? 因此,我们有了树的顶部。 如果它是数字或变量,那么一切都很简单。 如果该顶点(例如除法运算符),我们希望更多  fracaba/b ,因此对于除法,我们写类似

 public static class Div { internal static string Latex(List<Entity> args) => @"\frac{" + args[0].Latexise() + "}{" + args[1].Latexise() + "}"; } 


正如我们所见,一切都非常简单。 我在实施过程中遇到的唯一问题是不清楚如何放置括号。 如果仅将它们推到每个运算符上,我们将得到这样的废话:

\左\左\左\左x+3\右+\左a+b\右\右\右+c\右


但是,细心的人会立即(而不是像我一样)注意到,如果将其完全删除,则在构造该形式的表达式时 ca+b ,我们将打印

ca+b



简单地解决,我们输入运营商的优先级
 args[0].Latexise(args[0].Priority < Const.PRIOR_MUL) + "*" + args[1].Latexise(args[1].Priority < Const.PRIOR_MUL); 

瞧,你真漂亮。

乳胶化在这里完成。 乳胶这个词不存在,我是自己发明的,请不要踢我。

方程解


实际上,从数学的角度来看,您无法编写找到某个方程式所有解的算法。 因此,我们希望找到尽可能多的不同根源,以实现最终目标的不可实现性。 它包括两个部分:数值解(一切都尽可能简单)和解析性(但这就是历史)。

数值解。 牛顿法


他非常简单,知道功能 fx 我们将使用迭代公式搜索根

xn+1=xn fracfxfx


由于我们都在复杂的平面中求解,因此我们基本上可以编写一个二维循环,该循环将寻找解决方案,然后返回唯一的解决方案。 此外,我们现在可以解析地找到函数的导数,然后找到两个函数 fxfx 编译。

牛顿坐在这里

分析溶液


最初的想法很明显。 所以,表达式,等式的根 axbx 根总数相等 axbx ,与除法类似:

 internal static void Solve(Entity expr, VariableEntity x, EntitySet dst) { if (expr is OperatorEntity) { switch (expr.Name) { case "mul": Solve(expr.Children[0], x, dst); Solve(expr.Children[1], x, dst); return; case "div": Solve(expr.Children[0], x, dst); return; } } ... 


对于正弦,这看起来会有些不同:
 case "sinf": Solve(expr.Children[0] + "n" * MathS.pi, x, dst); return; 

毕竟,我们要查找所有的根,而不仅仅是找到0的根。

在确保当前表达式不是乘积,并且不是其他易于简化的运算符和函数之后,我们需要尝试找到一个用于求解方程的模板。

第一个想法是使用我们制作的模式简化表达式。 实际上,我们大约需要这个,但是首先我们需要进行变量替换。 实际上,方程式

 sinx2+ sinx0.4=0


没有模板,但对

\开casest2+t0.4=0t= sinx endcases


即使它存在。

因此,我们创建了GetMinimumSubtree类型的函数,该函数将返回最有效的变量替换。 什么是有效的替代品? 这是我们的替代品
  1. 最大限度地使用这种替代品
  2. 最大化树的深度(以便等式中  sin sinx2+3 我们有一个替换 \罪\罪x
  3. 我们确保通过这种替换,我们替换对变量的所有引用,例如,如果等式中  sinx2+ sinx+x 我们将更换 t=\罪x 那么一切都会变糟。 因此,在此等式中,最佳替代 x 是的 x (即没有好的替代品),但例如  sinx2+ sinx+c 我们可以安全地进行替换 t=\罪x

替换方程后,看起来简单得多。

多项式


所以,我们要做的第一件事,就是做expr.Expand() -打开所有括号,以消除表格的内容 xx+2 变成 x2+2x 。 现在,在披露之后,我们得到类似

c+x3+3x2ax3+x


不规范吗? 然后,我们首先收集有关每个单项式的信息,应用“线性子项”(在一篇文章中)并扩展所有术语。

我们对单项式有什么看法? 单项式是因子的乘积,因子之一是变量,或者是变量和整数的次数的算符。 因此,我们将介绍两个变量,一个具有度,另一个具有系数。 接下来,请仔细检查各个因素,每次我们确保 x 在一定程度上或根本没有程度。 如果遇到byaku-返回null。
比卡
如果我们突然遇见了 x3.2logx2 和其他提到的东西 x ,但不适合单项式-这很不好。


好的,我们已经编译了一个字典,其中的键是度(整数),值是一个单项系数。 这是上一个示例的样子:
 0 => c 1 => 1 2 => 3 3 => 1 - a 


这里,例如,二次方程解的实现
 if (powers[powers.Count - 1] == 2) { var a = GetMonomialByPower(2); var b = GetMonomialByPower(1); var c = GetMonomialByPower(0); var D = MathS.Sqr(b) - 4 * a * c; res.Add((-b - MathS.Sqrt(D)) / (2 * a)); res.Add((-b + MathS.Sqrt(D)) / (2 * a)); return res; } 

这一点尚未完成(例如,对于三次多项式,您需要正确完成此操作),但通常会在此处发生。

反向扫描


所以,这里我们做了一些类型替换 t=\反x3+a2 ,现在我们要从那里得到x。 在这里,我们只是一步一步地部署了功能和运算符,例如

t=\反x3+a2


 sint=x3+a2


 sinta2=x3


 sinta2 frac13=x



该代码段如下所示:
 switch (func.Name) { case "sinf": // sin(x) = value => x = arcsin(value) return FindInvertExpression(a, MathS.Arcsin(value), x); case "cosf": // cos(x) = value => x = arccos(value) return FindInvertExpression(a, MathS.Arccos(value), x); case "tanf": // tan(x) = value => x = arctan(value) return FindInvertExpression(a, MathS.Arctan(value), x); ... 

这些功能的代码在这里

一切,方程式的最终解(再见!)看起来像这样
  1. 如果我们知道零函数或运算符,请在此处发送求解(例如,如果 ab=0 然后运行求解(a)和求解(b))
  2. 寻找替代品
  3. 尽可能求解为多项式
  4. 对于所有解决方案,请部署替代产品以获得最终解决方案
  5. 如果作为多项式没有被求解, 我们只有一个变量,则可以通过牛顿法求解


因此,今天我们是:
  • 由于缓存而加快了编译功能
  • 在LaTeX中渲染
  • 我们解决了一小部分方程


因为它很简单 ,所以我可能不会谈论某个积分的数值计算。 我没有谈论解析,因为我在上一篇文章中对此有所批评。 这个想法是写你自己的。 但是,是否有必要讨论我们如何从字符串中解析表达式?
该项目在这里

在下一部分中...
如果其他人感兴趣,我们将尝试通过增加三次和四度以及更智能的折叠系统来使方程组的求解复杂化。 也许我们会选择模式,因为任何学生都会决定

1 sinx2+ cosx+0.5


但不是我们的算法。 此外,可能存在不定积分(开始)。 将数字扩展为四元数或矩阵尚无道理,但可以实现。 如果您对第三部分有特定的想法,请写下个人信息或评论。


关于项目
那些看过代码的人可能会注意到它是原始的而不是重构的。 而且,当然,我将进行重构,因此本文的主要思想是从理论上传达信息,然后再深入研究代码。 尽管我们可能仍然无法访问wolfram.alpha,但是拉请求请求是受欢迎的。 该项目已打开。


这是我们今天所做的几个例子
 var x = MathS.Var("x"); var y = MathS.Var("y"); var expr = x.Pow(y) + MathS.Sqrt(x + y / 4) * (6 / x); Console.WriteLine(expr.Latexise()); >>> {x}^{y}+\sqrt{x+\frac{y}{4}}*\frac{6}{x} 

xy+ sqrtx+ fracy4 frac6x

 var x = MathS.Var("x"); var expr = MathS.Sin(x) + MathS.Sqrt(x) / (MathS.Sqrt(x) + MathS.Cos(x)) + MathS.Pow(x, 3); var func = expr.Compile(x); Console.WriteLine(func.Substitute(3)); >>> 29.4752368584034 


 Entity expr = "(sin(x)2 - sin(x) + a)(b - x)((-3) * x + 2 + 3 * x ^ 2 + (x + (-3)) * x ^ 3)"; foreach (var root in expr.Solve("x")) Console.WriteLine(root); >>> arcsin((1 - sqrt(1 + (-4) * a)) / 2) >>> arcsin((1 + sqrt(1 + (-4) * a)) / 2) >>> b >>> 1 >>> 2 >>> i >>> -i 


感谢您的关注!

Source: https://habr.com/ru/post/zh-CN483294/


All Articles