Encuentra n-ésimo término de una secuencia en menos de O (N)

Tu sabes quien soy :

La complejidad del tiempo de esta pregunta se diferencia de una pregunta similar que se ha pedido. Esta es una pregunta de Zauba desarrollador contratación de desafío (evento terminó hace un mes):

f(0) = p
f(1) = q
f(2) = r

for n > 2

f(n) = a*f(n-1) + b*f(n-2) + c*f(n-3) + g(n)

where g(n) = n*n*(n+1)

p, q, r, a, b, c, nson dados. npuede ser tan grande como 10^18.

Enlace a un problema similar

En el enlace anterior, la complejidad del tiempo no se ha especificado y ya he resuelto este problema en O(n)el pseudocódigo está por debajo (sólo una aproximación, todos los posibles límites, y casos extremos se manejaron en el certamen).

if(n == 0) return p;
if(n == 1) return q;
if(n == 2) return r;
for(long i=3;i<=n;i++){
    now = a*r + b*q + c*p + i*i*(i+1);
    p = q; q = r; r = now;
}

Tenga en cuenta que tengo de módulo usado 10^9 + 7siempre que sea apropiado en el código original a desbordamientos de mango, maneja casos extremos apropiados siempre que sea necesario y que he utilizado java tipo de datos de longitud (si ayuda).

Pero como esto todavía requiere O(n)tiempo, estoy esperando una mejor solución que puede manejar n ~ 10^18.

EDITAR

Como usuario גלעד ברקן mencionado acerca de su relación con la potenciación de la matriz, que he tratado de hacer esto y pegado en un punto determinado, donde no estoy seguro de qué lugar en la cuarta fila, tercera columna de la matriz. La amabilidad de hacer todas las sugerencias y correcciones.

| a b c  1? |   | f(n) |        | f(n+1) |
| 1 0 0  0  |   |f(n-1)|        |  f(n)  |
| 0 1 0  0  |   |f(n-2)|    =>  | f(n-1) |
| 0 0 ?! 0  |   | g(n) |        | g(n+1) |

    M               A               B
meowgoesthedog:

exponenciación matriz es de hecho el camino correcto a seguir, pero hay un poco más de trabajo que hacer.

Desde g(n)no es constante de valor, no hay manera de aplicar la exponenciación matriz de manera eficiente ( O(log n)en lugar de O(n)) a la relación de recurrencia en su forma actual.


Una relación de recurrencia similares necesita ser encontrado para g(n)con sólo una trailing término constante. Dado que g(n)es cúbico, se requieren 3 términos recursivos:

g(n) = x*g(n-1) + y*g(n-2) + z*g(n-3) + w

Ampliar las expresiones cúbicos para cada uno de ellos:

n³ + n² = x(n³-2n²+n) + y(n³-5n²+8n-4) + z*(n³-8n²+21n-18) + w

        = n³(x+y+z) + n²(-2x-5y-8z) + n(x+8y+21z) + (w-4y-18z)

Coinciden con los coeficientes para obtener tres ecuaciones simultáneas para x, y, zmás otro para calcular w:

  x +  y +   z = 1
-2x - 5y -  8z = 1
  x + 8y + 21z = 0
  w - 4y - 18z = 0

Resolverlos para obtener:

x = 3    y = -3    z = 1    w = 6

Convenientemente, estos coeficientes son también números enteros *, lo que significa la aritmética modular se puede realizar directamente en la recurrencia.

* Dudo que esto era una coincidencia - que bien podría haber sido la intención del examinador contratación.

Por tanto, la ecuación de recurrencia matriz es:

|  a  b  c  1  0  0  0 |   | f(n-1) |   |   f(n) |
|  1  0  0  0  0  0  0 |   | f(n-2) |   | f(n-1) |
|  0  1  0  0  0  0  0 |   | f(n-3) |   | f(n-2) |
|  0  0  0  3 -3  1  6 | x |   g(n) | = | g(n+1) |
|  0  0  0  1  0  0  0 |   | g(n-1) |   |   g(n) |
|  0  0  0  0  1  0  0 |   | g(n-2) |   | g(n-1) |
|  0  0  0  0  0  0  1 |   |      1 |   |      1 |

La ecuación final matriz exponenciación es:

                        [n-2]
|  a  b  c  1  0  0  0 |       | f(2) |   |   f(n) |        | f(2) |   |  r |
|  1  0  0  0  0  0  0 |       | f(1) |   | f(n-1) |        | f(1) |   |  q |
|  0  1  0  0  0  0  0 |       | f(0) |   | f(n-2) |        | f(0) |   |  p |
|  0  0  0  3 -3  1  6 |   x   | g(3) | = | g(n+1) |   ,    | g(3) | = | 36 |
|  0  0  0  1  0  0  0 |       | g(2) |   |   g(n) |        | g(2) |   | 12 |
|  0  0  0  0  1  0  0 |       | g(1) |   | g(n-1) |        | g(1) |   |  2 |
|  0  0  0  0  0  0  1 |       |  1   |   |      1 |        |  1   |   |  1 |

(Cada operación es implícitamente modulo 10^9 + 7o el que sea dicho número se suministra).


Tenga en cuenta que Java %operador es el resto , que es diferente a la de módulo para los números negativos. Ejemplo:

-1 % 5 == -1     // Java
-1 = 4 (mod 5)   // mathematical modulus

La solución es bastante simple:

long mod(long b, long a)
{
    // computes a mod b
    // assumes that b is positive
    return (b + (a % b)) % b;
}

El original algoritmo iterativo:

long recurrence_original(
    long a, long b, long c,
    long p, long q, long r,
    long n, long m // 10^9 + 7 or whatever
) {
    // base cases
    if (n == 0) return p;
    if (n == 1) return q;
    if (n == 2) return r;

    long f0, f1, f2;
    f0 = p; f1 = q; f2 = r;
    for (long i = 3; i <= n; i++) {
        long f3 = mod(m,
            mod(m, a*f2) + mod(m, b*f1) + mod(m, c*f0) +
            mod(m, mod(m, i) * mod(m, i)) * mod(m, i+1)
        );
        f0 = f1; f1 = f2; f2 = f3;
    }
    return f2;
}

funciones de matriz Modulo:

long[][] matrix_create(int n)
{
    return new long[n][n];
}

void matrix_multiply(int n, long m, long[][] c, long[][] a, long[][] b)
{
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            long s = 0;
            for (int k = 0; k < n; k++)
                s = mod(m, s + mod(m, a[i][k]*b[k][j]));
            c[i][j] = s;
        }
    }
}

void matrix_pow(int n, long m, long p, long[][] y, long[][] x)
{
    // swap matrices
    long[][] a = matrix_create(n);
    long[][] b = matrix_create(n);
    long[][] c = matrix_create(n);

    // initialize accumulator to identity
    for (int i = 0; i < n; i++)
        a[i][i] = 1;

    // initialize base to original matrix
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            b[i][j] = x[i][j];

    // exponentiation by squaring
    // there are better algorithms, but this is the easiest to implement
    // and is still O(log n)
    long[][] t = null;
    for (long s = p; s > 0; s /= 2) {
        if (s % 2 == 1) {
            matrix_multiply(n, m, c, a, b);
            t = c; c = a; a = t;
        }
        matrix_multiply(n, m, c, b, b);
        t = c; c = b; b = t;
    }

    // write to output
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            y[i][j] = a[i][j];
}

Y, por último, el nuevo algoritmo mismo:

long recurrence_matrix(
    long a, long b, long c,
    long p, long q, long r,
    long n, long m
) {
    if (n == 0) return p;
    if (n == 1) return q;
    if (n == 2) return r;

    // original recurrence matrix
    long[][] mat = matrix_create(7);
    mat[0][0] = a; mat[0][1] = b; mat[0][2] = c; mat[0][3] = 1;
    mat[1][0] = 1; mat[2][1] = 1;
    mat[3][3] = 3; mat[3][4] = -3; mat[3][5] = 1; mat[3][6] = 6;
    mat[4][3] = 1; mat[5][4] = 1;
    mat[6][6] = 1;

    // exponentiate
    long[][] res = matrix_create(7);
    matrix_pow(7, m, n - 2, res, mat);

    // multiply the first row with the initial vector
    return mod(m, mod(m, res[0][6])
        + mod(m, res[0][0]*r)  + mod(m, res[0][1]*q)  + mod(m, res[0][2]*p)
        + mod(m, res[0][3]*36) + mod(m, res[0][4]*12) + mod(m, res[0][5]*2)
    );
}

Aquí están algunos puntos de referencia de la muestra para los dos algoritmos anteriores.

  • Original algoritmo iterativo:

    n       time (μs)
    -------------------
    10^1    9.3
    10^2    44.9
    10^3    401.501
    10^4    3882.099
    10^5    27940.9
    10^6    88873.599
    10^7    877100.5
    10^8    9057329.099
    10^9    91749994.4
    
  • Nuevo algoritmo de matriz:

    n       time (μs)
    ------------------
    10^1    69.168
    10^2    128.771
    10^3    212.697
    10^4    258.385
    10^5    318.195
    10^6    380.9
    10^7    453.487
    10^8    560.428
    10^9    619.835
    10^10   652.344
    10^11   750.518
    10^12   769.901
    10^13   851.845
    10^14   934.915
    10^15   1016.732
    10^16   1079.613
    10^17   1123.413
    10^18   1225.323
    

El algoritmo antiguo tomó más de 90 segundos para calcular n = 10^9, mientras que el nuevo algoritmo que logra en poco más de 0,6 mili segundos (un 150,000x aceleración)!

Complejidad temporal del algoritmo original era evidentemente lineal (como se esperaba); n = 10^10tomó demasiado tiempo para completar así que no continué.

Complejidad temporal del nuevo algoritmo era evidentemente logarítmica - duplicando el orden de magnitud del ndado lugar a la duplicación tiempo de ejecución (de nuevo, como se esperaba debido a la exponenciación por cuadratura).

Para los valores "pequeñas" de n( < 100) la sobrecarga de la asignación y las operaciones de matriz eclipsados el algoritmo en sí, pero rápidamente se convirtió en insignificante como naumentado.

Supongo que te gusta

Origin http://43.154.161.224:23101/article/api/json?id=226809&siteId=1
Recomendado
Clasificación