{'fraction_non_alphanumeric': 0.03775264703076809, 'fraction_numerical': 0.008860531419910759, 'mean_word_length': 4.368229403732363, 'pattern_counts': {'":': 2, '<': 10, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 13, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
Computer Algebra in Particle Physics

These lectures given to graduate students in theoretical particle physics, provide an introduction to the ``inner workings'' of computer algebra systems. Computer algebra has become an indispensable tool for precision calculations in particle physics. A good knowledge of the basics of computer algebra systems allows one to exploit these systems more efficiently.


Introduction
Many of the outstanding calculations leading to precise predictions in elementary particle physics would not have been possible without the help of computer algebra systems (CAS). Examples of such calculations are the calculation of the anomalous magnetic moment of the electron up to three loops [1], the calculation of the ratio R = e + e − → hadrons e + e − → µ + µ − (1) of the total cross section for hadron production to the total cross section for the production of a µ + µ − pair in electron-positron annihilation to order O (α 3 s ) [2] (also involving a three loop calculation) or the calculation of the 4-loop contribution to the QCD β-function [3].
While the examples cited above have been carried out to an impressive third or fourth order, they only involve a single scale. Additional complications occur if multiple scales are involved. This is a challenging problem and computational quantum field theory witnessed a tremendous boost in the past two years in this area. The breakthrough was the development of systematic algorithms, which allowed the calculation of higher loop amplitudes with more than one scale. Examples are here the calculation of the most interesting 2 → 2 processes [4,5,6,7], which depend on two scales and the calculation for e + e − → 3 jets [8,9], which depends on three scales. None of the examples cited above could have been done without the help of computer algebra. Turning the argument around, these calculations are just the ones which are at the edge of what is feasible with todays methods and technology. Limiting factors are not only missing knowledge how to calculate certain processes, but also the speed and memory of computers, missing knowledge on efficient algorithms or improper representation of the data inside the computer. Understanding the basics of computer algebra systems allows one to exploit these systems more efficiently. This is the main goal of these lectures.
Particle physics is one important field of application for computer algebra and exploits the capabilities of computer algebra systems. This leads to valuable feed-back for the development of computer algebra systems. Quite a few computer algebra systems have their roots within the high energy physics community or strong links with them: RE-DUCE [10], SCHOONSHIP [11], MATHEMATICA [12], FORM [13] or GiNaC [14], to name only a few. Looking at the history of computer algebra systems, the first programs date back to the 1960's. Fig. (1) gives a historical overview together with the first appearance of some programming languages. The first systems were almost entirely based on LISP ("LISt Programming language"). LISP is an interpreted language and, as the name already indicates, designed for the manipulation of lists. Its importance for symbolic computer programs in the early days can be compared to the importance of FORTRAN for numerical programs in the same period. Already in this first period, the program RE-DUCE had some special features for the application to high energy physics. An exception to the LISP-based programs was SCHOONSHIP, written in assembler language by Veltman and specially designed for applications in particle physics. The use of assembler code lead to an incredible fast program (compared to the interpreted programs at that time) and allowed the calculation of more complex scattering processes in high energy physics. The importance of this program was recognized in 1998 by awarding the Nobel prize to M. Veltman. Also the program MACSYMA [15] deserves to be mentioned explicitly, since it triggered important development with regard to algorithms. In the 1980's new computer algebra systems started to be written in C. This allowed to exploit better the resources of the computer (compared to the interpreted language LISP) and at the same time allowed to maintain portability (which would not have been possible in assembler language). This period marked also the appearence of the first commercial computer algebra system, among which Mathematica and Maple [16] are the best known examples. In addition, also a few dedicated programs appeared, an example relevant to particle physics is the program FORM by J. Vermaseren as a (portable) successor to SCHOONSHIP. In the last few years issues of the maintainability of large projects became more and more important and the overall programming paradigma changed from procedural programming to object-oriented design. In terms of programming languages this was reflected by a move from C to C++. Following this change of paradigma, the library GiNaC was developed. The GiNac library allows symbolic calculations in C++. At the same time the last few years marked also a transition away from commercial systems (back) to open-source projects.
The choice of the approriate computer algebra system depends on the personal needs. It is worth analyzing the problem to be solved first. One class of problems requires only basic support from the computer algebra systems. These problems are often "local" problems, where the problem expands into a sum of different terms and each term can be solved independently of the others. The complication which occurs is given by the fact that the number of terms can become quite large. Here the requirements on a computer algebra system are bookkeeping and the ability to handle large amounts of data. Quite a few problems in high energy physics fall into this class. A notorious example are tree-level processes involving several triple gauge-boson vertices. The Feynman rules, like the one for the three-gluon vertex, expand this vertex into up to six terms, which easily blow up the size of intermediate expressions.
The second class of problems requires more sophisticated methods. These can be either standarized non-local operations (factorization is an example), which to some extend are already implemented in some computer algebra systems, or dedicated algorithms, which are developed by the user to solve this particular problem. Here the ability to model abstract mathematical concepts in the programming language of the computer algebra system is essential. Certainly, the two complications can also occur at the same time, e.g. the need to im-  Figure 1: A historical survey on computer algebra system together with the first appearance of some programming languages.
plement dedicated algorithms and the need to process large amounts of data. Higher loop calculations in quantum field theory tend to fall into this category. To summarize, requirements on a computer algebra system are therefore: • Efficiency in performance: If the system needs to process large amounts of data, performance is a priority. Usually this implies that the user's program is compiled and not interpreted. Systems optimized for performance may also contain low-level routines, which exploit efficiently the resources of the computer (memory, hard disc).
• Support of object-oriented programming: This is of importance for users, who would like to implement complex algorithms for abstract mathematical entities. Being able to program at an abstract level (e.g. not in a low-level computer language) can reduce significantly the number of bugs. In a similar direction goes the requirement for strong type checking. Strong type checking can catch a number of bugs already at compilation time.
• Short development cycles: It is usually the case that most time is spent for the development and the implementation of algorithms. The actual running time of the completed program is usually negligible against the development time. Since computer programs are developed by humans, the development time can be reduced if the computer algebra system allows for interactive use and provides high-quality output on the screen.
• Source code freely available: No computer program is free of bugs, neither commercial products nor non-commercial programs. The probability of finding a bug in a particular computer algebra system while working on a large project with this computer algebra system is not negligible. If the source code is available it is simpler to trace down the bug, understand its implications and to fix it.
Unfortunately, there is no system which would fullfill all requirements and the choice of the appropriate system involves therefore some inevitable trade-off. As examples for computer algebra systems I summarize the main features of Maple, FORM and GiNaC: • Maple: This is a commercial product. The advantages are a graphical user interface and the support for some sophisticated operations like factorization, integration or summation. The disadvantages are that Maple is quite inappropriate when it comes to processing large amounts of data.
• FORM: The advantage of FORM is its speed and its capability to handle large amounts of data. It is widely used within the high energy community and the program of choice for calculations involving large intermediate expressions.
• GiNaC: This is a library for symbolic computations in C++. The main feature is the possibility to implement one's own algorithms in an object-oriented way. It can handle large amounts of data and for some benchmark tests the speed is comparable to FORM.
These lecture notes follow a bottom-up approach from basic data structures to complex algorithms for loop calculations in high energy physics. In the next section I discuss data structures, object-oriented programming and the design of a simple toy computer algebra system. The discussion of the design will follow closely the design of the GiNaC-library. This is partly motivated by the fact that here the source code is freely available, as well as partly by the fact that I am involved in the development of a program [17], which combines the GiNaC-library with a C/C++ interpreter and a what-you-see-is-what-you-get editor.
The resulting system allows an interactive use and displays results in metafont-type quality. Section 3 deals entirely with efficiency. The efficient implementation of algorithms is discussed in detail for three examples: Shuffling relations, the multiplication of large numbers and the calculation of the greatest common denominator. The development of computer algebra systems initiated also research on systematic algorithms for the solution of certain mathematical problems. In section 4 I discuss some of the most prominent ones: Factorization, symbolic integration, symbolic summation and simplifications with the help of Gröbner bases. Section 5 is devoted to computational perturbative quantum field theory. I review algorithms, which have been developed recently, for the calculation of multi-loop integrals. General textbooks, on which parts of these lectures are based, are: D. Knuth, "The Art of Computer Programming" [18], K. Geddes et al., "Algorithms for Computer Algebra" [19] and J. von zur Gathen and J. Gerhard, "Modern Computer Algebra" [20].

Data structures
In this section I start from the basics: How data structures representing mathematical expressions, can be stored in the physical memory of the computer. Additional information on this subject can be found in the excellent book by Knuth [18]. The first programs for symbolic computations implemented mathematical expressions by nested lists and I discuss as an example symbolic differentiation in LISP first. Lists are special cases of "containers", and alternative types of containers are discussed in the following. In particular I examine the CPU time necessary to access or operate on the elements in various containers. The size of programs for the solution of certain problems depends on the complexity of the problem and can become large. In order to avoid that these programs become unmanageable, object-oriented techniques have been invented. I review these techniques briefly. At the end of this section I discuss the design of a simple toy computer algebra system.

Lists
I start the section on data structures with a concrete example: Symbolic differentiation programmed in LISP. This is THE classic example for symbolic calculations. Symbolic differentiation can be specified by a few rules: These rules are sufficient to differentiate polynomials in x. An implementation in LISP 1 looks like this: (* ,(ARG1 E) ,(DIFF (ARG2 E) X)))))) A few comments on this short program are in order. Within LISP the prefix notation is usually used, e.g. A+B is represented by (+ A B). The first three lines define aliases to the build-in LISP functions CAR, CADR and CADDR to extract the first, second or third element of a list, respectively. They are only introduced to make the program more readable. The program itself shoud be readable even without the knowledge of LISP, except for the appearance of the single quote " ' ", the backquote " ' " and the comma "," character. LISP generally interprets the first element in a list as the name of an operation and tries to evaluate this operation with the remaining elements of the list as arguments. This behaviour can be prohibited by putting a single quote " ' " in front of the list and the list remains unevaluated. The backquote " ' " acts like the single quote, except that any commas that appear within the scope of the backquote have the effect of unquoting the following expression. In this simple example it is further assumed that addition and multiplication take only two arguments. LISP is an interactive language and entering (DIFF '(* A X) 'X) at the prompt for yields (+ (* 0 X) (* A 1)), which stands for Simplifications like 0·x = 0, a·1 = a or 0+a = a are out of the scope of this simple example. This example already shows a few important features of computer algebra programs: • The distinction between objects, which contain sub-objects and objects without further substructures. The former are generally referred to as "containers", the later are called "atoms". Examples for atoms are symbols like a or x, while examples for containers are data structures which represent multiplication or addition of some arguments.
• The use of recursive techniques. In the example above the function DIFF calls itself whenever it encounters a multiplication or addition. Note that the recursive function call has simpler arguments, so that the recursion will terminate.
• Lists are used to represent data structures like addition and multiplication. The first element in the list specifies what the list represents.
• Lists can be nested. The output of the example above, (+ (* 0 X) (* A 1)), consists of two lists nested inside another list.
• The output of a symbolic procedure is not necessarily in the most compact form.

Containers
A container is an object that holds other objects. Lists and arrays are examples of containers. There are several ways how the information of a container can be stored in physical memory. In particular, the time needed to access one specific element will depend on the lay-out of the data in the memory. The appropriate choice depends on the specific problem under consideration. In this context, the "big-O" notation is useful: An indication O(n) for an operation on n elements means that the operation takes time proportional to the number of elements involved. Fig. (2) gives a rule-of-thumb for the significance of the cost of certain operations. An array (also called "vector" within the C++ terminology) stores the information linearly very expensive Figure 2: A summary on the costs of certain operations. Operations of order O(1) or O(log(n)) are considered "cheap" operations. Further, O(n log(n)) is considered closer to O(n) than to O(n 2 ). It is usually a considerable speep-up, if an operation which naively takes O(n 2 ) time, can be improved to O(n log(n)). In generally one tries to avoid operations, which take O(n 2 ) time.
in memory. Given the fixed size l of one entry and the address a 1 of the first entry, the address a i of the i-th entry is easily obtained as This involves one multiplication and one addition. The CPU time to access one specific element is therefore independent of the number of elements in the array. However, suppose that we have an array of n elements and that we would like to insert a new element between the ith and (i + 1)th entry. This involves to shift the entries with index n, n − 1, n − 2, ..., i + 1 by one position and is an O(n) operation. If these operations occur frequently, an array is not the best suited data structure.
In that case a list structure is more appropriate. A list is often implemented as a doublelinked list, where each node of the double-linked list contains one field of information, one pointer to the next element and one pointer to the previous element. Inserting a new element somewhere in the middle of the list involves only updating the pointers and is an O(1) operation. Typical list operations are to add or to remove elements in the middle of the list. However there is also a drawback for lists. To access the ith element of a list, there is no other way than to start at the first element and to follow the pointers to the next elements sequentially, until one arrives at the ith entry. This is an O(n) operation. A generalization of a double-linked list is a rooted tree, where each node may have several sub-nodes. A list can be viewed as a tree, where each node has exactly one sub-node. An important special case is a binary tree, where each node has up to two sub-nodes. A binary tree can be used to encode an order relation: Elements of the left sub-tree of a particular node are "less-than" the current node, whereas elements of the right sub-tree are "greater-than" the current node. An associative array (sometimes also called a map) keeps pairs of values. Given one value, called the key, one can access the other, called the mapped value. One can think of an associative array as an array for which the index need not be an integer. An associative array can be implemented by a binary tree. In this case it is further assumed that there is a less-than operation for the keys and the associative array keeps it's elements ordered with respect to this relation. To find the mapped value corresponding to a specific key k, subscription list operations back operations vector one-dimensional array O(1) The cost for specific operations with different types of containers. A dash indicates that the corresponding operation is usually not provided for this container. For the hash map it is assumed that the map is not tightly filled, such that collisions occur rarely.
one first compares the key k with the key k R of the entry at the root of the tree. If k < k R , the mapped value is in the left sub-tree and the procedure is repeated with the top-node of the left sub-tree as root, if k > k r the mapped value is in the right sub-tree and if k = k R we have already found the corresponding (key,value) pair. To find the mapped value takes on the average O(log(n)) operations. It is important to note that insertion of new elements is also an O(log(n)) operation and does not require to sort the complete tree. Insertion is done as follows: One starts at the root and one compares the key k of the new element with the key k R of the root. Let us assume that k > k R . If the root has a right sub-tree, the procedure is repeated with the top-node of the right sub-tree as root. If the root does not have a right sub-tree, we attach a new right sub-tree to the root, consisting of the new element. An alternative implementation for an associative array uses a hash map. Suppose that there is an easy to calculate function, called hash function, which maps each key to a specific address in a reserved memory area. In general, there will be no hash function, which can guarantee that two different keys are not mapped to the same address, but a good hash function will avoid these collisions as much as possible. If a collision occurs, there are several strategies to deal with this case: The simplest implementation for insertions would just use the next free entry, whereas to access elements one would do a linear search. An associative array based on a hash map requires therefore a hash function and an "is equal"-operation for its keys. Since the calculation of the hash value should be a fast operation, many hash functions are based on exclusive-or operations and bit-wise rotations. If collisions are not frequent, insertions and access can be done in constant time. Collisions are less frequent, if the memory area for the hash map is not tightly filled. Therefore a hash map is appropriate if speed for access and insertions is important and sufficient memory is available.

Object-oriented design: A little bit C++
Object-oriented programming techniques were invented to allow the development and maintenance of large, complex software applications. Experience showed that techniques which worked for smaller projects do not necessarily scale to larger projects. In this section I discuss some features of C++ as an example for an object-oriented programming language. A detailed introduction to C++ and object-oriented techniques can be found in the book by Stroustrup [22]. Most of the topics discussed here are also present in other object-oriented languages or can be modelled by the user in languages which do not have native support for object-oriented techniques.
There are several reasons, which motivate the particular choice of C++ from the set of object-oriented programming languages: Since 1998 C++ is standardized, which enhances the portability of programs to different platforms. This is of particular importance in academia, where one moves in the early stages of a career from one post-doc position to another. Furthermore, C++ is widely used. As a consequence, additional development tools are available. The list includes debuggers, editors with automatic high-lighting facilities for C++ code and tools for the automatic generation of documentation. Finally, C++ allows operator overloading. In the scientific domain, operations like addition and multiplication are frequently used. Operator overloading allows one to use the same notation for different data types, e.g. one can write for example independently if a and b are numbers, vectors or matrices. This makes programs more readable. It is also a major argument against Java in scientific computing. Java does not allow operator overloading and writing v.add_vec(w); for the addition of two vectors makes programs less readable. A first tool for object-oriented programming is a modular approach corresponding to a "divide and conquer" strategy. If a problem can be cleary separated into two sub-problems, the complexity is already greatly reduced. A basic module or the smallest independent entity in C++ is a class. A class consists of data members and methods operating on the data (methods are also called member functions). An example for a class would be the complex numbers, whose data members are two double precision variables x and y, representing the real and imaginary part, respectively. Member functions could be a print routine, addition and subtraction, etc.. Within the "divide and conquer" strategy falls also the strict separation of the implementation from the interface. This is done by private and public members. Data members are usually taken to be private members, to protect them to be changed accidently from outside the class. A well designed separation between an interface and an implementation allows one to replace the implementation of a specific method by a more efficient algorithm without changing the interface. Therefore no changes have to be done to the rest of the program.
A second tool for object-oriented programming is data abstraction. General purpose programming languages come usually with a limited number of build-in data types (integers, real double precision, etc.). In many cases the user would like to have additional data types, in high energy physics one might like to have a data type "complex number" or even a data type "Feynman diagram". Now, there might be programming languages which have a build-in data type "complex number", but it is quite unlikely to find a language which has a data type "Feynman diagram". The ability to extend the build-in data types by user-defined data types is therefore essential. Furthermore it occurs quite often that several data types are related to each other. In C++ similar concepts are modeled through inheritance and derived classes. For example, if there are the data types "strongly interacting particle", "gluon" and "quark", the type "strongly interacting particle" would be a base class from which the classes "gluon" and "quark" are derived. Derived classes are specializations of base classes and may have additional properties. Therefore, a derived class may be used where-ever only the base class is required. Inheritance and derived classes can be used to avoid that similar code is implemented more than once. Within the C++ community one draws in inheritance diagrams arrows from the derived class pointing to the base class. A third tool of object-oriented programming is the ability to re-use and extend a given program. The traditional re-use of an existing program consists in an application, where the new code calls the old code. Object-oriented programming techniques allow also the reverse situation, e.g. that the old code calls the new code. In C++ this can be done through virtual functions which are resolved at run-time. A typical example is the situation where the old program is a larger framework, which one would like to extend with some new functionality. If implemented properly, this will work even without recompiling the old code. The fourth tool of object-oriented programming techniques are generic algorithms. For example, sorting a list of integers or a list of real double precision variables can be done with the same algorithm. In C++ the use of templates allows the implementation of generic algorithms. The Standard Template Library (STL) already contains implementations for the most important algorithms, among other things it provides vectors, lists, maps and hash maps, discussed in the previous section. C++ was developed from the C programming language and retains C as a subset. As in C, data can be stored in memory either automatically on the stack, dynamically allocated on the heap or statically at a fixed address. For subroutine calls it is usually not efficient to copy a large data structure to local variables upon entering the subroutine. Instead it is more appropriate to pass a reference or a pointer to the subroutine.

A simple toy computer algebra system
In this section I will discuss the design of a simple toy computer algebra system. This toy model should know about symbols ("a", "b", etc.), integers ("1", "2", etc.), addition and multiplication. It should be possible to enter an expression like 5a + 3b + 2a (7) which is automatically simplified to 7a + 3b. The discussion of the implementation for this toy program follows closely the structure of the GiNaC-library [14,23]. For the GiNaClibrary the source code is freely available and the interested reader is invited to study the techniques explained below in a real case example. From the description above we can identify five objects: symbols, integers, addition, multiplication and expressions. Symbols and integers cannot have any subexpressions and are atoms, addition and multiplication are containers. An expression can be an atom or a container. The natural design is therefore to start from a base class "basic", representing a basic expression, from which the classes "symbol", "numeric", "add" and "mul" are derived. The lay-out of the atomic classes is straightforward: The class "symbol" must store it's name (a string like "a" or "x"), whereas the class "numeric" must store it's numeric value. Since both are derived from the class "basic", the relevant part of code in C++ could look as follows: Before dealing with the lay-out of the remaining classes (in particular with the lay-out of the class "basic"), it is worth to consider memory management and efficiency first. Already in our simple toy example, expressions can be nested and can become large. It is inefficient to copy large expressions upon entering or leaving a subroutine. Passing a pointer is more efficient. However, programming with pointers at a high level is not elegant and error-prone. One therefore hides all pointer operations into an encapsulating class "ex": The user deals only with the class "ex" and never manipulates pointers directly. An instance of the class "ex" consists only of a pointer to a "basic"-object and is therefore extremely light-weight. When a new expression is created in a subroutine, this subroutine returns an "ex", e.g. a pointer to some "basic"-object. This requires that the "basic"object is created dynamically on the heap. If it would be created on the stack, it will get automatically destroyed at the end of the subroutine, and the program will abort as soon as it tries to access this object outside the subroutine. However with dynamic memory allocation one has to ensure that objects, which are no longer needed, get deleted and that the occupied memory space is freed. Otherwise one would run out of memory soon. This can be done with a technique called "reference counting with copy-on-write semantics" [24]. The class "basic" has a counter, which keeps track of how many times this "basic"-object is pointed to: It happens quite often that some expression is assigned to more than one variable. With reference counting, there is no need to store the same "basic"-object twice in memory. In the following lines of code ex e1 = 3*a + 2*b; ex e2 = e1; both e1 and e2 point to the same "basic"-object and no copying takes place in the second line of this code. After these lines have been executed, the refcount variable of the "basic"-object is equal to 2. However, copying is necesarry when one expression is changed: This is called copy-on-write semantics. In this example e2 pointed first to the expression 3a + 2b, and then to the new expression 3a + 2b + 4. The implementation ensures that the reference counter of 3a + 2b is decreased by one, as soon as e2 points to the new expression 3a + 2b + 4. Obviously, if the refcount variable of some "basic"-object equals zero, it is no longer referenced by any variable and therefore it can be safely deleted. This releases the memory occupied by the "basic"-object and avoids memory leaks.
Let us now discuss the lay-out of the container classes "add" and "mul". As an example we consider again the expression 3a + 2b + 4. The naive representation would be a nested structure, where the top-level container is of type "add" with three summands. One summand is the numerical value 4, while the other two summands are of type "mul" and given by 3a and 2b, respectively. Unfortunately this representation will result in rather deep trees, which are slow to manipulate. Since products of the form "numerical coefficient" times "something else" occur quite often, it is advantageous to introduce an additional data type for this pair: class expair { public: ex rest; // first member of pair, an arbitrary expression ex coeff; // second member of pair, must be numeric }; These pairs are easy to manipulate, for example, if two pairs in a sum have the same value for the variable rest, one can simply add their numerical coefficients. The introduction of these pairs flattens expression trees significantly. Our sum 3a + 2b + 4 can now be represented by a sequence of two pairs (a, 3), (b, 2) and the overall numerical constant 4. If one would introduce powers into the computer algebra system, a similar structure is seen for products, for example 5x 2 y 3 z can be represented by the sequence of pairs (x, 2), (y, 3) and (z, 1) and the overall numerical constant 5. Again, if two pairs in the product have the same value for the variable rest, one can simply add the numerical values of their variables coeff, which now represent the exponents. It is therefore appropriate to introduce for the containers "add" and "mul" a common base class "expairseq", which implements this data structure, e.g. a sequence of pairs together with an additional numerical coefficient: std::vector<expair> seq; ex overall_coeff; }; The sequence of pairs is implemented by using a vector from the Standard Template Library. The classes "add" and "mul" are then derived from "expairseq": class add : public expairseq { }; class mul : public expairseq { }; Note that "add" and "mul" do not need any additional data members. Finally, we overload the operators + and * : These operators call the constructors of the classes "add" and "mul", respectively. Fig. (4) summarizes the class hierarchy of the simple toy computer algebra program. Note that the class "expair" is an internal helper class and not derived from "basic".
We now discuss how to implement automatic simplifications. For example, we would like to have that 5a + 3b + 2a is automatically evaluated to 7a + 3b. To this aim we introduce for the class "basic" a status flag "evaluated" and a method eval(): class basic { public: virtual ex eval(void) const; protected: mutable unsigned flags; }; Every time a "basic"-object is assigned, this flag is checked. If the flag is not set, the method eval() is called. The base class "basic" has a trivial evaluation routine, which does nothing except setting the flag to evaluated. This implementation is also sufficient for the atomic classes "symbol" and "numeric". The method eval() is a virtual function and can be redefined in derived classes. Since all expressions are always accessed through pointers via the class ex the mechanism of virtual functions ensures that the appropriate method is called. In our example, the classes "add" and "mul" would redefine the virtual function eval: class add : public expairseq { public: ex eval(void) const; } An implementation of the method eval() for the class "add" would sort the elements of the sequence of "expair"s and combine element which have the same "rest". Note that automatic evaluation happens the first time an object is assigned, not the first time an object is constructed. This is due to the fact, that an object of type "add" like 2x − x evaluates to x, which is of type "symbol" and not of type "add".

Efficiency
This section is devoted to issues related to efficiency. I discuss three examples: Shuffling relations, fast multiplication and the calculation of the greatest common divisor. Shuffling relations are discussed as an application of recursive techniques. For the multiplication of large numbers I outline three methods: the classical text-book method, Karatsuba's algorithm and a method based on fast Fourier transform. An algorithm for the calculation for the greatest common denominator is of central importance for many other algorithms. Therefore the Euclidean algorithm and improvements are discussed in detail.

Shuffling
Recursive techniques are often used in symbolic calculations. A classical example are the Fibonacci numbers defined by f (0) = 1, f (1) = 1 and for n ≥ 2. Recursive procedures are easily implemented, but it should be noted that in most cases they provide not an efficient way to solve a problem. For example, the recursive definition of the Fibonacci numbers is certainly the most efficient way to create a table with the first n Fibonacci numbers, but a recursive approach is highly inefficient, if we just need one Fibonacci number f (n), if n is large. For the Fibonacci numbers a closed formula is known, and it is more efficient to use this closed formula in the latter case. With this warning, I will now discuss some issues related to the implementation of recursive procedures in computer algebra systems. As an example I will use shuffling relations.
Shuffling relations occur frequently in calculations. I discuss them for Euler-Zagier sums, which are defined by Euler-Zagier sums form an algebra, that is to say that the product of two Euler-Zagier sums with the same upper summation limit n is again a finite sum of single Euler-Zagier sums. The product can be worked out recursively via the formula This multiplication can be implemented by a subroutine, which takes three lists res, arg1 and arg2 as arguments. When the routine is entered the first time, res is empty and arg1 and arg2 contain (m 1 , ..., m k ) and (m ′ 1 , ..., m ′ l ), respectively. If arg1 (or arg2) is the empty list, we are basically done: We append the content of arg2 (or of arg1) to res and return res. Otherwise we remove the first element from arg1 and append it to res and use recursion, this corresponds to the first line on the r.h.s of eq. (11). Similar, for the second (or third line) of eq. (11) one removes the first element from arg2, (or the first elements from arg1 and arg2) and appends it (or the sum of the two elements) to res. Most computer algebra systems provide a list data type within their framework. In GiNaC this type is called lst and is a derived class from basic. It is tempting to implement the shuffle multiplication using this data type. In GiNaC this could look as follows: where it is understood that res, arg1 and arg2 will always point to a lst. However, such an approach can lead to a considerable performance loss, since the arguments res, arg1 and arg2 are now under the spell of the automatic evaluation procedure. The algorithm according to eq. (11) shuffles basically elements from arg1 and arg2 to res. At each step new lists are created with appended or removed elements, and the automatic evaluation procedure will check for each new list, if all of its elements are already evaluated. Usually all elements are already evaluated, therefore this procedure is a waste of computer resources. It is therefore better to use for the lists a temporary data structure which is not related to the automatic evaluation procedure. In our case, a vector containing the type ex would do the job: typedef std::vector<ex> exvector; ex shuffle_mul(const exvector & res, const exvector & arg1, const exvector & arg2); Note that in the discussion of the GiNaC-library we already crossed the data structure expair, which is not derived from the class basic and therefore not subject to the automatic evaluation procedure. Situations where this technique can be used occur quite frequently. Another example is the shuffle product of iterated integrals. Let us consider the integral A product of two such integrals can be reduced to a sum of single integrals according to where the sum is over all permutations of z 1 , ..., z n , which keep the relative order of z 1 , ..., z k and z k+1 , ..., z n fixed. Recursively, we can write for the product and the algorithm can be implemented in complete analogy to the one for eq. (11). Similar considerations apply if we just want to transform one representation to another form. An examples is a change of basis from Euler-Zagier sums to harmonic sums, defined by The difference between Euler-Zagier sums and harmoinc sums is the upper summation limit for the subsums: i − 1 for Euler-Zagier sums and i for harmonic sums. The conversion between the two bases uses the formulae and are implemented by routines taking two arguments:

Multiplication of large numbers
In this paragraph I discuss issues related to the efficiency for the multiplication of large numbers. Multiprecision arithmetic is needed in many applications. For the practitioner, there are several freely-available libraries which provide support for multiprecision numbers. Examples are the libraries GMP [25], CLN [26] or NTL [27].
Let a be an integer with n digits in the base B, e.g.
For simplicity we assume that n is even (if n is odd we can just add a zero to the front). We can write a as where a h and a l have now n/2 digits in base B. Let b be another integer with n digits. The product ab can be calculated as This is the classical method. To multiply two integers with n digits requires 4 multiplications of integers with n/2 digits and four additions. Usually the computational cost of the additions can be neglected against the one for the multiplications. It is not too hard to see that the classical method is of order O(n 2 ).
The efficiency can be improved for large integers by rewritting eq. (19) as follows: This method requires only 3 multiplications of integers with n/2 digits and was invented by Karatsuba [28]. It can be shown that this algorithms grows like O(n log 2 3 ) ≈ O(n 1.58 ) with the number of digits n.
There is even a faster algorithm developed by Schönhage and Strassen [29] and based on the fast Fourier transform. It is most easily explained for the multiplication of polynomials. A polynomial of degree n is usually represented by its coefficients a i : However, a polynomial of degree n is also uniquely defined by the values at n + 1 distinct points x 0 , x 1 , ..., x n . This representation is called the modular representation. Suppose that we would like to multiply a polynomial a(x) of degree n by a polynomial b(x) of degree m. Then the product is of degree n + m. Suppose that we know the polynomials a(x) and b(x) at n + m + 1 distinct points: Then defines uniquely the polynomial a(x)b(x) and requires only O(n + m) operations. In the modular representation, multiplication of two polynomials of degree n is an O(n) operation. However, this is only the cost if the objects are already in this particular representation.
In general, we have to add the costs for converting the input polynomials to the modular representations and the cost for converting the result back to a standard representation. The method will be competitive only if these transformations can be done at a cost lower than, say, Karatsuba multiplication. Using a method based on the fast Fourier transform, it turns out that these transformations can be done efficiently and that the cost for these transformations is O(n log n). Let us first consider the transformation from the standard representation of a polynomial of degree less than n to the modular represenation. For simplicity we assume that n is even. We have the freedom to chose the n distinct evaluation points and a clever choice can reduce the required amount of calculation. A suitable choice is to evaluate the polynomial at the special points {1, ω, ω 2 , ..., ω n−1 }, where ω is a primitive n-th root of unity, e.g.
A few examples for primitive roots of unity: In C, i and −i are primitive 4-th roots of unity, −1 is not a primitive 4-th root of unity, since (−1) 2 = 1. In Z 17 , 4 is a 4-th root of unity, since 4 4 = 1 mod 17 and 4 2 = 16 mod 17 as well as 4 3 = 13 mod 17. The evaluation of the polynomial at this particular set of evaluation points is nothing than performing a discrete Fourier transform. Given a finite set of values (a 0 , a 1 , ..., a n−1 ), the discrete Fourier transform maps those values to the set (â 0 ,â 1 , ...,â n−1 ), wherê An efficient way to calculate the values (â 0 ,â 1 , ...,â n−1 ) is given by the fast Fourier transform. Since we assumed that n is even, the n-th roots of unity satisfy We write the polynomial a(x) in the form where the polynomials b(y) and c(y) have at most one-half the degree of a(x). Since we have ω i+n/2 2 = (ω i ) 2 it is sufficient to evaluate the polynomials b(y) and c(y) at n/2 distinct points instead of n. This is the basic building block for the fast Fourier transform. Using this technique recursively yields a method which performs the Fourier transform at O(n log n). In complete analogy, the conversion from the modular representation to the standard representation is done by the inverse discrete Fourier transform. Given a finite set of values (â 0 ,â 1 , ...,â n−1 ), the inverse discrete Fourier transform maps those values to the set (a 0 , a 1 , ..., a n−1 ), where Again, a similar decomposition like in eq. (28) is used to speed up the calculation.

The greatest common divisor and the Euclidean algorithm
It is often required to simplify rational functions by canceling common factors in the numerator and denominator. As an example let us consider One factor of (x + y) is trivially removed, the remaining factors are cancelled once we noticed that (x 2 − y 2 ) = (x + y)(x − y). For the implementation in a computer algebra system this is however not the way to proceed. The factorization of the numerator and the denominator into irreducible polynomials is a very expensive calculation and actually not required. To cancel the common factors in the numerator and in the denominator it is sufficient to calculate the greatest common divisor (gcd) of the two expressions. The efficient implementation of an algorithm for the calculation of the gcd is essential for many other algorithms. Like in the example above, most gcd calculations are done in polynomial rings. It is therefore useful to recall first some basic definitions from ring theory: A commutative ring (R, +, ·) is a set R with two operations + and ·, such that (R, +) is an abelian group and · is associative, distributive and commutative. In addition we always assume that there is a unit element for the multiplication. An example for a commutative ring would be Z 8 , e.g. the set of integers modulo 8. In this ring one has for example 3 + 7 = 2 and 2 · 4 = 0. From the last equation one sees that it is possible to obtain zero by multiplying two non-zero elements. An integral domain is a commutative ring with the additional requirement Sometimes an integral domain D is defined by requiring a · b = a · c and a = 0 ⇒ b = c (cancellation law).
It can be shown that these two requirements are equivalent. An example for an integral it is sufficient to focus on an algorithm for the calculation of the gcd.
An element p ∈ D − {0} is called prime if from p|ab it follows that p|a or p|b. An element p ∈ D − {0} is called irreducible if p is not a unit and whenever p = ab either a or b is a unit. In an integral domain, any prime element is automatically also an irreducible element. However, the reverse is in general not true. This would require some additional properties in the ring.
where either r = 0 or v(r) < v(b). This means that in an Euclidean domain division with remainder is possible. An example for an Euclidean domain is given by the integer numbers Z.
Finally, a field is a commutative ring in which every nonzero element has a multiplicative inverse, e.g. R − {0} is an abelian group. Any field is automatically an Euclidean domain. Examples for fields are given by the rational numbers Q, the real numbers R, the complex numbers C or Z p , the integers modulo p with p a prime number. The algorithm for the calculation of the gcd in an Euclidean domain dates back to Euclid [30]. It is based on the fact that if a = bq + r, then gcd(a, b) = gcd(b, r).
This is easily seen as follows: Let c = gcd(a, b) and d = gcd(b, r). Since r = a − bq we see that c divides r, therefore it also divides d. On the other hand d divides a = bq + r and therefore it also divides c. We now have c|d and d|c and therefore c and d are associates. It is clear that for r = 0, e.g. a = bq we have gcd(a, b) = b. Let us denote the remainder as r = rem(a, b). We can now define a sequence r 0 = a, r 1 = b and r i = rem(r i−2 , r i−1 ) for i ≥ 2. Then there is a finite index k sucht that r k+1 = 0 (since the valuation map applied to the remainders is a strictly decreasing function). We have gcd(a, b) = gcd(r 0 , r 1 ) = gcd(r 1 , r 2 ) = ... = gcd(r k−1 , r k ) = r k .
This is the Euclidean algorithm. We briefly mention that as a side product one can find elements s, t such that This allows the solution of the Diophantine equation for s and t whenever gcd(a, b) divides c.
We are primarily interested in gcd computations in polynomial rings. However, polynomial rings are usually only unique factorization domains, but not Euclidean domains, e.g. division with remainder is in general not possible. As an example consider the polynomials a(x) = x 2 + 2x + 3 and b(x) = 5x + 7 in Z[x]. It is not possible to write a(x) in the form a(x) = b(x)q(x) + r(x), where the polynomials q(x) and r(x) have integer coefficients. However in Q[x] we have and we see that the obstruction arrises from the leading coefficent of b(x). It is therefore appropriate to introduce a pseudo-division with remainder. Let D[x] be a polynomial ring over a unique factorization doamin D. For a(x) = a n x n + ...
with deg(r(x)) < deg(b(x)). This pseudo-divison property is sufficient to extend the Euclidean algorithm to polynomial rings over unique factorization domains.
Unfortunately, the Euclidean algorithm as well as the extended algorithm with pseudodivision have a severe drawback: Intermediate expressions can become quite long. This can be seen in the following example, where we would like to calculate the gcd of the polynomials in Z[x]. Calculating the pseudo-remainder sequence r i (x) we obtain This implies that a(x) and b(x) are relatively prime, but the numbers which occur in the calculation are large. An analysis of the problem shows, that the large numbers can be avoided if each polynomial is split into a content part and a primitive part. The content of a polynomial is the gcd of all it's coefficients. For example we have 15795x 2 + 30375x − 59535 = 1215 13x 2 + 25x + 49 (45) and 1215 is the content and 13x 2 + 25x + 49 the primitive part. Taking out the content of a polynomial in each step requires a gcd calculation in the coefficient domain and avoids large intermediate expressions in the example above. However the extra cost for the gcd calculation in the coefficient domain is prohibitive for multivariate polynomials. The art of gcd calculations consists in finding an algorithm which keeps intermediate expressions at reasonable size and which at the same time does not involve too much computational overhead. An acceptable algorithm is given by the subresultant method [31,32]: Similar to the methods discussed above, one calculates a polynomial remainder sequence r 0 (x), r 1 (x), ... r k (x). This sequence is obtained through r 0 (x) = a(x), r 1 (x) = b(x) and where c i is the leading coefficient of The ψ i are defined by ψ 1 = −1 and Then the primitive part of the last non-vanishing remainder equals the primitive part of gcd(a(x), b(x)).
I finally discuss an heuristic algorithm for the calculation of polynomial gcds [33]. In general an heuristic algorithm maps a problem to a simpler problem, solves the simpler problem and tries to reconstruct the solution of the original problem from the solution of the simpler problem. For the calculation of polynomial gcds one evaluates the polynomials at a specific point and one considers the gcd of the results in the coefficient domain. Since gcd calculations in the coefficient domain are cheaper, this can lead to a sizeable speed-up, if both the evaluation of the polynomial and the reconstruction of the polynomial gcd can be done at reasonable cost. Let us consider the polynomials Evaluating these polynomials at the point ξ = 100 yields a(100) = 621352707 and b(100) = 1196825521. The gcd of theses two numbers is c = gcd(621352707, 1196825521) = 30607.
To reconstruct the polynomial gcd one writes c in the ξ-adic representation Then the candidate for the polynomial gcd is In our example we have 30607 = 7 + 6 · 100 + 3 · 100 2 (52) and the candidate for the polynomial gcd is g(x) = 3x 2 + 6x + 7. A theorem guarantees now if ξ is chosen such that then g(x) is the greatest common divisor of a(x) and b(x) if and only if g(x) divides a(x) and b(x). This can easily be checked by a trial division. In the example above, g(x) = 3x 2 + 6x + 7 divides both a(x) and b(x) and is therefore the gcd of the two polynomials.
Note that there is no guarantee that the heuristic algorithm will succeed in finding the gcd. But if it does, this algorithm is usually faster than the subresultant algorithm discussed previously. Therefore the strategy employed in computer algebra systems like Maple or GiNaC is to try first a few times the heuristic algorithm with various evaluation points and to fall back onto the subresultant algorithm, if the gcd has not been found by the heuristic algorithm.

Remarks
In this section we discussed issues related to efficiency. It is worth to recall two general strategies: • The first one is the "divide-and-conquer" approach. If a large task can be partitioned recursively into smaller units, this can lead to a considerable speed-up. We have seen examples for this approach in Karatsuba's algorithm, in the algorithm for the fast Fourier transform and in the search for elements in a binary tree.
• The second strategy is the modular technqiue. Instead of solving a complicated problem right from the start, one tries first to find a related simpler problem. From the solution of the simpler problem, one tries then to reconstruct the solution of the original problem. Since the reduction to the simpler problem might involve some information loss, the reconstruction of the full solution may not be possible or may not be unique. However, if there is a simple and efficient way to check, if a guess for a solution is the correct solution, this method can be highly competitive. Fig. (7) summarizes this approach in a diagram. We discussed as an example for this strategy the heuristic gcd algorithm.

Classical Algorithms
The development of computer algebra systems has triggered research in mathematics on constructive algorithms for the solution of some important problems. Examples are the factorization of polynomials, symbolic integration and symbolic summation, and simplifications with the help of Gröbner bases. Most of these topics involve a big deal of mathematics.
Here I can only sketch an outline of the algorithms.

Factorization
It occurs frequently that we would like to factorize a polynomial. Here I discuss the factorization of a polynomial u(x) ∈ Z[x] over the integers Z. I outline one specific algorithm due to Berlekamp. The algorithm for the factorization is divided into three steps. The first (and relative simple) step factors out the greatest common divisior of the coefficients and performs a square-free decomposition. The second step uses Berlekamp's algorithm and factors the polynomial in the ring Z p , where p is a prime number. If p were sufficiently large, such that all products of coefficients would always lie between −p/2 and p/2, the factorization over Z could be read off from the factorization over Z p . Unfortunately, the required bound on p can become rather large. One uses therefore a small prime number p and reconstructs in step 3 from a factorization over Z p a factorization over Z p r , using a technique called Hensel lifting. Since r can be chosen large, this will yield a factorization over Z.
It should be noted that there are also other (and more efficient) algorithms for the factorization of polynomials. Examples are the probabilistic algorithm by Cantor and Zassenhaus [34] or the algorithm by Kaltofen and Shoup [35]. The computer program NTL by Shoup [27] provides a state-of-the-art implementation for the factorization of univariate polynomials.

Square-free decomposition
Suppose a polynomial u(x) contains a factor v(x) to some power m, e.g.
The exponent m can easily be reduced to one as follows: One starts by forming the derivative and calculates the gcd From eq. (54) and eq. (55) one sees that (v(x)) m−1 is a factor of the gcd and u(x) can be written as Note that g(x) divides u(x) by the definition of the gcd. This process can be repeated, until in each term all factors occur with power one. The computational cost is one (or several) gcd calculation(s).

Berlekamp's algorithm
After square-free decomposition we can assume that our polynomial is a product of distinct primes. We assume that deg u(x) = n. Berlekamp's algorithm [36] factorizes this polynomial in Z p , where p is a prime number. For 0 ≤ k < n one obtains coefficients q k,j from the mod u(x) representation of x kp , e.g.
This defines a n × n matrix A non-trivial solution of Note that the solution of eq. (61) is obtained by linear algebra in Z p . The calculation of for 0 ≤ s < p will then detect the non-trivial factors of u(x) in Z p .

Hensel lifting
Assume now that we have a factorization The Hensel lifting promotes this factorization to a factorization Since u(x) was assumed to be square-free, we have gcd(v 1 (x), w 1 (x)) = 1 mod p. For simplicity we also assume that the leading coefficient of u(x) is 1.
Since gcd(v 1 (x), w 1 (x)) = 1 mod p we can compute with the Euclidean algorithm polynomials a(x) and b(x) with deg a(x) < deg w 1 (x) and deg b( Suppose now that we are given (v r , w r ) and that we wish to compute (v r+1 , w r+1 ). To this aim one computes first a polynomial c r (x) such that By polynomial division of a(x)c r (x) by w 1 (x) one obtains a quotient q r (x) and remainder a r (x): One further sets v r+1 and w r+1 are now given by As an example let us consider u(x) = x 2 + 27x + 176. This polynomial is already squarefree and using Berlekamp's algorithm we try to find a factorization in Z 3 . We use the asymmetric representation {0, 1, 2} for Z 3 instead of the more symmetric representation {−1, 0, 1}. We find the factorization Hensel lifting yields then u(x) = (x + 7)(x + 2) mod 9 = (x + 16)(x + 11) mod 27.
The factorization in Z 27 agrees already with the factorization in Z.

Symbolic integration
Textbook integration techniques are often not more than a collection of heuristic tricks, together with a look-up integration table. A major achievement, which was initiated by the development of computer algebra systems, was the invention of a systematic procedure to decide whether a given function from a certain class of functions has an integral inside this class. If this is the case, the procedure provides also a constructive method to calculate this integral. This is the Risch integration algorithm [37]. It is beyond the scope of these notes to describe this algorithm in all details, but some important features can already be seen, if we restrict ourselves to rational functions. A rational function is a quotient of polynomials in the integration variable x: Suppose that we know the factorization of the denominator over C: b n x n + ...
with m 1 + ... + m r = n. Then after polynomial division with remainder and partial fraction decomposition we can write f (x) in the form where p(x) is a polynomial in x and the d ij are complex numbers. The integration of p(x) is trivial. For the pole terms we have The major inconvenience for this approach is the need to factor the denominator completely and thereby introducing algebraic extensions (like square roots or complex numbers), even if the integrand and the integral can entirely be expressed in a smaller domain (like Q).
As an example consider the integral The method above rewrites the integrand as and introduces the complex unit i, which cancels out in the final result. As can be seen from eq. (75) and eq. (76) the result for the integration of a rational function can always be written as a rational function plus some logarithms. It is desirable to compute as much of the integral as possible in the domain of the integrand, and to find the minimal algebraic extension necesary to express the integral. This is done in two step: The first step uses Hermite's reduction method and reduces the problem to one where the denominator is square-free. The second step applies then the Rothstein-Trager algorithm to obtain the logarithmic part. We first consider Hermite's reduction method. Let with deg a(x) < deg b(x) and gcd(a(x), b(x)) = 1. Assume further that m is the highest power to which irreducible factors occur in the factorization of the denominator, e.g.
With the help of the Euclidean algorithm we may then calculate polynomials r(x) and s(x) such that Then we obtain for the integral and the problem is reduced to one with a smaller power of v(x) in the denominator. Therefore we can assume that we are left with an integral of the form where deg a(x) < deg b(x) and b(x) is square-free. This integral will yield a result of the form where the c i 's are the zeros of b(x) and the r i 's are the residues of f (x) at the c i 's. There are efficient algorithms for the determination of the constants c i and r i . The first algorithm was invented by Rothstein [38] and Trager [39]. The method was later improved by Trager and Lazard and Rioboo [40].
Before concluding this section, let us comment on the Risch integration algorithm. The integration method outlined above for the integration of rational functions was generalized by Risch to an algorithm for elementary functions. The class of elementary functions is defined as follows: An elementary function of a variable x is a function that can be obtained from rational functions by repeatedly adding a finite number of logarithms, exponentials and algebraic numbers or functions (for example algebraic numbers like the complex unit i or square roots). Any (finite) nested combination of those functions is again an elementary function. For a systematic approach to integration it is useful to reformulate some concepts in an algebraic way: A differential field is a field F of characteristic 0 with a mapping D : F → F , which satisfies The mapping D is called a derivation or differential operator. An example for a differential field is given by the field of rational functions in one variable with D being the usual differentiation. We have seen in example eq. (77) that the integral for this example is again a rational function. In general, however, the integral of a rational function is expressed by logarithms and rational functions. In this case, the logarithms are an extension to the original differential field of rational functions. In the case of elementary functions, we have to consider logarithmic, exponential and algebraic extensions. Let F be a differential field and let G be a differential extension of it. A function g ∈ G is called logarithmic over F , if there exists a f ∈ F such that A function g ∈ G is called algebraic over F , if there exists a polynomial p ∈ F [z] such that Given an elementary function f , the Risch algorithm will decide whether the integral of f can be expressed as an elementary function. If this is the case, the algorithm provides also a constructive method to express the integral in terms of elementary functions. If this is not the case, we know at least, that the integral cannot be expressed in terms of elementary functions.

Symbolic summation
Symbolic summation can be thought of as the discrete analog of symbolic integration. For indefinite integration one looks for a function F (x) such that F ′ (x) = f (x). Then the definite integral over f (x) is given by For indefinite summation one looks for a function T (k) defined for integers k, such that the difference equals a given function t(k). The definite sum is then given by One now looks for an algorithm, which for a given class of functions f (k) decides, if the indefinite sum can be expressed in terms of functions of this class. If the answer is positive, we would like to have a function T (k), which fullfills eq. (89). This problem has been solved for the class of "hypergeometric terms". A sum of hypergeometric terms is a sum for which the ratio of two consecutive terms is a rational function r(k) of the summation index k, e.g.
A function t(k), which satisfies eq. (92) is called a hypergeometric term. In the following we will outline Gosper's algorithm [41]. Suppose first that we can write r(k) in the form where a(k), b(k) and c(k) are polynomials in k and gcd (a(k), b(k + j)) = 1, for all non-negative integers j. In fact, such a factorization exists for every rational function r(k) and there is a systematic way to find it. We then look for a rational function x(k), which satisfies the first order difference equation If such a solution x(k) does not exist, the sum in eq. (91) cannot be done within the class of hypergeometric terms. Otherwise, the solution is given by This completes Gosper's algorithm. Up to now we considered indefinite summation. Although a solution for an indefinite summation problem may not exist, we might be able to find a solution for a specific definite summation problem. Definite summation problems have been studied for sums of the type where both F (n + 1, k) F (n, k) and F (n, k + 1) are rational functions of n and k. Algorithms for these type of sums have been given by Sister Celine, Wilf and Zeilberger and Petkovsek. The book by Petkovsek, Wilf and Zeilberger [42] gives a good introduction to this subject.

Gröbner bases
In this paragraph we consider simplifications with the help of Gröbner bases. Assume that we have a (possibly rather long) expression f , which is a polynomial in several variables x 1 , ..., x k . In addition we have several siderelations of the form which are also polynomials in x 1 , ..., x k . A standard task is now to simplify f with respect to the siderelations s j , e.g. to rewrite f in the form f = a 1 s 1 + ... + a r s r + g, where g is "simpler" than f The precise meaning of "simpler" requires the introduction of an order relation on the multivariate polynomials. As an example let us consider the expressions which we would like to simplify with respect to the siderelations As an order relation we choose lexicographic ordering, e.g. x is "more complicated" as y, and x 2 is "more complicated" than x. This definition will be made more precise below. A naive approach would now take each siderelation, determine its "most complicated" element, and replace each occurence of this element in the expression f by the more simpler terms of the siderelation. As an example let us consider for this approach the simplification of f 2 with respect to the siderelations s 1 and s 2 : and f 2 would simplify to 4y 4 − 2y. In addition, since f 1 does not contain x 2 nor xy, the naive approach would not simplify f 1 at all. However, this is not the complete story, since if s 1 and s 2 are siderelations, any linear combination of those is again a valid siderelation. In particular, is a siderelation which can be deduced from s 1 and s 2 . This implies that f 2 simplies to 0 with respect to the siderelations s 1 and s 2 . Clearly, some systematic approach is needed. The appropriate tools are ideals in rings, and Gröbner bases for these ideals. We consider multivariate polynomials in the ring R[x 1 , ..., x k ]. Each element can be written as a sum of monomials of the form We define a lexicographic order of these terms by if the leftmost nonzero entry in (m 1 − m ′ 1 , ..., m k − m ′ k ) is positive. With this ordering we can write any element f ∈ R[x 1 , ..., x k ] as where the h i are monomials and h i+1 > h i with respect to the lexicographic order. The term h n is called the leading term and denoted lt(f ) = h n . Let B = {b 1 , ..., b r } ⊂ R[x 1 , ..., x k ] be a (finite) set of polynomials. The set is called the ideal generated by the set B. The set B is also called a basis for this ideal.
(In general, given a ring R and a subset I ⊂ R, I is called an ideal if a + b ∈ I for all a, b, ∈ I and ra ∈ I for all a ∈ I and r ∈ R. Note the condition for the multiplication: The multiplication has to be closed with respect to elements from R and not just I.) Suppose that we have an ideal I and a finite subset H ⊂ I. We denote by lt(H) the set of leading terms of H and, correspondingly by lt(I) the set of leading terms of I. Now suppose that the ideal generated by lt(H) is identical with the one generated by lt(I), e.g. lt(H) is a basis for lt(I) . Then a mathematical theorem guarantees that H is also a basis for I, e.g.
e.g. G is a basis for I and lt(G) is a basis for lt(I) . Such a set G is called a Gröbner basis for I. Buchberger [43] gave an algorithm to compute G. The importance of Gröbner bases for simplifications stems from the following theorem: Let G be a Gröbner basis for an ideal and no term of g is divisible by any monomial in lt(G).
In plain text: f is an expression which we would like to simplify according to the siderelations defined by I. This ideal is originally given by a set of polynomials {s 1 , ..., s r } and the siderelations are supposed to be of the form s i = 0. From this set of siderelations a Gröbner basis {b 1 , ..., b r ′ } for this ideal is calculated. This is the natural basis for simplifying the expression f . The result is the expression g, from which the "most complicated" terms of G have been eliminated, e.g. the terms lt(G). The precise meaning of "most complicated" terms depends on the definition of the order relation. In our example, {s 1 , s 2 } is not a Gröbner basis for s 1 , s 2 , since lt(s 1 ) = x 2 and lt(s 2 ) = xy and lt (ys 1 − xs 2 ) = x ∈ / lt(s 1 ), lt(s 2 ) .
A Gröbner basis for s 1 , s 2 is given by With b 1 = x and b 2 = 2y 3 − 1 as a Gröbner basis, f 1 and f 2 can be simplified as follows: e.g. f 1 simplifies to 1 and f 2 simplifies to 0.

Remarks
The Risch integration algorithm and Gosper's algorithm for summation both operate on a class of functions and can decide if the integration-or summation problem can be solved inside this class of functions. If this is the case, they also provide the solution in terms of functions of this class. The relevant classes of functions are the class of elementary functions for integration and the class of hypergeometric terms for summation. Unfortunately, these classes do not contain important functions relevant to loop calculations in perturbative quantum field theory. For example, Euler's dilogarithm is not in the class of elementary functions. For the class of hypergeometric terms, the situation is even worse. The harmonic numbers are not included in the class of hypergeometric terms. Therefore, logarithms like are not in the class of hypergeometric terms. The three functions in eq. (117) are the three basic transcendental functions occuring in one-loop calculations. Beyond one loop, there are additional functions.

Algorithms for high energy physics
In this section I discuss algorithms relevant to perturbative calculations in high energy physics. Topics include: Generation of all contributing Feynman diagrams, contraction of indices and Dirac algebra gymnastic, reduction of tensor loop integrals to scalar loop integrals and the evaluation of scalar loop integrals in terms of analytic functions. This section can only offer a selection of topics. Not included are for example methods based on Mellin transformations [44] or asymptotic expansions. The last method has been reviewed in [45,46].

Graph generation
The first step in any perturbative calculation is usually the determination of all relevant Feynman diagrams. This is trivial for processes involving not more than a handful diagrams, but requires a systematic procedure for processes involving a few hundreds or more diagrams. An efficient algorithm has been developed by Nogueira [47] and implemented into the program QGRAF [48]. The algorithm avoids recursive generation of Feynman diagrams and comparisons of stored diagrams. The algorithm for the generation of Feynman graphs is divided into two steps: In the first step all relevant topologies are generated. A topology is just a collection of nodes and edges, which connect nodes. A topology with n nodes is represented by a n × n adjacency matrix A. The entry A ij of the adjacency matrix denotes the number of edges connecting the nodes i and j. The algorithm generates all relevant adjacency matrices. Using an order relation for adjacency matrices, one can avoid to generate similar adjacency matrices, e.g. matrices which may be related to each other by a permutation of the nodes. In the second step the external nodes are labelled with the external particles and the edges and internal nodes are substituted by propagators and interaction vertices in all possible ways compatible with the Feynman rules. To avoid to generate equivalent Feynman graphs more than once during this stage, the symmetry group of each Feynman graph is determined and a further order relation is used to return only the graph which is the "smallest" within its symmetry class with respect to this order relation. In addition the symmetry factor is returned.
Other algorithms for the generation of Feynman diagrams have been considered for exam-ple by Küblbeck, Böhm and Denner [49] and implemented into FeynArts [50] as well as by Kajantie, Laine and Schröder [51].

Contraction of indices and Dirac algebra
After all diagrams have been generated, they are translated to mathematical expressions with the help of Feynman rules. Edges correspond to propagators and vertices to interaction vertices. As an example for Feynman rules we give the rule for gluon propagator in a covariant gauge and the rule for the quark-gluon vertex: The resulting expressions involve summations over repeated indices. The contraction of these indices can be done by applying succesivly a few rules: If a contraction over a string over Dirac matrices occurs, like in one uses first the anti-commuation relations of the Dirac matrices to bring the two matrices with the same index next to each other and uses then eq. (119). For the calculation of squared amplitudes there will be always a trace over the strings of Dirac matrices, which can be evaluated according to Traces over an odd number of Dirac matrices vansih. The recursive procedure for the evaluation of a trace over 2n Dirac matrices will generate (2n−1)(2n−3)...3 · 1 = (2n−1)!! terms. This number grows exponentially with n. If there are no further relations between the indices µ 1 , ..., µ 2n this is indeed the number of terms in the final result. However, in almost all practical applications, the free indices µ 1 , ..., µ 2n get contracted with a tensor, which is at least symmetric in some of its indices. In that case the recursive procedure is inefficient. A better way consists in splitting the string of Dirac matrices into two parts [52]: This process is repeated until each factor contains only a few Dirac matrices (say not more than two). One then rewrites each factor in terms of the basis The general term of this basis has the form where [...] denotes anti-symmetrization. Obviously, Products of these basis elements can be combined with a Clebsch-Gordan type formula: The Clebsch-Gordan coefficients C ρ 1 ...ρ k µ 1 ...µ i ;ν 1 ...ν j are anti-symmetric in the sets {µ 1 ...µ i }, {ν 1 ...ν j } and {ρ 1 ...ρ k }. Consider now the situation where the tensor with which the free indices µ 1 , ..., µ 2n are contracted, is symmetric in µ i and µ j . In this situation, any term involving a Clebsch-Gordan coefficient with µ i and µ j in the same index field can immediately be discarded. This can lead to a considerable speed-up.
Minor complications occur if γ 5 appears in the calculation. Within dimensional regularisation this requires a consistent definition of γ 5 . One possible definition is the 't Hooft -Veltman scheme, which takes γ 5 as a generic four-dimensional object. As a consequence one has to distinguish between four-dimensional and D = 4−2ε dimensional quantities. In this scheme D is assumed to be greater than 4. It is further assumed that the four-dimensional subspace and the (−2ε) dimensional subspace are orthogonal. For the calculation one splits all quantities into a four-dimensional part (denoted with a tilde) and a (−2ε) dimensional part (denoted with a hat).
These quantities satisfy relations likẽ γ 5 is then defined as a generic four-dimensional object: As a consequence, γ 5 anti-commutes with the four-dimensional Dirac matrices, but commutes with the remaining ones: The program FORM [13] is one of the most efficient tools for manipulations involving contraction of indices and traces over Dirac matrices. The program TRACER [53] has been written for the manipulations of the Dirac algebra in the 't Hooft -Veltman scheme.

One-loop integrals and Passarino-Veltman reduction
We now consider the reduction of tensor loop integrals (e.g. integrals, where the loop momentum appears in the numerator) to a set of scalar loop integrals (e.g. integrals, where the numerator is independent of the loop momentum). For one-loop integrals a systematic algorithm has been first worked out by Passarino and Veltman [54]. Consider the following three-point integral where p 1 and p 2 denote the external momenta. The reduction technique according to Passarino and Veltman consists in writing I µν 3 in the most general form in terms of form factors times external momenta and/or the metric tensor. In our example above we would write where {p µ 1 , p ν 2 } = p µ 1 p ν 2 + p µ 2 p ν 1 . One then solves for the form factors C 21 , C 22 , C 23 and C 24 by first contracting both sides with the external momenta p µ 1 p ν 1 , p µ 2 p ν 2 , {p µ 1 , p ν 2 } and the metric tensor g µν . On the left-hand side the resulting scalar products between the loop momentum k µ and the external momenta are rewritten in terms of the propagators, as for example The first two terms of the right-hand side above cancel propagators, whereas the last term does not involve the loop momentum anymore. The remaining step is to solve for the formfactors C 2i by inverting the matrix which one obtains on the right-hand side of equation (133). Due to this step Gram determinants usually appear in the denominator of the final expression. In the example above we would encounter the Gram determinant of the triangle One drawback of this algorithm is closely related to these determinants : In a phase space region where p 1 becomes collinear to p 2 , the Gram determinant will tend to zero, and the form factors will take large values, with possible large cancellations among them. This makes it difficult to set up a stable numerical program for automated evaluation of tensor loop integrals. There are modifications of the Passarino-Veltman algorithm, which avoid to a large extent the appearance of Gram determinants. These improved algorithms are based on spinor methods [55].

Beyond one-loop
The Passarino-Veltman algorithm is based on the observation, that for one-loop integrals a scalar product of the loop momentum with an external momentum can be expressed as a combination of inverse propagators. This property does no longer hold if one goes to two or more loops. Fig. (8) shows a two-loop diagram, for which the scalar product of a loop momentum with an external momentum cannot be expressed in terms of inverse propagators. A different and more general method for the reduction of tensor integrals is needed. In the following I will outline algorithms, which work beyond one loop. The first step is to relate tensor integrals to scalar integrals with raised powers of the propagators and different values of the space-time dimension D [56]. The starting point is the Schwinger parameterization for the propagators Let us first consider a scalar two-loop integral. Combining the exponentials arising from different propagators one obtains a quadratic form in the loop momenta. For instance, for a given two-loop integral with loop momenta k 1 and k 2 , one has then The momenta k 3 , . . . , k k are linear combinations of the loop momenta k 1 , k 2 and the external momenta. The coefficients a, b, c, d µ , e µ and f are directly readable from the actual graph: a(b) = x i , where the sum runs over the legs in the k 1 (k 2 ) loop, and c = x i with the sum running over the legs common to both loops. With a suitable change of variables for the loop momenta k 1 , k 2 , one can diagonalize the quadratic form and the momentum integration can be performed as Gaussian integrals over the shifted loop momenta according to Let us now consider a tensor integral. After the change of variables for the diagonalization of the quadratic form, we have a polynomial in the Schwinger parameters and the loop momenta k 1 and k 2 in the numerator. Integrals with an odd power of a loop momentum in the numerator vanish by symmetry, while integrals with an even power of the loop momentum can be related by Lorentz invariance to scalar integrals: The generalization to arbitrary higher tensor structures is obvious. In the remaining Schwinger parameter integrals, the tensor integrals introduce additional factors of the parameters x i and of 1/P. These additional factors can be absorbed into scalar integrals with higher powers of propagators and shifted dimensions, by introducing operators i + , which raise the power of propagator i by one, or an operator d + that increases the dimension by two, All Schwinger integrals are rewritten in terms of these scalar integrals. Therefore, using an intermediate Schwinger parametrization, we have expressed all tensor integrals in terms of scalar integrals. The price we paid is that these scalar integrals involve higher powers of the propagators and/or have shifted dimensions. Each integral can be specified by its topology, its value for the dimension D and a set of indices, denoting the powers of the propagators. In general the number of different integrals is quite large. There are now two different methods to proceed: The first method (integration by parts and solutions of differential equations) observes that there are non-trivial relations among the obtained set of integrals and employs an elimination procedure to reduce this set to a small set of so-called master integrals. These master integrals are then evaluated explicitly. Since some integrals have to be evaluated explicitly anyway, one may ask if there is an efficient algorithm which evaluates directly these integrals for arbitrary D and arbitrary set of indices. This is the philosophy of the second method (expansion of transcendental functions).

Integration by parts and solutions of differential equations
Let us start with the method, which reduces the (large) number of scalar integrals to a (small) set of master integrals. There exist non-trivial relations among various scalar integrals. Some of these relations can be obtained from integration-by-part identities [57] or the invariance of scalar integrals under Lorentz transformations [58]. Integration-by-part identities are based on the fact, that the integral of a total derivative is zero: Here, k is the loop momentum, the p i 's are the external momenta and v can either be a loop momentum or an external momentum. Working out the derivative yields a relation among several scalar integrals. In a similar way one obtains relations based on Lorentz-invariance. A scalar integral is evidently invariant under an infinitessimal Lorentz transformation, parametrized as This implies that where I(p 1 , ..., p n ) is a scalar integral with the external momenta p i . Again, working out the derivatives yields a relation among several scalar integrals. Each relation is linear in the scalar integrals and in principle one could use Gauss elimination to reduce the set of scalar integrals to a small set of master integrals. In practice this approach is inefficient. An efficient algorithm has been given by Laporta [59]. The starting point is to introduce an order relation for scalar integrals. This can be done in several ways, a possible choice is to order the topologies first: A scalar integral corresponding to a topology T 1 is considered to be "smaller" than an integral with topology T 2 , if T 1 can be obtained from T 2 by pinching of some propagators. Within each topology, the scalar integrals can be ordered according to the powers of the propagators and the dimension of space-time. Laporta's algorithm is based on the fact that starting from a specific topology, integration-by-part and Lorentz-invariance relations only generate relations involving this topology and "smaller" ones. To avoid to substitute a specific identity into a large number of other identities, one starts from the "smallest" topology and generates all relevant relations for this topology. Inside this class, integrals with higher powers of the propagators or higher dimensions are then expressed in terms of a few master integrals. These manipulations involve only a small subset of the complete system of integrals and relations and can therefore be done efficiently. Once this topology is completed, one moves on to the next topology, until all topologies have been considered. All integrals, which cannot be eliminated by this procedure are called master integrals. These master integrals have to be evaluated explicitly.
To evaluate these integrals the procedure used in [58,60] consists in finding first for each master integral a differential equation, which this master integral has to satisfy. The derivative is taken with respect to an external scale, or a ratio of two scales. It turns out that the resulting differential equations are linear, inhomogeneous first order equations of the form where y is usually a ratio of two kinematical invariants and T (y) is the master integral under consideration. The inhomogeneous term g(y) is usually a combination of simpler master integrals. In general, a first order linear inhomogeneous differential equation is solved by first considering the corresponding homogeneous equation. One introduces an integrating factor such that T 0 (y) = 1/M(y) solves the homogenous differential equation (g(y) = 0). This yields the general solution of the inhomogenous equation as where the integration constant C can be adjusted to match the boundary conditions. This method yields a master integral in the form of transcendental functions (e.g. for example hypergeometric functions), which still have to be expanded in the small parameter ε of dimensional regularization. Although this can be done systematically and will be discussed in the next paragraph, the authors of [58, 60] followed a different road. As an example let us consider the calculation for e + e − → 3 jets, which involves three scales. This scales can be taken to be s 12 , s 23 and s 123 . It is convenient to have only one dimension-full quantity s 123 and to introduce two dimensionless quantities x 1 = s 12 /s 123 and x 2 = s 23 /s 123 . Factoring out a trivial dimension-full normalization factor, one writes down an ansatz for the solution of the differential equation as a Laurent expression in ε. Each term in this Laurent series is a sum of terms, consisting of basis functions times some unknown (and to be determined) coefficients. This ansatz is inserted into the differential equation and the unknown coefficients are determined order by order from the differential equation. This apporach will succeed if we know in advance the right set of basis functions. The basis functions are a subset of multiple polylogarithms. In sec. (3.1) we already defined in eq. (12) the functions G(z 1 , ..., z k ; y). We now sligthly enlarge this set and define G(0, ..., 0; y) with k zeros for z 1 to z k to be This permits us to allow trailing zeros in the sequence (z 1 , ..., z k ). We can now define two subsets of these functions. The first subset are harmonic polylogarithms [61] for which y = x 1 and all z i are from the set {0, 1}. The second subset are two-dimensional harmonic polylogarithms [60] for which y = x 2 and all z i are from the set {0, x 1 , 1 − x 1 , 1}. The ansatz consists in taking the harmonic polylogarithms and the two-dimensional harmonic polylogarithms as a set of basis functions for the three-scale problem e + e − → 3 jets. The coefficients for harmonic polylogarithms are rational functions in x 1 and x 2 , the coefficients of the two-dimensional harmomic polylogarithms may in addition also contain (one-dimensional) harmonic polylogarithms.

Expansion of transcendental functions
In this paragraph we now discuss the second approach. Instead of reducing first a large set of scalar integrals to a small set of master integrals, which have to be worked out explicitly, we may ask if there is an efficient way to evaluate the scalar integrals directly [62,63,9]. The basic observation is that many relevant integrals can be expressed as (possibly nested) sums involving Gamma-functions. As an example let us discuss the one-loop triangle with two external masses defined by Tri 2 (m, ν 1 , ν 2 , ν 3 ; where k 2 = k 1 − p 1 − p 2 , k 3 = k 2 − p 3 and x 1 = s 12 /s 123 . We inserted a prefactor (−s 123 ) −m+ε+ν 123 to make the integral dimensionless and used the short-hand notation ν ij = ν i + ν j for sums of indices. Within this approach the integral is needed for arbitrary (integer) powers of the propagators and possibly also in shifted dimensions 6 − 2ε, 8 − 2ε, etc.. The space-time dimension is written as D = 2m − 2ε, where m is an integer. It is not too hard to derive a series representation for this integral, consisting of a combination of hypergeometric functions 2 F 1 .
One observes that the small paramter ε of dimensional regularization appears in the Gamma-functions. This expression has now to be expanded into a Laurent series in ε. For this particular example it is possible to convert the hypergeometric functions into an integral representation, to expand the integrand and to perform the resulting integrals. However, more complicated topologies have a representation as nested sums, for which a useful integral representation is not known. Fortunately, the expansion in ε can be done systematically at the level of nested sums. Using the formula all Gamma functions can be synchronized to the form Γ(n + aε). They are then expanded using the formula Γ(n + ε) = Γ(1 + ε)Γ(n) × 1 + εZ 1 (n − 1) + ε 2 Z 11 (n − 1) + ε 3 Z 111 (n − 1) + ... + ε n−1 Z 11...1 (n − 1) .

The cut technique
We conclude the discussion of techniques for loop calculations with a method based on unitarity [64]. I will discuss this method for one-loop amplitudes. It has been applied to a two-loop calculation by Bern et al. [4]. Within the unitarity based approach one chooses first a basis of integral functions I i ∈ F . For one-loop calculations in massless QCD a possible set consists of scalar boxes, triangles and two-point functions. The loop amplitude A loop is written as a linear combination of these functions The unknown coefficients c i are to be determined. r is a rational function in the invariants, not proportional to any element of the basis of integral functions. The integral functions themselves are combinations of rational functions, logarithms, logarithms squared and dilogarithms. The latter three can develop imaginary parts in certain regions of phase space, for example Knowing the imaginary parts, one can reconstruct uniquely the corresponding integral functions. In general there will be imaginary parts corresponding to different channels (e.g. to the different possibilities to cut a one-loop diagram into two parts). The imaginary part in one channel of a one-loop amplitude can be obtained via unitarity from a phase space integral over two tree-level amplitudes. With the help of the Cutkosky rules we have A loop is the one-loop amplitude under consideration, A tree L and A tree R are tree-level amplitudes appearing on the left and right side of the cut in a given channel. Lifting eq. (160) one obtains A tree L A tree R + cut free pieces, where "cut free pieces" denote contributions which do not develop an imaginary part in this particular channel. By evaluating the cut, one determines the coefficients c i of the integral functions, which have an imaginary part in this channel. Iterating over all possible cuts, one finds all coefficients c i . One advantage of a cut-based calculation is that one starts with tree amplitudes on both sides of the cut, which are already sums of Feynman diagrams. Therefore cancellations and simplifications, which usually occur between various diagrams, can already be performed before we start the calculation of the loop amplitude.
In general, a cut-based calculation leaves as ambiguity the ration piece r in eq. (158), which can not be obtained with this technique. One example for such an ambiguity would be d D k (2π) D k µ k ν − 1 3 q µ q ν + 1 12 g µν q 2 k 2 (k − q) 2 .
This term does not have a cut and will therefore not be detected in a cut-based calculation. However, Bern, Dixon, Dunbar and Kosower [64] have proven the following power counting criterion: If a loop-amplitude has in some gauge a representation, in which all n-point loop integrals have at most n − 2 powers of the loop momentum in the numerator (with the exception of two-point integrals, which are allowed to have one power of the loop momentum in the numerator), then the loop amplitude is uniquely determined by its cuts. This does not mean that the amplitude has no cut-free pieces, but rather that all cut-free pieces are associated with some integral functions.
In particular N = 4 supersymmetric amplitudes satisfy the power-counting criterion above and are therefore cut-constructible.
QCD does in general not satisfy the power-counting theorem and leaves as an ambiguity the rational function r. In principle one can obtain the rational piece r by calculating higher order terms in ε within the cut-based method. At one-loop order an arbitrary scale µ 2ε is introduced in order to keep the coupling dimensionless. In a massless theory the factor µ 2ε is always accompanied by some kinematical invariant s −ε for dimensional reasons. If we write symbolically the cut-free pieces c 0 (s 0 /µ 2 ) −ε can be detected at order ε: In practice, it is more efficient to take into account additional constraints, like the factorization in collinear limits [65], to determine the rational piece r.

Remarks
Let us summarize the state-of-the-art of systematic algorithms for the computation of loop integrals occuring in perturbative quantum field theory. In sect. 4 we discussed Risch's algorithm for symbolic integration and Gosper's algorithm for symbolic summation. These algorithms operate