Diseño de cursos de análisis de algoritmos (4) Utilice el método de dividir y conquistar para encontrar el par de puntos y la ruta con la distancia entre dos puntos cualesquiera en el árbol menor que K

Descargo de responsabilidad

Este artículo es solo una nota de estudio personal, consúltelo con cuidado. Si hay algún error, critíquelo y corríjalo.

Articulo de referencia

El primer artículo se centra principalmente en el centro de gravedad del árbol.

El segundo artículo es exactamente igual a esta pregunta.

https://blog.csdn.net/a_forever_dream/article/details/81778649

https://blog.csdn.net/jackypigpig/article/details/69808594

Reclamación:

(1) Utilice un pseudocódigo para describir el algoritmo para encontrar el centro de gravedad del árbol.

(2) Cuando se utilice el siguiente árbol como entrada, escriba el proceso de solución y el resultado de la solución para resolver el problema anterior. Se requiere escribir el proceso de cambio de las principales variables en el proceso de solución.

(3) Escriba un programa para resolver el problema y analice la complejidad temporal del algoritmo.

análisis

todo el proceso:

1. Encuentra el centro de gravedad del árbol.

2. Calcule la matriz de distancia desde cada punto hasta el centro de gravedad.

3. Todos los pares de puntos que pasan por el centro de gravedad menos todos los pares de puntos que pasan por los subnodos del centro de gravedad para obtener el logaritmo de puntos legal.

4. Repita las operaciones 1, 2 y 3 para los nodos secundarios del centro de gravedad (recursividad)

1. Encuentra el centro de gravedad del árbol.

El centro de gravedad del árbol también se llama centro de masa del árbol. Es decir, para un nodo del árbol, el número máximo de nodos de todos los subárboles después de que se elimina (en comparación con la eliminación de otros nodos) es el más pequeño. como muestra la imagen:

Para requerir el centro de gravedad del árbol, quizás la primera idea sea atravesar todos los nodos una vez, tratar cada nodo como el centro de gravedad, calcular el número de nodos en cada subárbol y luego comparar. Pero esta ley violenta es indeseable. Calculará la misma ruta muchas veces, y podemos obtener el número máximo de nodos de subárbol con cada punto como centro de gravedad al escanear todo el árbol solo una vez. Aquí se utiliza el método del árbol para dividir y conquistar por puntos. Dividir y conquistar, lo entiendo como una llamada recursiva para el recorrido posterior al pedido, pero la recursividad solo se puede repetir desde un nodo como punto de partida. ¿Cómo podemos calcular el número máximo de nodos de subárbol a partir de todos los puntos? Aquí todavía tomamos el árbol anterior como ejemplo. Use n para representar el número de nodos en todo el árbol, donde n = 5; use size [i] para representar el número de nodos en el árbol enraizados en i; use max_child [i] para representar el número máximo de nodos de subárboles enraizados en i; use min Para actualizar el valor mínimo en max_child [i], se usa para actualizar el centro de gravedad, y el valor inicial de min es un número grande.

De acuerdo con la recursividad del recorrido posterior, primero podemos calcular el tamaño [4] = 1 del punto 4, y la parte roja es n-tamaño [4] = 4 nodos. La razón de esta división es que el punto 4 solo tiene un nodo padre, por lo que la parte roja siempre se puede usar como un árbol hijo del mismo. Comparando el tamaño de size [4] y n-size [4], el número máximo de nodos de subárbol arraigados en el punto 4 es max_child [4] = 4, y min se actualiza a 4 de max_child [4].

Luego calcule el tamaño [5] = 1 del punto 5, que es similar al punto 4. La parte roja es de tamaño n [5] = 4 nodos, el número máximo de nodos de subárbol con el punto 5 como raíz max_child [5] = 4, el mínimo no cambia.

A continuación, calcule el punto 2, que es su propio nodo, más el tamaño de sus puntos de subárbol 4 y 5. La parte verde es su subárbol, que son tamaño [4] = 1 nodo, tamaño [5] = 1 nodo, y la parte roja es n-tamaño [2] = 2 nodos, por lo que el punto 2 es El número máximo de nodos de subárbol de la raíz max_child [2] = 2 y min se actualiza a 2.

Lo siguiente es calcular el punto 3, que es 1 nodo de sí mismo. La parte roja es de tamaño n [3] = 4 nodos, por lo que el número máximo de nodos de subárbol arraigados en el punto 3 es max_child [3] = 4, min no se modifica.

A continuación, calcule el punto 1, que es su propio nodo, más el tamaño de sus puntos de subárbol 2 y 3. La parte verde son dos subárboles, tamaño [2] = 3 nodos, tamaño [3] = 1 nodos. La parte roja desapareció, tamaño n [1] = 0 nodos. Por lo tanto, el número máximo de nodos de subárbol arraigados en el punto 1 es max_child [1] = 3, y min no cambia.

Una vez que se actualiza min, el centro de gravedad también se actualizará al nodo correspondiente. Simplemente no está escrito aquí, puede ver el código.

Por lo tanto, no importa a quién elijas al comienzo de esta recursividad, si eliges cualquier punto, puedes calcular el número máximo de nodos de subárbol enraizados en cada punto y luego encontrar el centro de gravedad.

El código para encontrar el centro de gravedad del árbol es el siguiente:

// 全局变量
int n=17; // 所有结点数
int size[n];// 以n为根的树的结点数
int max_child[n];// 以n为根的树的最大子树的结点数
int min;// max_child中最小的那个
int first[n+1],edge[(n-1)*2]// 顶点表,边表
int gravity=0;// 被选为重心的结点

// 传入参数
// start 代表当前结点
// parent 代表当前结点的父结点,这里是为了防止遍历start的子结点的时候把父结点也遍历进去了
void getGravity(int start, int parent)
{
    // size[start]代表当前结点的个数,初始为1是算上本身
    size[start]=1;
    // max_child[start]代表当前结点的最大子树结点数
    max_child[start]=0;
    // 遍历以start结点为起点的所有边(除去连接父结点的边)
    for(int i=first[start]; i; i=edge[i].next)
    {
        // end为当前边的终点(也是start点的子结点)
        int end=edge[i].end;
        // 如果这个终点已经被遍历或者这个终点是父结点,就跳过
        if(visited[end] || end==parent){
            continue;
        }
        // 继续遍历终点的子结点,这里其实就是遍历start的一棵子树的所有结点数
        getGravity(end, start);
        // 遍历完这棵子树的所有结点后,把子树的结点数加起来
        size[start]+=size[end];
        // 如果这颗子树的结点数大于max_child,就更新它
        if(size[end] > max_child[start]){
            max_child[start]=size[end];
        }
    }
    // 上面的循环是用来遍历start的每一棵子树并比较出最大子树结点
    // 接下来就是算“红色部分”,也就是n-size[i]部分的结点数,并比较出最终的最大子树结点
    if(n-size[start] > max_child[start]){
        max_child[start]=n-size[start];
    }
    // 从max_child中比较出最小的,以找出重心
    if(min < max_child[start]){
        min=max_child[x];
        gravity=start;
    }
}

El método de almacenamiento para el árbol aquí es usar la tabla de vértices primero para almacenar cada punto, y primero [i] representa el número del primer borde correspondiente al nodo numerado i. Cada fila del borde de la mesa de borde representa un borde, incluido el punto de inicio, el punto final, el peso de este borde y el siguiente borde con el mismo punto de inicio. Debido a que estos bordes no están dirigidos, un borde se almacena dos veces en la tabla de bordes.

El código para agregar nodos y bordes de árbol es el siguiente:

int first[n+1],edge[(n-1)*2]// 顶点表,边表
int num=0;
void addNodeAndEdge(int start,int end,int weight)
{
    num++;// 编号,没错,要从1开始
    edge[num].start=start;// 起点
    edge[num].end=end;// 终点
    edge[num].weight=weight;// 权重
    edge[num].next=first[start];// 下一条相同起点的边
    first[start]=num;// 加入顶点
}

2. Calcule la matriz de distancia desde cada punto hasta el centro de gravedad.

Después de seleccionar el centro de gravedad del árbol, calculamos la distancia (es decir, el peso) desde todos los puntos hasta este centro de gravedad, también utilizando un método recursivo. Esto debería ser fácil de entender, sin demasiadas explicaciones.

int t=0;
// start是传入的点,parent是start的父结点,weight是start和parent连线的权重
// parent在这里是为了防止遍历start的子结点的时候把父结点也遍历进去了
// 因为每个点到start的距离是自上而下地累加,所以传入weight
// 该递归函数的主要作用是返回每个点到重心的距离,所以一开始调用递归函数的时候
// start默认是重心,parent和weight默认是0。
void getDistance(int start, int parent, int weight)
{
    // dis数组保存了每个点到重心的距离(权重),为什么用t来做下标而不是点的编号呢
    // 因为后面的做法只用数点对的个数,不在乎是谁到谁
    // t是从1开始的
    dis[++t]=weight;
    for(int i=first[start]; i; i=edge[i].next)
    {
        int end=edge[i].end;
        // 如果这个终点已经被遍历或者这个终点是父结点,就跳过
        // 这点很重要,因为如果传入的start不是根结点而是根结点的子树的时候
        // 它就不会把根结点再遍历一次
        if(visited[end] || end==parent){
            continue;
        }
        getDistance(end, start, weight+edge[i].weight);
    }
}

En este paso, obtenemos la distribución y la distancia desde cada punto al centro de gravedad se almacena en dis. Nuevamente, el subíndice de dis no tiene nada que ver con el número de nodo. Tome el ejemplo anterior, como se muestra en la siguiente figura:

3. Todos los pares de puntos que pasan por el centro de gravedad menos todos los pares de puntos que pasan por los subnodos del centro de gravedad para obtener el logaritmo de puntos legal.

A continuación, considere el número de caminos cuya longitud es menor que K. Mi primera idea es encontrar los puntos menores o iguales a K en dis, de modo que seleccionamos el logaritmo puntual cuya distancia del punto al centro de gravedad es menor que K, y luego calculamos el logaritmo puntual que pasa por el centro de gravedad y la distancia es menor que K. Pero según el segundo artículo de referencia, este no es el caso. El segundo artículo de referencia en la sección de citas:

Después de (obtener la matriz de distribución), las rutas conectadas en este subárbol pasarán a través del centro de gravedad y contribuirán a la respuesta (es decir, el par de puntos ( i,j) ( i<j) cuya distancia es menor que k ) será así:  dis[i]+dis[j] <= K y después de eliminar el centro de gravedad, i y j  no son En el mismo bloque Unicom .

Pero, obviamente, es un poco incómodo satisfacer la condición de "no en el mismo bloque de interconexión", así que hay un pequeño truco: independientemente de si la condición está en un bloque de interconexión o no, calcule el número de rutas coincidentes del árbol actual y luego obtenga El número de, menos la distancia de punto a camino (pasando el centro de gravedad) en el subárbol enraizado por el nodo hijo del centro de gravedad es menor o igual que el número de K, y es suficiente.

Significa que después de tener ahora la distancia desde cada punto al centro de gravedad, clasificamos dis de pequeño a grande (la clasificación es para calcular los pares de puntos más pequeños que K), y los sumamos en pares, y sumamos todos los puntos excepto el centro de gravedad. Haga clic en todas las combinaciones, como:

dis [2] + dis [5] corresponde a 2——1——3;

dis [3] + dis [5] corresponde a 4——2——1——3;

Pero hay una situación que no es buena, como dis [3] + dis [4] corresponde a 4——2——1——2——5.

Los puntos 4 y 5 no necesitaban pasar el punto 1 del centro de gravedad. ¿Cómo se puede eliminar esto? Primero averigüe las características de este par de puntos. Es fácil encontrar que estos pares de puntos están todos en un subárbol. Tanto el 4 como el 5 están conectados al centro de gravedad a través del 2. En otras palabras, si un par de puntos pasa a través del nodo hijo del centro de gravedad, entonces es ilegal. Con esta condición de juicio, podemos eliminarlo.

Después de comprender el método de eliminación, ahora hablemos de la función de trabajo. Sus parámetros entrantes son la distancia (peso) desde el inicio del nodo y el inicio hasta el nodo principal.

Se utiliza para calcular todas las combinaciones hasta el punto de inicio (se cuentan las legales e ilegales). En la primera función dfs, tenemos un centro de gravedad. La función de trabajo será llamada una vez por el centro de gravedad, el inicio entrante es el centro de gravedad y el peso es 0, y se calculan todos los pares de puntos que pasan por el centro de gravedad. De vuelta a dfs, atraviesa todos los nodos secundarios del centro de gravedad y llama al trabajo por separado. El inicio entrante es el nodo secundario y el peso es el peso desde el nodo secundario hasta el centro de gravedad, y calcula todos los pares de puntos que pasan por el nodo secundario actual, aunque es Después del par de puntos del nodo hijo, la distancia calculada sigue siendo hasta el centro de gravedad. Porque si es un par de puntos que pasa por el centro de gravedad o un par de puntos que pasa por el nodo hijo, deben compararse con k, por lo que la distancia que calculan debe ser hasta el centro de gravedad.

// 传入的start要么是重心,weight=0
// 要么是重心的孩子结点,weight是重心与孩子结点的距离(权重)
int work(int start,int weight) {
    t=0;
    // 如果start是重心,算出 以重心为根的树中的结点 到start的距离,然后两两组合,选出加起来小于k的点对
    // 如果start是重心的孩子结点,算出 以该孩子结点为根 的子树中的结点 到重心的距离,然后两两组合,选出加起来小于k的点对
    // eg:重心是S的孩子结点是a,那么算出的点对数是 以a为根的树 的所有结点 两两组合,但是距离算的是 所有结点 到S的距离,因为仍然要判断小于k,和经过重心的点对要一致
    // getDistance需要传入weight就是为了 重心的孩子结点的 孩子结点的 dis是到重心的距离
    getDistance(start, 0, weight);
    // 得到dis数组后,对其进行从小到大排序
    // 注意,这里的t已经不是0了,它是全局变量,在getDistance里面遍历了start出发的所有结点
    sort(dis+1,dis+1+t);
    // pair_num表示经过重心的点对数量
    int pair_num=0;
    int i=1,j=t;
    // 这个while循环就是把两个dis相加的和小于等于K的点对数量计算出来
    while (i<j){
        while (i<j && dis[i]+dis[j]>K) 
            j--;
        pair_num+=j-i;
        i++;
    }
    return pair_num;
}

4. Repita las operaciones 1, 2 y 3 para los nodos secundarios del centro de gravedad (recursividad)

La primera vez que la función dfs a menudo pasa en un nodo a voluntad para encontrar el centro de gravedad de todo el árbol, como el punto 1 en la figura anterior. Aquí, usará el punto 1 para llamar a la función de trabajo para encontrar todos los logaritmos puntuales hasta el punto 1, incluidos los legales e ilegales. Luego atraviesa los nodos secundarios del punto 1 en el bucle for, que son los puntos 2 y 3 en la figura anterior. Usa el punto 2 y el punto 3 para llamar al trabajo nuevamente para encontrar todos los pares de puntos que pasan por los puntos 2 y 3 respectivamente. , Y luego restado por ans, obtienes un par de puntos legal.

Podría pensar que 4 y 5 son pares de puntos menores que k, pero si los resta, ¿desaparecerá? Entonces, después de restar, el punto 2 y el punto 3 se pasan a dfs respectivamente para la recursividad, y se contará el par del punto 4 y el punto 5. ans es una variable global y continuará acumulándose y restando después del primer dfs.

// 起始点是start,递归调用会dfs所有的结点
void dfs(int start){
    // 以start为起点找到重心
    // 注意,虽然一开始我们说了从树的任何一个点开始遍历都能找出一个确定的重心,
    // 但是这里从start开始,如果它有父结点,就不要再遍历的,只找以它为根的树的重心
    getGravity(start,0);
    // 用这个重心算出所有跨过该重心的路径数
    ans += work(gravity,0);
    // 标记这个重心已访问
    visited[gravity]=1;
    // 从重心开始访问子结点
    for(int i=first[start]; i; i=edge[i].next){
        int end=edge[i].end;
        if (visited[end]){
            continue;
        }
        // 减去以子结点为根的树的所有跨过子结点的路径数
        ans -= work(end, edge[i].weight);
        // 从子结点开始继续递归
        dfs(end);
    }
    return;
}

Luego ponga la función principal:

int main()
{
    // 输入结点个数
    scanf("%d",&n);
    // 输入每个结点的信息
    for(int i=1;i<n;i++)
    {
        int start, end, weight;
        scanf("%d %d %d",&start,&end,&weight);
        addNodeAndEdge(start,end,weight);
        addNodeAndEdge(end,start,weight);
    }
    dfs(1);
    printf("%d\n", ans);
    return 0;
}

Finalmente, el código general, que no ha sido probado, se debe principalmente a que no hay tiempo, pero entiendo completamente el código y es suficiente para la prueba.

#include <stdio.h>
#define MAX 10000;

// 全局变量
int n; // 所有结点数
int size[n],max_child[n],min=MAX;// 以n为根的树的结点数,以n为根的树的最大子树的结点数
int first[n+1],edge[(n-1)*2]// 顶点表,边表
int visited[n+1];// 标记已访问过的点
int dis[];// 每个点到重心的距离
int gravity=0;// 重心
int num=0,t=0;// 结点编号
int ans=0;// 最终结果:小于等于K的点对数量

void addNodeAndEdge(int start, int end, int weight)
{
    num++;// 编号
    edge[num].start=start;// 起点
    edge[num].end=end;// 终点
    edge[num].weight=weight;// 权重
    edge[num].next=first[start];// 下一条相同起点的边
    first[start]=num;// 加入顶点
}

// 传入重心,以及重心和它的父结点之间的权重
int work(int start,int weight) {
    t=0;
    // 算出start到各个结点的距离
    getDistance(start, 0, weight);
    // 得到dis数组后,对其进行从小到大排序
    // 注意,这里的t已经不是0了,它是全局变量,在getDistance里面遍历了start出发的所有结点
    sort(dis+1,dis+1+t);
    // pair_num表示点对数量
    int pair_num=0;
    int i=1,j=t;
    // 这个while循环就是把两个dis相加的和小于等于K的点对数量计算出来
    while (i<j){
        while (i<j && dis[i]+dis[j]>K) j--;
        pair_num+=j-i;
        i++;
    }
    return pair_num;
}

void getDistance(int start, int parent, int weight)//fa表示x的父亲,z表示x到目标点的距离
{
    // dis数组保存了每个点到重心的距离(权重),为什么用t来做下标而不是点的编号呢
    // 因为后面的做法只用数点对的个数,不在乎是谁到谁
    dis[++t]=weight;
    for(int i=first[start]; i; i=edge[i].next)
    {
        int end=edge[i].end;
        // 如果这个终点已经被遍历或者这个终点是父结点,就跳过
        // 这点很重要,因为如果传入的start不是根结点而是根结点的子树的时候
        // 它就不会把根结点再遍历一次
        if(visited[end] || end==parent){
            continue;
        }
        getDistance(end, start, weight+edge[i].weight);
    }
}
// 传入参数
// start 代表当前结点
// parent 代表当前结点的父结点,这里是为了防止遍历start的子结点的时候把父结点也遍历进去了
void getGravity(int start, int parent)
{
    // size[start]代表当前结点的个数,初始为1是算上本身
    size[start]=1;
    // max_child[start]代表当前结点的最大子树结点数
    max_child[start]=0;
    // 遍历以start结点为起点的所有边(除去连接父结点的边)
    for(int i=first[start]; i; i=edge[i].next)
    {
        // end为当前边的终点(也是start点的子结点)
        int end=edge[i].end;
        // 如果这个终点已经被遍历或者这个终点是父结点,就跳过
        if(visited[end] || end==parent){
            continue;
        }
        // 继续遍历终点的子结点,这里其实就是遍历start的一棵子树的所有结点数
        getGravity(end, start);
        // 遍历完这棵子树的所有结点后,把子树的结点数加起来
        size[start]+=size[end];
        // 如果这颗子树的结点数大于max_child,就更新它
        if(size[end] > max_child[start]){
            max_child[start]=size[end];
        }
    }
    // 上面的循环是用来遍历start的每一棵子树并比较出最大子树结点
    // 接下来就是算“红色部分”,也就是n-size[i]部分的结点数,并比较出最终的最大子树结点
    if(n-size[start] > max_child[start]){
        max_child[start]=n-size[start];
    }
    // 从max_child中比较出最小的,以找出重心
    if(min < max_child[start]){
        min=max_child[x];
        gravity=start;
    }
}

// 递归地求每个树的经过重心的点对数量
// 起始点是start
void dfs(int start){
    // 以start为起点找到重心
    // 注意,虽然一开始我们说了从树的任何一个点开始遍历都能找出一个确定的重心,
    // 但是这里的意思是,从start开始,它的父结点就不要再遍历的,只找它和它的子树的重心
    getGravity(start,0);
    // 用这个重心算出所有跨过该重心的路径数
    ans += work(gravity,0);
    // 标记这个重心已访问
    visited[gravity]=1;
    // 从重心开始访问子结点
    for(int i=first[start]; i; i=edge[i].next){
        int end=edge[i].end;
        if (visited[end]){
            continue;
        }
        // 减去以子结点为根的树的所有跨过子结点的路径数
        ans -= work(end, edge[i].weight);
        // 以子结点为根的树继续求重心、所有跨过路径数
        dfs(end);
    }
    return;
}

int main()
{
    // 输入结点个数
    scanf("%d",&n);
    // 输入每个结点的信息
    for(int i=1;i<n;i++)
    {
        int start, end, weight;
        scanf("%d %d %d",&start,&end,&weight);
        addNodeAndEdge(start,end,weight);
        addNodeAndEdge(end,start,weight);
    }
    dfs(1);
    printf("%d\n", ans);
    return 0;
}

La complejidad del tiempo no se calcula temporalmente. . .

Supongo que te gusta

Origin blog.csdn.net/qq_33514421/article/details/112379820
Recomendado
Clasificación