Explicar en detalle los problemas de programación dinámica de la serie.

Después de leer este artículo, puede ir a Likou para ganar los siguientes temas:

198. Un robo de casa

213. House Robbery II

337. Robo de casa III

-----------

Algunos lectores me preguntaron en privado cómo hacer la serie de preguntas "House Robber" de LeetCode (la versión en inglés se llama House Robber). Descubrí que esta serie de temas son muy elogiados. Son temas de programación dinámica más representativos y hábiles. Habla sobre este tema.

Hay un total de tres recorridos en la serie de familia y robo.El diseño de dificultad es muy razonable y progresivo. El primero es un problema de programación dinámica relativamente estándar, mientras que el segundo incorpora las condiciones de una matriz circular, y el tercero es aún más absoluto. Combina las soluciones ascendentes y descendentes de la programación dinámica con árboles binarios. Creo que es muy Inspirador. Si nunca lo ha hecho antes, se recomienda estudiar.

A continuación, partimos del primer análisis.

Ladrón de casa I

6147bbdf310af02edb4e2e39ee005905.jpg

public int rob (int [] nums);

El tema es fácil de entender y las características de la programación dinámica son obvias. Como resumimos en el artículo anterior "Explicación detallada de la programación dinámica", para resolver el problema de la programación dinámica hay que buscar "estado" y "elección", nada más .

Imagínese que usted es el ladrón profesional, caminando por esta hilera de casas de izquierda a derecha. Hay dos opciones frente a cada casa : agarrar o no.

Si agarras esta casa, definitivamente no puedes agarrar la siguiente casa de al lado, solo puedes elegir entre la siguiente casa.

Si no toma esta casa, puede ir a la siguiente casa y continuar tomando decisiones.

Después de haber atravesado la última casa, no es necesario que la agarre, y el dinero que puede obtener es obviamente 0 ( caso base ).

La lógica anterior es muy simple, de hecho, el "estado" y la "elección" se han aclarado: el índice de la casa frente a ti es el estado, y si agarrar o no agarrar es la elección .

44518b0a94fb3984312ce557966ca429.jpeg

En las dos opciones, elija el resultado más grande cada vez, y el resultado final es la mayor cantidad de dinero que puede obtener:

// La función principal 
public int rob (int [] nums) { 
    return dp (nums, 0); 
} 
// Devuelve el valor máximo que nums [start ..] puede tomar 
private int dp (int [] nums, int start) { 
    if (start> = nums.length) { 
        return 0; 
    } 

    int res = Math.max ( 
            // No agarre, vaya a la siguiente casa 
            dp (nums, start + 1),  
            // agarre, vaya a la siguiente casa 
            nums [start ] + dp (nums, start + 2) 
        ); 
    return res; 
}

Después de aclarar la transición de estado, puede encontrar que hay start un subproblema superpuesto para la misma  ubicación, como se muestra en la siguiente figura:

267f2bb39fbad1ca422faedbe34c3c56.jpeg

Los ladrones tienen muchas opciones para llegar a esta posición. ¿No sería una pérdida de tiempo si entran en recursividad cada vez que llegan aquí? Entonces, hay subproblemas superpuestos, que se pueden optimizar con memo:

private int [] memo; 
// Función principal 
public int rob (int [] nums) { 
    // Inicialización memo 
    memo = new int [nums.length]; 
    Arrays.fill (memo, -1); 
    // El ladrón comienza en 0 La casa comenzó a robar 
    return dp (nums, 0); 
} 

// Devuelve el valor máximo que dp [start ..] puede tomar 
private int dp (int [] nums, int start) { 
    if (start> = nums.length) { 
        return 0; 
    } 
    // Evite el cálculo doble 
    if (memo [start]! = -1) return memo [start]; 

    int res = Math.max (dp (nums, start + 1),  
                    nums [start] + dp ( nums, start + 2)); 
    // Ingrese el memo 
    memo [inicio] = res; 
    return res; 
}

Esta es la solución de programación dinámica de arriba hacia abajo. También podemos modificarla ligeramente y escribir una solución de abajo hacia arriba :

int rob (int [] nums) { 
    int n = nums.length; 
    // dp [i] = x significa: 
    // comienza el robo desde la i-ésima casa, y la cantidad máxima de dinero que puedes tomar es x 
    // caso base: dp [n] = 0 
    int [] dp = new int [n + 2]; 
    for (int i = n-1; i> = 0; i--) { 
        dp [i] = Math.max (dp [i + 1], nums [i] + dp [i + 2]); 
    } 
    return dp [0]; 
}

También encontramos que la transición de estado solo está  dp[i] relacionada con los dos estados más recientes, por lo que se puede optimizar aún más para reducir la complejidad del espacio a O (1).

int rob (int [] nums) { 
    int n = nums.length; 
    // 记录 dp [i + 1] 和 dp [i + 2] 
    int dp_i_1 = 0, dp_i_2 = 0; 
    // 记录 dp [i] 
    int dp_i = 0; 
    para (int i = n - 1; i> = 0; i--) { 
        dp_i = Math.max (dp_i_1, nums [i] + dp_i_2); 
        dp_i_2 = dp_i_1; 
        dp_i_1 = dp_i; 
    } 
    return dp_i; 
}

El proceso anterior se ha explicado en detalle en nuestra "Explicación detallada de planificación dinámica", creo que todos podrán comprenderlo. Creo que es muy interesante que el seguimiento de este número requiera algunas adaptaciones inteligentes basadas en nuestro pensamiento actual.

PD: He escrito más de 100 artículos originales con cuidado y me he cepillado mano a mano con 200 preguntas de hebilla, todas las cuales se publican en  la hoja de trucos del algoritmo de labuladong , que se actualiza continuamente . Se recomienda recopilar, cepillar las preguntas en el orden de mis artículos , dominar varias rutinas de algoritmos y luego lanzarlas al mar de preguntas.

Ladrón de casas II

Esta pregunta es básicamente la misma que la primera descripción. El ladrón todavía no puede robar las casas vecinas. La entrada sigue siendo una matriz, pero le dice que estas casas no son una fila, sino un círculo .

En otras palabras, la primera casa y la última casa ahora están adyacentes entre sí y no pueden ser saqueadas al mismo tiempo. Por ejemplo, para una matriz de entrada  nums=[2,3,2], el resultado devuelto por el algoritmo debe ser 3 en lugar de 4, porque el principio y el final no se pueden robar al mismo tiempo.

Parece que esta restricción no debería ser difícil de resolver. Mencionamos una solución a las matrices circulares en el anterior "Monotonic Stack Solving Next Greater Number", entonces, ¿cómo lidiar con este problema?

En primer lugar, la primera y la última habitación no se pueden robar al mismo tiempo, por lo que solo hay tres situaciones posibles: o no se roba ninguna; o se roba la primera casa y no roban la última; o se roba la última casa y no se roba la primera.

be9fa34adb48b9b85e67f79719eb5c54.jpeg

Eso es simple. En estos tres casos, el tipo de resultado es el más grande, ¡que es la respuesta final! Sin embargo, de hecho, no necesitamos comparar los tres casos, simplemente compare el Caso 2 y el Caso 3, porque estos dos casos tienen una mayor variedad de casas que el caso, y la cantidad de dinero en la casa no es negativa, por lo que hay muchas opciones. El resultado de decisión óptimo ciertamente no es pequeño .

Así que modifique ligeramente la solución anterior:

public int rob (int [] nums) { 
    int n = nums.length; 
    if (n == 1) return nums [0]; 
    return Math.max (robRange (nums, 0, n - 2),  
                    robRange (nums, 1, n - 1)); 
} 

// 仅 计算 闭 区间 [inicio, fin] 的 最优 结果
int robRange (int [] nums, int start, int end) { 
    int n = nums.length; 
    int dp_i_1 = 0, dp_i_2 = 0; 
    int dp_i = 0; 
    for (int i = end; i> = start; i--) { 
        dp_i = Math.max (dp_i_1, nums [i] + dp_i_2); 
        dp_i_2 = dp_i_1; 
        dp_i_1 = dp_i; 
    } 
    return dp_i; 
}

En este punto, también se ha resuelto la segunda cuestión.

Ladrón de casas III

La tercera pregunta logró cambiar de opinión nuevamente: el ladrón descubrió que la casa a la que se enfrentaba no era una fila, ni un círculo, ¡sino un árbol binario! La casa está en el nodo del árbol binario y las dos casas conectadas no pueden ser robadas al mismo tiempo. De hecho, es un crimen legendario de alta inteligencia:

9a1b4661c90d296a60d61921628717c2.jpg

El pensamiento general no ha cambiado en absoluto, sigue siendo la opción de agarrar o no agarrar y optar por la elección con mayor beneficio. Incluso podemos escribir el código directamente según esta rutina:

Map <TreeNode, Integer> memo = new HashMap <> (); 
public int rob (TreeNode root) { 
    if (root == null) return 0; 
    // Use memo para eliminar 
    subproblemas superpuestos if (memo.containsKey (root))  
        return memo.get (root); 
    // agarrar, y luego ir a la siguiente casa 
    int do_it = root.val 
        + (root.left == null?  
            0: rob (root.left.left) + rob (root.left.right )) 
        + (root.right == null?  
            0: rob (root.right.left) + rob (root.right.right)); 
    // No agarres , luego ve a la siguiente casa 
    int not_do = rob (root.left) + rob (root.right); 

    int res = Math.max (do_it, not_hacer); 
    memo.put (root, res); 
    return res; 
}

Este problema está resuelto, la complejidad del tiempo es O (N), el N número de nodos.

PD: He escrito más de 100 artículos originales con cuidado y me he cepillado mano a mano con 200 preguntas de hebilla, todas las cuales se publican en  la hoja de trucos del algoritmo de labuladong , que se actualiza continuamente . Se recomienda recopilar, cepillar las preguntas en el orden de mis artículos , dominar varias rutinas de algoritmos y luego lanzarlas al mar de preguntas.

Pero el punto inteligente de esta pregunta es que hay soluciones más hermosas. Por ejemplo, la siguiente es una solución que vi en el área de comentarios:

int rob (TreeNode root) { 
    int [] res = dp (root); 
    return Math.max (res [0], res [1]); 
} 

/ * devuelve una matriz de tamaño 2 arr 
arr [0] significa que no Si toma root, la cantidad máxima de dinero que puede obtener 
arr [1] representa la cantidad máxima de dinero que obtiene si toma root * / 
int [] dp (TreeNode root) { 
    if (root == null) 
        return new int [] {0, 0 }; 
    int [] left = dp (root.left); 
    int [] right = dp (root.right); 
    // Si agarras , no puedes agarrar la siguiente casa 
    int rob = root.val + left [0] + right [ 0]; 
    // No agarrar, la siguiente casa se puede agarrar o no, dependiendo del tamaño de los ingresos 
    int not_rob = Math.max (izquierda [0], izquierda [1]) 
                + Math.max (derecha [0], derecha [1] ); 

    return new int [] {not_rob, rob}; 
}

La complejidad del tiempo es O (N), y la complejidad del espacio es solo el espacio requerido por la pila de funciones recursivas, sin el espacio adicional del memo.

Ves que su pensamiento es diferente al nuestro. Modificó la definición de la función recursiva y modificó ligeramente el pensamiento para hacer que la lógica sea autoconsistente, y aún así obtuvo la respuesta correcta, y el código es más hermoso. Esta es una característica del problema de programación dinámica que mencionamos en el artículo anterior "Diferentes definiciones producen diferentes soluciones".

De hecho, esta solución se ejecuta mucho más rápido que nuestra solución, aunque la complejidad temporal del nivel de análisis del algoritmo es la misma. La razón es que esta solución no utiliza notas adicionales, lo que reduce la complejidad de las operaciones de datos, por lo que la eficiencia operativa real será más rápida.


Supongo que te gusta

Origin blog.51cto.com/15064450/2570830
Recomendado
Clasificación