自动化递归计算

1.简介


毫不夸张地说,子例程(英语子例程)对编程的影响是巨大的。它们是在编程之初引入的,它们并没有失去与今天的联系。没有它们,实际的编程是无法想象的。尽管从正式的角度来看,它们并不是必需的,因为纯理论对算法的属性比其大小更感兴趣。

在自动机理论中,很少讨论嵌套自动机的概念,自动机子程序(AMP)的实践将以此为基础。如果考虑类似的(嵌套)自动机层次结构,则是非常肤浅的。这种关系的原因之一可能是在硬件级别[1、2]上实现嵌套层次结构的复杂性。

与设计数字电路的实践相比,编程更灵活,并且提供更多功能。我们必须确保这一点,还要进一步考虑嵌套自动机的软件实现,然后再考虑自动机递归算法的概念。

对于形成嵌套自动机模型的所有特定问题,其形式定义都不会引起任何问题。但是,另一方面,选择构建模型层次结构肯定会对其软件实现产生重大影响。

2.自动递归算法


在上一篇文章[3]中,仅给出了自动程序(AP)控制模型的正式定义,而没有考虑其实现和使用的特定示例。还提到了实现递归算法的可能性。接下来,使用阶乘计算示例,首先,我们将考虑在对象自动机C ++范例框架内创建自动例程的机制的软件实现,其次,由于我们将考虑递归算法,因此我们将本质上定义实现任何此类例程的通用原理自动编程框架中的算法。

在API中使用递归函数可能非常简单。这演示了列出目标机器程序1的代码。此处的FSimpleFactorial自动机类不是进程,而是自动机例程,因为 包含最终状态“ 00”(有关子程序的更多详细信息,请参见[3])。在行为级别上,创建的自动机对象对应于具有从初始状态“ f0”到最终状态“ 00”的一个无条件转换的自动机,并在通常的阶乘阶乘函数(阶乘(...))的动作y1内调用此转换。

清单1.从AMS调用阶乘函数
#include "lfsaappl.h"

class FSimpleFactorial : public LFsaAppl
{
public:
    double dF{1};		//	 
    FSimpleFactorial(int n);
    virtual ~FSimpleFactorial() {};
private:
    int nN{0};
    void y1();
    double factorial(int n);
};

#include "stdafx.h"
#include "FSimpleFactorial.h"

LArc TT_SimpleFactorial[] = {
    LArc("f0", "00", "--",	"y1"),		//
    LArc()
};
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
FSimpleFactorial::FSimpleFactorial(int n)
    :LFsaAppl(TT_SimpleFactorial, "FSimpleFactorial")
{
    nN = n;
}
//
void FSimpleFactorial::y1() { dF = factorial(nN); }

double FSimpleFactorial::factorial(int n)
{
    if (n == 0) return 1;
    else return n*factorial(n-1);
}


由于FSimpleFactorial对象是嵌套的自动机(子例程),因此必须有一个“包装器”-调用它的过程。他的示例是一个从名为FTskSimple的对象产生的过程,其代码如清单2所示。

清单2.创建嵌套自动机的过程
#include "lfsaappl.h"

class FTskSimple : public LFsaAppl
{
public:
    LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FTskSimple(nameFsa); }
    bool FCreationOfLinksForVariables();
    FTskSimple(string strNam);
    virtual ~FTskSimple();

    CVar    *pVarF;
    CVar    *pVarN;
    CVar    *pVarStart;

    LFsaAppl *pFCall{nullptr};
protected:
    int x1();
    void y1(); void y2(); void y3();
};

#include "stdafx.h"
#include "FTskSimple.h"
#include "FSimpleFactorial.h"
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
LArc TT_Task[] = {
    LArc("st", "t1", "--",	"y1"),	//
    LArc("t1", "t2", "x1",	"y3"),	//
    LArc("t2", "t1", "--",	"y2"),	//
    };

FTskSimple::FTskSimple(string strNam)
    :LFsaAppl(TT_Task, strNam)
{
}

FTskSimple::~FTskSimple() { if (pFCall) delete pFCall; }

bool FTskSimple::FCreationOfLinksForVariables() {
    pVarF = CreateLocVar("dF", CLocVar::vtDouble, "factorial value");
    pVarN = CreateLocVar("nN", CLocVar::vtInteger, "input value");
    pVarStart = CreateLocVar("bStart", CLocVar::vtBool, "start?");
    return true;
}

int FTskSimple::x1() { return pVarStart->GetDataSrc(); }

void FTskSimple::y1()
{
    // reset flag start
    pVarStart->SetDataSrc(this, 0.0);
    // creating and calling a subroutine
    pFCall = new FSimpleFactorial(pVarN->GetDataSrc());
}
void FTskSimple::y2()
{
    // display factorial value
    pVarF->SetDataSrc(this, static_cast<FSimpleFactorial*>(pFCall)->dF);
}

void FTskSimple::y3()
{
    // reset flag start
    pVarStart->SetDataSrc(this, 0.0);
    static_cast<FSimpleFactorial*>(pFCall)->nN = pVarN->GetDataSrc();
    // creating and calling a subroutine
    pFCall->FCall(this);
}


从初始状态“ st”到状态“ t1”的过渡过程将创建一个类型为FSimpleFactorial的对象,这将是创建嵌套自动机的基础(通常意义上,称为子程序)。此外,基于布尔类型bStart的局部变量的当前状态(另请参见谓词代码x1),它导致从状态“ t1”到“ t2”的转换。在此过渡上,y1的操作首先重置此局部变量的值(以防止错误重启),其次,调用FCall过程,该过程创建嵌套的自动机。

VKPA环境的自动机的解释器组织到嵌入式自动机的初始状态“ f0”的转换,后者的状态成为过程的当前状态。嵌套自动机到最终状态“ 00”的过渡将计算返回到先前的嵌套级别,在我们的情况下,返回到顶层自动机向状态t2的过渡完成。然后,该过程在从状态“ t2”到状态“ t1”的无条件转换上,执行动作y2,该动作y2设置了计算过程dF的局部变量的阶乘的结果。

请注意,非常重要的一点是,嵌套自动机的结果只有在完成其工作并转换到顶级自动机的下一个状态后才可用。可以通过指向嵌套自动机对象的指针来获取(结果)。

如果没有递归函数运行时,一切都会很好。与通常的子例程相比,由于嵌套,它通常要大得多,因此可能会违反自动过程动作的条件原子性,其总执行时间应适合离散时钟周期的值。

在我们的情况下,阶乘的计算量非常小,n = 170的两倍结果的最大值适合1微秒。为了计算较大的值,需要切换为长运算(例如,参见[4,5])。同时,阶乘计算时间将增加甚至更多,并且几乎可以保证超过离散时钟周期,从而影响以非抢占式多任务模式运行的其余网络机器的速度以及整个应用程序的响应性,这将在其``刹车''中体现出来。

您可以保持离散的时钟周期内,并通过将计算扩展为步骤来消除“刹车”缺陷。为此,将常用的阶乘算法转换为自动形式很方便。的确,由于自动机的解释模式,计算时间将增加,但是应用程序的时间属性将恢复正常。

清单3显示了其阶乘形式以及为随后转换为自动机形式准备的形式的阶乘的计算。后者明确表示隐藏在程序源代码中的原子步骤。在这种情况下,我们要讨论的形式为y = f(...)的运算符,其中f是普通或递归程序函数。这样的记录掩盖了函数的运行时间,并给变量y的值的“瞬时”分配造成了错误的印象。实际上,两者都不存在。

清单3.用于计算阶乘的软件功能
int factorial(int n)
{
    if (n == 0) return 1;
    else return n*factorial(n-1);
}

double Factorial(int n)
{
    double dF = 1;
    if (n == 0)
        return dF;
    else {
        double dPrev = Factorial(n-1);
        double dF = n*dPrev;
        return dF;
    }
}


在自动编程概念的框架中,软件自动对象的原子动作(方法)被认为是有条件的瞬间。并且,如果仅动作的总操作时间适合于自动机空间离散时间的单个周期,则软件实现将与其形式模型相对应。

备注1.在一个或另一个离散时钟周期上,操作的总实时运行时间可能相差很大。如果超过周期时间,则VKPA环境会尝试在自动机空间操作的后续周期中对其进行补偿,并尽可能减少后续周期的离散时间。

在图。图1显示了包含标记的框图,该标记反映了等效的有限状态机的状态及其谓词和操作的名称。自动机模型本身以自动机图的形式显示在图2中。 2.

图片
图1.递归分解算法框图

图片
。 2.递归阶乘算法的自动机模型

清单4中显示了用于计算阶乘的自动例程的代码。转换表(TP)中的注释反映了以毫秒为单位的阶乘计算时间,以及在n = 170时在计算过程上花费的离散步数。如果计算时间取决于计算平台的速度和/或项目的类型(调试/发布),则离散步骤的数量(时钟周期)仅由算法的属性确定,并且可以用作算法速度的客观评估,无论其表示形式和实现形式如何。

在自动机模型的情况下,离散滴答声的数量可以非常简单地计算,并且在收集了某些统计信息之后,得出了算法速度的解析公式。同时,这种估计是非常近似的,并且基于结论,而这些结论通常是由于相当复杂的推理而获得的。

清单4.自动化析因例程
#include "lfsaappl.h"

class FFactRec : public LFsaAppl
{
public:
    FFactRec(int n);
    virtual ~FFactRec() { if (pFFactRec) delete pFFactRec; };
    int nN;		//	n
    double dF;	//	n!
private:
	int x1();
    void y1(); void y2(); void y3();
protected:
    void CallFactorial();
    double  PrevFactorial();
    FFactRec *pFFactRec{nullptr};
};

#include "stdafx.h"
#include "FFactRec.h"

#define QT_NO_DEBUG_OUTPUT

extern LArc TT_FactRec[];
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
//	   
FFactRec::FFactRec(int n)
    :LFsaAppl(TT_FactRec, "FFactRec")
{
    nN=n; dF=1;
}

LArc TT_FactRec[] = {
//        Debug    Release     Steps
//msec: 120-129   98-101        340
    LArc("f1", "00", "x1",	"y3"),	//
    LArc("f1", "f2", "^x1",	"y1"),	//
    LArc("f2", "00", "--",	"y2"),	//
    LArc()
};

int FFactRec::x1() { return nN==0; }
//   
void FFactRec::y1() { CallFactorial(); }
//	1.      
//	2.    
void FFactRec::y2() { dF = PrevFactorial()*nN; }
// 0!
void FFactRec::y3() { dF = 1; }
//   (  )
void FFactRec::CallFactorial()	{
    //	     
    pFFactRec = new FFactRec(nN-1);
    pFFactRec->FCall(this);
};
//    
double  FFactRec::PrevFactorial()	{ return pFFactRec->dF; };


3.结论


我承认,由于要解决的任务的具体细节,我实际上不必在实际项目中使用和开发递归算法,除非出于培训目的和测试内核实现嵌套的目的。自动化例程-几乎不断且大量。其中,称为惯性的子例程特别有趣(有关更多详细信息,请参见[3])。但是他们应该单独考虑一下。

递归算法是程序理论的重要组成部分[6]。如果没有例程和递归的实现,则很难从所有角度想象一个有效且方便的计算模型。并且在给定对象编程的可能性的情况下实现递归(请参阅本文示例),在创建子程序模型之后,并不是那么困难。当然,您可以使用将递归转换为常规形式的某些方法,并使用相同的周期绕过它。但是最好不要愚蠢地摆脱它,而要有直接的实现机制。当然,就资源(相同堆栈内存的开销)而言,它当然会更慢,也更昂贵,但是对于现有的硬件功能,这并不是那么关键。

但是,递归算法作为评估算法模型的功能,通用性和有效性的测试也很有趣。从这个意义上讲,自动机模型不仅非常有效地实现了递归,而且还增加了“自动机芯片”作为奖励-控制递归算法内部状态,计算步骤以评估性能,控制算法执行时间等功能。等等

对抗递归是没有意义的。它必须能够使用。我引用完全同意的观点:“乍一看,递归似乎很复杂。但是在某些情况下,如果一切都正确完成,递归方法将非常有效。但是,有时最好使用循环。理解方法和有效使用它们的能力将对您的工作有所帮助,并且在面试中将是一个优势” [7]。我只能靠自己补充一点,即递归算法的自动模型可以使用计算模型的“自动属性”来更快地了解递归,调试和完善这种算法。

还有最后一个。不过,我想回答这个问题-协程递归如何进行?我已经问过了,但是还没有得到答案...毕竟,创建一百万个协程(例如,请参见[8])是一回事,而实现嵌套级别不相同但足够高的递归算法是另一回事。而且,似乎这个问题的答案不仅让我一个人感兴趣... [9]

文献
1. .., .. . I, . ., 1981, 2, 135-144. [ ], : mi.mathnet.ru/at5725 . . . ( 10.03.2020).
2. .., .. . II, . ., 1981, 3, 112-121. [ ], : mi.mathnet.ru/at5743 . . . ( 10.03.2020).
3. . [ ], : habr.com/ru/post/484588 . . . ( 10.03.2020).
4. . [ ], : habr.com/ru/post/255761 . . . ( 10.03.2020).
5. C++. [ ], : habr.com/ru/post/172285 . . . ( 10.03.2020).
6. . . : . . – .: , 1983. – 256 .
7. , ? Python. Ethan Jarrell. Recursion vs. Looping in Python [ ], : nuancesprog.ru/p/3325 . . . ( 15.03.2020).
8. Your first coroutine with Kotlin. [ ], : kotlinlang.org/docs/tutorials/coroutines/coroutines-basic-jvm.html . . . ( 18.03.2020).
9. . CyberForum.ru. [ ], : www.cyberforum.ru/unity/thread2479923.html . . . ( 18.03.2020).

All Articles