The Faddeev-LeVerrier algorithm is one of my favourite algorithms related to linear algebra for a variety of reasons:

  • It’s incredibly elegant
  • It computes many things at once - the trace of a matrix, its determinant, inverse and the characteristic polynomial (and thus, its eigenvalues).
  • The task of computing the characteristic polynomial usually requires the use of symbolic computation - one needs to compute det(AλI)\det(A - \lambda I) to get a function of λ\lambda, while the Faddeev-LeVerrier algorithm can do it numerically.

Initial considerations

The objective of the Faddeev-LeVerrier algorithm is to compute coefficients of the characteristic polynomial of a square matrix.

pA(λ)det(λInA)=k=0nckλk p_{A}(\lambda )\equiv \det(\lambda I_{n}-A)=\sum_{k=0}^{n}c_{k}\lambda ^{k}

The first observation to be made is that cn=1c_n = 1 and c0=(1)ndetAc_0 = (-1)^n \det A. The algorithm states:

M00MkAMk1+cnk+1Icnk=1ktr(AMk) M_{0} \equiv 0 \\ M_{k} \equiv AM_{k-1}+c_{n-k+1}I \\ c_{n-k} =-{\frac {1}{k}}\mathrm {tr} (AM_{k}) \\

A few observations assuming a nn by nn matrix AA:

  • Mn+1=0M_{n+1} = 0.
  • The trace of AA is equal to c2-c_2.
  • detA\det A is equal to (1)nc0(-1)^n c_0 (a consequence of the previous observations about cc).
  • A1A^{-1} is equal to 1c0Mk-\frac{1}{c_0}M_k.

An implementation

I start my implementation by asserting a few things about the environment and the input:

faddeev_leverrier{
  ⎕io ⎕ml0 1(/)2:
}

I’m interested only in matrices (arrays of rank 2 - 2=≢⍴⍵) and I specifically want them square -=/⍴⍵. If the conditions aren’t met, the algorithm returns an empty coefficient vector.

faddeev_leverrier{
  ⎕io ⎕ml0 1(/)2:
  nM0In n11+n
}

Next up, I define n as the number of rows/columns of the input matrix, M0 as the starting matrix for the algorithm, and I as an identity matrix of order n. The actual recursive implementation of the algorithm will return a vector of the characteristic polynomial coefficients and the matrix corresponding to the current iteration. Since the matrix corresponding to the last iteration is simply n n⍴0 (per observation 1), I will discard it:

faddeev_leverrier{
  ⎕io ⎕ml0 1(/)2:n
  M0In n11+n {
    =0:1 I
  } n
}

I have also added a guard for the final iteration, which simply returns the identity matrix and 1 as the coefficient.

faddeev_leverrier{
  ⎕io ⎕ml0 1(/)2:n
  M0In n11+n {
    =0:1 I(cp MP)-1XM0+.×MP
  } n
}

I recursively call the function asking for the pair of Mn1M_{n-1} and the corresponding c value. I also multiply matrices M0 (the input matrix) and MP (the previous matrix), as it’ll prove itself useful in upcoming computations, first one of them being the computation of c for the current iteration. For this purpose, I take the trace of X and divide it by the negated iteration index. The dyadic transposition (axis rearrangement) 0 0⍉X yields the leading diagonal of X, while +/ sums it.

faddeev_leverrier{
  ⎕io ⎕ml0 1(/)2:n
  M0In n11+n {
    =0:1 I(cp MP)-1XM0+.×MP
    c(+/0 0X)÷-
  } n
}

I am one step away from the final version of the algorithm, the only thing left is returning the result tuple:

faddeev_leverrier{
  ⎕io ⎕ml0 1(/)2:n
  M0In n11+n {
    =0:1 I(cp MP)-1XM0+.×MP
    c(+/0 0X)÷-(cp,c)(X+I×c)
  } n
}

The computation of the current M matrix is derived verbatim from the second equation of the algorithm.

Applications

I will combine today’s post with the yesterday’s one about polynomial roots. Namely, the yesterday’s post already contains an example use of my faddeev_leverrier function. Let’s do something different today.

With a few not so beautiful hacks on top of my existing function, I can compute all the properties of my test matrix:

faddeev_leverrier{
  ⎕io ⎕ml0 1(/)2:n
  invtrace
  M0In n11+ncpoly {
    =0:1 I(cp MP)-1XM0+.×MP
    c(+/0 0X)÷-
    MCX+I×c
    _{=n-1:invMC0}
    _{=1:trace-c0}
    (cp,c)MC
  } n
  (inv×-÷cpoly) trace cpoly ((cpoly)ׯ1*n)
}

The function will return a vector of:

  • inverse matrix
  • trace
  • characteristic polynomial
  • determinant
  data
3 1 5
3 3 1
4 6 4
  faddeev_leverrier data
┌─────────────────┬──┬───────────┬──┐
│ 0.15  0.65 ¯0.35101 ¯10 4 ¯4040│
│¯0.2  ¯0.2   0.3 │  │           │  │
│ 0.15 ¯0.35  0.15│  │           │  │
└─────────────────┴──┴───────────┴──┘

The results appear to match my pencil and paper calculations (not presented in this blog post - as an exercise, feel free to compute these properties of my test matrix yourself). Of course, there is a way to compute the remaining properties besides the characteristic polynomial using more idiomatic APL:

'alt'⎕cy'dfns'

demo{
  ⎕io ⎕ml0 1(/)2:
  () (+/0 0) (-alt× )
}

The results appear to be the same:

  demo data
┌─────────────────┬──┬──┐
│ 0.15  0.65 ¯0.351040│
│¯0.2  ¯0.2   0.3 │  │  │
│ 0.15 ¯0.35  0.15│  │  │
└─────────────────┴──┴──┘

To my surprise, though, it appears that the Faddeev-LeVerrier algorithm is faster for this particular test case! The benchmark data:

      cmpx 'demo data' 'faddeev_leverrier data'
  demo data               2.7E¯5 |   0% 
* faddeev_leverrier data  1.5E¯5 | -45%                 
      demo data
┌─────────────────┬──┬──┐
│ 0.15  0.65 ¯0.351040│
│¯0.2  ¯0.2   0.3 │  │  │
│ 0.15 ¯0.35  0.15│  │  │
└─────────────────┴──┴──┘
      faddeev_leverrier data
┌─────────────────┬──┬───────────┬──┐
│ 0.15  0.65 ¯0.35101 ¯10 4 ¯4040│
│¯0.2  ¯0.2   0.3 │  │           │  │
│ 0.15 ¯0.35  0.15│  │           │  │
└─────────────────┴──┴───────────┴──┘

Replacing the generalised alternant with a (hopefully) more performant determinant function yields oddly similar results:

      cmpx 'demo data' 'faddeev_leverrier data'
  demo data               2.0E¯5 |   0% 
* faddeev_leverrier data  1.5E¯5 | -25%         

Eigenvectors

The final part of my blog post will shed some more light on eigenvectors. Let’s compute the eigenvalues first:

  50 solve faddeev_leverrier data
0J2 0J¯2 10

For simplicity, I will present the pencil and paper calculations for λ=10\lambda = 10 alongside the APL code. I start with computing AλIA - \lambda I:

eigenvec{
  ⎕io ⎕ml0 1(/)2:
  nIn n11+ns-×I
}

AλI=715371466 A - \lambda I = \begin{vmatrix} -7 & 1 & 5\\ 3 & -7 & 1\\ 4 & 6 & -6 \end{vmatrix}

(AλI)v=0(A - \lambda I) \cdot v = 0 yields a homogenous equation system which needs to be solved. A very sad property of this system is that it’s underspecified, so xk1x_{k-1} up to nn are all functions of xnx_n. Surprisingly, that’s not a problem, though! If we ignore the presence of xnx_n, turns out one row of the matrix is redundant. If we assume that xn=1x_n = 1, in our example, we solve a non-homogenous system of equations with two variables.

eigenvec{
  ⎕io ⎕ml0 1(/)2:
  nIn n11+ns-×I
  N{¯9 ¯11+.(×⎕ct<|)9 11.}
  q1,11-sN¨1,-/q1s
}

x1=1823x2=1123x3=1 x_1 = \frac{18}{23} \\ x_2 = \frac{11}{23} \\ x_3 = 1

Of course, one could scale x3x_3 as they please, and modify x1x_1 and x2x_2 accordingly. I also use the same real/imaginary truncation technique as outlined in the previous post to strip (likely) unnecessary real/imaginary parts. Finally, I test my code:

  data(eigenvec)¨ 50 solve faddeev_leverrier data
┌───────────┬───────────┬───────────────────────────┐
│¯1J¯1 0J1 1¯1J1 0J¯1 10.7826086957 0.4782608696 1│
└───────────┴───────────┴───────────────────────────┘

The results seem to match. Great!

Summary

I believe that today’s post was much more involved than the previous ones. The full source code follows:

faddeev_leverrier{
  ⎕io ⎕ml0 1(/)2:n
  M0In n11+n {
    =0:1 I(cp MP)-1XM0+.×MP
    c(+/0 0X)÷-(cp,c)(X+I×c)
  } n
}

demo{
  ⎕io ⎕ml0 1(/)2:
  () (+/0 0) (-alt× )
}

⍝ A less general version that uses det instead.
demo{
  ⎕io ⎕ml0 1(/)2:
  () (+/0 0) (det )
}

eigenvec{
  ⎕io ⎕ml0 1(/)2:
  nIn n11+ns-×I
  N{¯9 ¯11+.(×⎕ct<|)9 11.}
  q1,11-sN¨1,-/q1s
}

⍝ Alternatively, the "more powerful" Faddeev-LeVerrier algorithm implementation.
faddeev_leverrier{
  ⎕io ⎕ml0 1(/)2:n
  invtrace
  M0In n11+ncpoly {
    =0:1 I(cp MP)-1XM0+.×MP
    c(+/0 0X)÷-
    MCX+I×c
    _{=n-1:invMC0}
    _{=1:trace-c0}
    (cp,c)MC
  } n
  (inv×-÷cpoly) trace cpoly ((cpoly)ׯ1*n)
}