La combinación de programación dinámica y estructura de datos-árbol DP

Este artículo se originó en una cuenta pública personal: TechFlow, el original no es fácil, busca atención


Hoy es el 15º en algoritmo y estructura de datos , y el 4º en la serie de programación dinámica.

He estado hablando sobre el problema de la mochila en los artículos anteriores. No sé si te sientes un poco cansado. Aunque hay nueve mochilas en el artículo clásico, además de los concursantes, podemos entender que la optimización monótona ya es muy buena. Al igual que el problema de la mochila con la dependencia y el problema de la mochila híbrida, algunas espadas se inclinan, por lo que no compartiré mucho aquí. Si está interesado, puede consultar la Baidu Backpack 9. Usted mismo hoy veremos una pregunta interesante. A través de esta pregunta interesante, echemos un vistazo al método de programación dinámica en una estructura de árbol .

El significado de esta pregunta es muy simple: dado un árbol, no es necesariamente un árbol binario. Las ramas del árbol están ponderadas y pueden considerarse como longitud. ¿Cuál es la longitud del enlace más largo en el árbol?

Por ejemplo, si dibujamos un árbol a la mano, puede ser feo, no lo culpe:

Si lo miramos a simple vista e intentamos encontrar un poco la respuesta, el camino más largo debería ser el rojo en la imagen a continuación:

Pero, ¿y si dejamos que el algoritmo funcione?

En realidad, hay una manera muy inteligente de resolver este problema. No hablemos de ello primero. Echemos un vistazo a cómo la programación dinámica resuelve este problema.

Árbol DP

La programación dinámica no solo funciona en matrices, de hecho, la programación dinámica se puede usar siempre que cumpla las condiciones para la transición de estado y no tenga efectos secundarios de la programación dinámica, independientemente de la estructura de datos. Lo mismo es cierto en el árbol. Después de entender esto, solo quedan dos preguntas. La primera pregunta es ¿cuál es el estado, y la segunda pregunta es cómo transferir entre estados?

En el problema de la mochila anterior, el estado es el volumen actual de la mochila, y la transferencia es nuestra decisión de tomar un nuevo artículo. Pero esta vez tenemos que hacer una programación dinámica en el árbol, relativamente hablando, el estado y la transición correspondiente estarán ocultos. No importa, ordenaré las ideas desde el principio y explicaré la derivación y el proceso de pensamiento poco a poco.

En primer lugar, todos sabemos que la transición entre estados es esencialmente un proceso de cálculo del todo por local . Nos movemos a través de subestados relativamente fáciles y obtenemos el resultado general. Esta es la esencia de la programación dinámica: hasta cierto punto, también está cerca del método de divide y vencerás, y existe una relación lógica entre los problemas grandes y los problemas pequeños. Entonces, cuando nos enfrentamos a un gran problema, podemos aprender del método de dividir y conquistar y pensar en comenzar con pequeños problemas.

Por lo tanto, veamos la situación más simple, de pequeña a grande, de micro a macro:

En este caso, es obvio que solo hay un enlace, por lo que la longitud es naturalmente 5 + 6 = 11, que obviamente también es la longitud más larga. No hay ningún problema en este caso. Hagamos la situación un poco más complicada. Agreguemos otra capa al árbol:

Esta imagen es un poco más complicada, pero el camino no es difícil de encontrar, debería ser EBFH . La longitud total de la ruta es 12:

Pero si cambiamos la longitud del camino, por ejemplo, si alargamos el camino de FG y FH, ¿qué resultado obtendremos?

Obviamente la respuesta cambiará en este caso, FGH es la más larga.

Este ejemplo es solo para ilustrar un problema muy simple, que para un árbol, la ruta más larga por encima de él no necesariamente pasa a través del nodo raíz . Por ejemplo, en el ejemplo anterior, si la ruta debe pasar por B, la longitud más larga solo se puede construir como 4 + 2 + 16 = 22, pero si no puede pasar por B, puede obtener la longitud más larga de 31.

Puede parecer inútil sacar esta conclusión, pero en realidad es muy útil para nosotros resolver nuestras ideas. Como no podemos garantizar que el camino más largo pase definitivamente por la raíz del árbol, no podemos transferir directamente la respuesta. Que debemos hacer

Solo responder esta pregunta no es suficiente, aún necesitamos observar y pensar profundamente.

Proceso de transferencia

Veamos las siguientes dos imágenes:

¿Encontraste algún patrón?

Como nuestra estructura de datos tiene forma de árbol, esta ruta más larga, independientemente de los dos nodos que conecte, puede garantizarse que pasará por el nodo raíz de un subárbol . No subestimes esta humilde conclusión, de hecho es muy importante. Con esta conclusión, cortamos toda la ruta en el nodo raíz.

Después de la división, obtuvimos dos enlaces al nodo hoja . La pregunta es, hay muchos enlaces desde el nodo raíz al nodo hoja. ¿Por qué son estos dos?

Es simple, porque estos dos enlaces son los más largos. Entonces, después de sumar de esta manera , se puede garantizar el enlace más largo . Estos dos enlaces son del nodo hoja a A, por lo que el enlace más largo que obtenemos es la ruta más larga del subárbol con A como el nodo raíz.

Nuestro análisis anterior indicó que el camino más largo no se puede transferir, pero se puede transferir la distancia más larga a la hoja . Tomemos un ejemplo:

La distancia más larga entre F y las hojas es obviamente la mayor de 5 y 6, B es un poco más complicado, y D y E son ambos nodos de las hojas, lo cual es fácil de entender. También tiene un nodo hijo F, que no es un nodo hoja para F, pero hemos calculado que la distancia más larga desde F al nodo hoja es 6, por lo que la distancia más larga desde B al nodo hoja a través de F es 2 + 6 = 8) De esta manera, obtenemos la ecuación de transición de estado, pero lo que transferimos no es la respuesta requerida sino la distancia más larga y la segunda distancia más larga desde el nodo actual al nodo hoja .

Porque solo la distancia más larga no es suficiente, porque necesitamos agregar la distancia más larga del nodo raíz a la ruta más larga para obtener la ruta más larga a través del nodo raíz, porque dijimos antes, todas las rutas deben pasar a través del nodo raíz de un subárbol . Esto no tiene sentido, pero esta condición es muy importante. Como todos los enlaces pasan a través del nodo raíz de al menos un subárbol, calculamos la ruta más larga de todos los subárboles a través del nodo raíz. ¿No es la respuesta la más larga?

A continuación demostramos este proceso:

El proceso de transferencia está marcado con un bolígrafo rosa en la figura anterior: para el nodo de hoja, la distancia más larga y la segunda distancia más larga son ambas 0, y el proceso de transferencia principal ocurre en el nodo intermedio.

El proceso de transición también es fácil de entender: para el nodo intermedio i, atravesamos todos sus nodos secundarios j, y luego mantenemos el valor máximo y el segundo valor más grande, escribimos la ecuación de transferencia de estado:

METRO un X yo = metro un X ( re yo s ( yo , j ) + M a x j ) Max_i = max (dis (i, j) + Max_j)

S e c M a x i = S e c M a x ( d i s ( i , j ) + M a x j ) SecMax_i = SecMax (dis (i, j) + Max_j)

Quiero entender la transición de estado, y el resto es el problema de codificación. Puede ser contradictorio hacer transiciones de estado en el árbol, especialmente cuando es recursivo, pero en realidad no es difícil. Escribamos el código y miremos. Primero veamos esta parte del árbol. Para simplificar la operación, podemos pensar en todos los números de nodo en el árbol como int . Para cada nodo, habrá una matriz para almacenar todos los bordes conectados a este nodo, incluido el nodo padre.

Dado que solo prestamos atención a la longitud del enlace en el árbol, y no nos importa la estructura del árbol, después de la construcción del árbol, el resultado de la raíz del árbol es el mismo, independientemente de qué punto sea el todo . Entonces, solo encontramos un nodo como el nodo raíz de todo el árbol y recurse. Para enfatizar, esta es una propiedad muy importante, porque en esencia, el árbol es un gráfico acíclico totalmente conectado no dirigido. Entonces, no importa qué nodo sea el nodo raíz, se puede conectar todo el subárbol.

Creamos una clase para almacenar información de nodo, incluida la identificación y dos longitudes más largas y segundas. Veamos el código, debería ser mucho más simple de lo que piensas.

class Node(object):
    def __init__(self, id):
        self.id = id
        # 以当前节点为根节点的子树到叶子节点的最长链路
        self.max1 = 0
        # 到叶子节点的次长链路
        self.max2 = 0
        # 与当前节点相连的边
        self.edges = []

    # 添加新边
    def add_edge(self, v, l):
        self.edges.append((v, l))


# 创建数组,存储所有的节点
nodes = [Node(id) for id in range(12)]

edges = [(0, 1, 3), (0, 2, 1), (1, 3, 1), (1, 4, 4), \
(1, 5, 2), (5, 6, 5), (5, 7, 6), (2, 8, 7), (7, 9, 2), (7, 10, 8)]

# 创建边
for edge in edges:
    u, v, l = edge
    nodes[u].add_edge(v, l)
    nodes[v].add_edge(u, l)

Como solo estamos tratando de transmitir ideas, se omite una gran cantidad de código orientado a objetos, pero debería ser suficiente para que comprendamos las ideas problemáticas.

A continuación, observamos el código para la programación dinámica en el árbol:

def dfs(u, f, ans):
    nodeu = nodes[u]
    # 遍历节点u所有的边
    for edge in nodes[u].edges:
        v, l = edge
        # 注意,这其中包括了父节点的边
        # 所以我们要判断v是不是父节点的id
        if v == f:
            continue
        # 递归,更新答案
        ans = max(ans, dfs(v, u, ans))
        nodev = nodes[v]
        # 转移最大值和次大值
        if nodev.max1 + l > nodeu.max1:
        	nodeu.max2 = nodeu.maxi1
            nodeu.max1 = nodev.max1 + l
        elif nodev.max1 + l > nodeu.max2:
            nodeu.max2 = nodev.max1 + l
    # 返回当前最优解
    return max(ans, nodeu.max1 + nodeu.max2)

Parece un DP muy complicado en forma de árbol, de hecho, el código es solo una docena de líneas, ¿es sorprendentemente simple?

Pero todavía es un tema del que se habla con frecuencia. Estas docenas de líneas de código parecen simples, pero todavía hay algunos detalles, especialmente cuando se trata de operaciones recursivas. Para los estudiantes que no están particularmente familiarizados con la recursividad, puede ser difícil. Se recomienda que pueda verificar manualmente el papel de acuerdo con la figura anterior. Creo que habrá una comprensión más profunda.

Otro enfoque

El artículo aún no ha terminado, todavía tenemos un huevo pequeño. De hecho, hay otro método para esta pregunta: este método es muy inteligente y también se presenta a todos.

Anteriormente dijimos que debido a que el árbol registra el estado conectado de los nodos, no importa qué nodo sea el nodo raíz, no afectará la longitud y la estructura de la ruta en todo el árbol. En este caso, si somos imaginativos, podemos aplastar un árbol, ¿puede verse como una cuerda o un palo de madera conectados entre sí?

Veamos la imagen a continuación:

Tenemos al punto B al punto C más cerca , y no afectará a la estructura del árbol, después de todo, esto es una arquitectura abstracta, que no está preocupado por el ángulo entre las ramas de los árboles. Podemos imaginar que recogimos el punto A , y los otros puntos se hunden debido a la gravedad, y finalmente se dibujarán en línea recta.

Por ejemplo, en la imagen de arriba, tomamos el punto A y BCD colgó. En este momento, el punto más bajo es el punto D. Luego tomamos el punto D, y el punto inferior se convierte en el punto C, entonces la distancia entre DC es el enlace más largo del árbol:

Resolvimos todo el proceso. Primero, seleccionamos al azar un punto como la raíz del árbol, y luego encontramos el punto más alejado de él. La segunda vez, seleccionamos este punto más lejano como la raíz del árbol y encontramos el punto más lejano nuevamente. La distancia entre estos dos puntos más lejanos es la respuesta.

Este enfoque es muy intuitivo, pero no puedo pensar en un método que pueda probarse rigurosamente. Los amigos reflexivos pueden dejarme un mensaje en segundo plano. Si no puede resolverlo, puede intentar conectarlos con algunas cuerdas y luego llevarlos juntos para hacer un experimento. Vea si los dos puntos obtenidos de esta manera son los dos puntos más alejados del árbol.

Finalmente, veamos el código:

def dfs(u, f, dis, max_dis, nd):
    nodeu = nodes[u]
    for edge in nodes[u].edges:
        v, l = edge
        if v == f:
            continue
        nodev = nodes[v]
        # 更新最大距离,以及最大距离的点
        if dis + l > max_dis:
            max_dis, nd = dis+l, nodev
        # 递归
        _max, _nd = dfs(v, u, dis+l, max_dis, nd)
        # 如果递归得到的距离更大,则更新
        if _max > max_dis:
            max_dis, nd = _max, _nd
    # 返回
    return max_dis, nd

# 第一次递归,获取距离最大的节点
_, nd = dfs(0, -1, 0, 0, None)
# 第二次递归,获取最大距离
dis, _ = dfs(nd.id, -1, 0, 0, None)
print(dis)

En este punto, incluso si este interesante tema está terminado, ¿sabe que ha aprendido los dos métodos en el artículo? Puede parecer un poco confuso por primera vez. Es normal tener muchos problemas, pero el principio básico no es difícil . Haga un dibujo y haga un buen cálculo, definitivamente puede obtener el resultado correcto.

El artículo de hoy es solo eso. Si siente algo recompensado, sígalo o vuelva a publicarlo . Su esfuerzo es muy importante para mí.

Inserte la descripción de la imagen aquí

101 artículos originales publicados · Me gustaron 54 · Visitantes más de 10,000

Supongo que te gusta

Origin blog.csdn.net/TechFlow/article/details/105407065
Recomendado
Clasificación