当前位置:首页 > 技术 > 正文内容

图论最短路径算法:高级应用与扩展

访客 技术 2026年9月29日 8

最短路径算法进阶应用

本文旨在探讨最短路径算法(特别是Dijkstra算法)的几种进阶应用与扩展技巧,包括最短路径计数、奇偶性最短路径问题以及点集中任意两点间的最短路径求解。

1. 最短路径计数

在标准Dijkstra算法的基础上,我们可以扩展其功能以统计从起点到图中每个顶点的最短路径总数。这在某些场景下,仅仅知道最短距离是不够的,还需要知道达到该距离有多少种不同的方式。

核心思想: 在Dijkstra的松弛操作中,除了更新距离数组外,还需要维护一个用于记录路径数量的数组,例如path_counts。

转移逻辑:

  • 当发现一条严格更短的路径:如果从当前节点u经过边(u, v)到达节点v的距离dist[u] + weight小于当前dist[v],则说明找到了新的最短路径。此时,不仅要更新dist[v],还要将path_counts[v]的值直接设置为path_counts[u],因为所有通过旧路径到达v的方式都被新路径取代了。
  • 当发现一条等长的最短路径:如果dist[u] + weight等于dist[v],这意味着发现了一条与已知最短路径等长的新路径。此时,应将path_counts[u]累加到path_counts[v]上,因为两种路径都提供了最短的到达方式。结果通常需要对一个大质数取模。

#include <iostream>
#include <vector>
#include <queue>
#include <limits> // For numeric_limits

const long long INF = std::numeric_limits<long long>::max();
const int MOD = 100003; // 模数
const int MAX_NODES_COUNT = 100005; // 最大节点数

struct Edge {
    int to_node;
    int weight; // 边权重
};

// 优先队列中的状态:节点ID和当前累积距离
struct DijkstraState {
    int node_id;
    long long current_dist;

    // 比较器,用于构建最小堆
    bool operator<(const DijkstraState& other) const {
        return current_dist > other.current_dist;
    }
};

std::vector<Edge> adjacency_list[MAX_NODES_COUNT];
long long min_distances[MAX_NODES_COUNT];
int path_counts[MAX_NODES_COUNT];
int num_graph_nodes, num_graph_edges;

void dijkstra_shortest_path_count(int start_node) {
    for (int i = 1; i <= num_graph_nodes; ++i) {
        min_distances[i] = INF;
        path_counts[i] = 0;
    }

    std::priority_queue<DijkstraState> pq;

    min_distances[start_node] = 0;
    path_counts[start_node] = 1; // 起点到自身的最短路径为1条(距离0)
    pq.push({start_node, 0});

    while (!pq.empty()) {
        DijkstraState current_state = pq.top();
        pq.pop();

        int u = current_state.node_id;
        long long d_u = current_state.current_dist;

        // 如果已找到更短路径,则忽略当前状态
        if (d_u > min_distances[u]) {
            continue;
        }

        for (const auto& edge : adjacency_list[u]) {
            int v = edge.to_node;
            int weight = edge.weight;

            if (min_distances[v] > d_u + weight) {
                min_distances[v] = d_u + weight;
                path_counts[v] = path_counts[u]; // 继承前驱节点的路径数
                pq.push({v, min_distances[v]});
            } else if (min_distances[v] == d_u + weight) {
                // 发现等长最短路径,累加路径数
                path_counts[v] = (path_counts[v] + path_counts[u]) % MOD;
            }
        }
    }
}

/*
// 示例用法
int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    std::cin >> num_graph_nodes >> num_graph_edges;
    for (int i = 0; i < num_graph_edges; ++i) {
        int u, v;
        std::cin >> u >> v;
        // 假设无向图且边权为1
        adjacency_list[u].push_back({v, 1});
        adjacency_list[v].push_back({u, 1});
    }

    dijkstra_shortest_path_count(1); // 从节点1开始

    for (int i = 1; i <= num_graph_nodes; ++i) {
        std::cout << path_counts[i] << std::endl;
    }

    return 0;
}
*/
  

2. 状态拆分:奇偶最短路径

在某些图论问题中,路径的长度不仅仅关注其数值大小,还可能涉及到路径包含的边数(步数)的奇偶性。例如,如果一个进程需要在L阶段完成,并且其相邻节点需要在L-1阶段完成,这会引入一个关于路径长度奇偶性的依赖关系。起点(阶段0)作为原材料提供者,需要判断某个节点在L阶段生产零件时,是否能在奇偶性匹配的步数内从起点接收到原材料。

核心思路: 这种问题可以通过Dijkstra算法的状态拆分来解决。我们将每个节点的状态拆分为两个:到达该节点的"最短偶数步路径"和"最短奇数步路径"。

算法实现:

  • 定义一个二维距离数组,例如min_steps[node_id][parity],其中parity为0表示偶数步路径,1表示奇数步路径。初始化为无穷大。
  • 在Dijkstra算法的优先队列中,存储的状态也需要包含当前路径的奇偶性,或者可以通过累计步数来推导。当从节点u通过一条边到达节点v时,如果当前路径到u的步数是P_u,边的权重(步数)是1,那么到达v的步数就是P_u + 1,其奇偶性会反转。
  • 松弛操作时,根据新路径的奇偶性更新对应的min_steps[v][new_parity]。

#include <iostream>
#include <vector>
#include <queue>
#include <cstring> // For memset

const int INF_STEPS = 0x3f3f3f3f; // 表示无穷大步数
const int MAX_NODES_PARITY = 100005;

struct GraphLink {
    int target_node;
    int step_cost; // 边的步数成本,通常为1
};

// 优先队列中的状态:节点ID和当前累积步数
struct ParityPathState {
    int node_id;
    int total_accumulated_steps;

    bool operator<(const ParityPathState& other) const {
        return total_accumulated_steps > other.total_accumulated_steps;
    }
};

std::vector<GraphLink> graph_adj[MAX_NODES_PARITY];
// min_steps[node][0] 存储到达节点node的最短偶数步路径长度
// min_steps[node][1] 存储到达节点node的最短奇数步路径长度
int min_steps[MAX_NODES_PARITY][2];

int N_parity_nodes, M_parity_edges, Q_parity_queries;

void dijkstra_parity_shortest_path(int start_node) {
    // 初始化所有节点的奇偶步数距离为无穷大
    for (int i = 1; i <= N_parity_nodes; ++i) {
        min_steps[i][0] = INF_STEPS;
        min_steps[i][1] = INF_STEPS;
    }

    std::priority_queue<ParityPathState> pq;

    min_steps[start_node][0] = 0; // 起点到自身0步,为偶数
    pq.push({start_node, 0});

    while (!pq.empty()) {
        ParityPathState current = pq.top();
        pq.pop();

        int u = current.node_id;
        int current_total_steps = current.total_accumulated_steps;
        int current_parity = current_total_steps % 2;

        // 如果当前取出的路径已劣于已知最短路径,则跳过
        if (current_total_steps > min_steps[u][current_parity]) {
            continue;
        }
        
        for (const auto& link : graph_adj[u]) {
            int v = link.target_node;
            int step_cost = link.step_cost;

            int next_total_steps = current_total_steps + step_cost;
            int next_parity = next_total_steps % 2; // 步数奇偶性反转

            if (min_steps[v][next_parity] > next_total_steps) {
                min_steps[v][next_parity] = next_total_steps;
                pq.push({v, next_total_steps});
            }
        }
    }
}

/*
// 示例用法
int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    std::cin >> N_parity_nodes >> M_parity_edges >> Q_parity_queries;
    for (int i = 0; i < M_parity_edges; ++i) {
        int u, v;
        std::cin >> u >> v;
        // 假设边权为1,表示1步
        graph_adj[u].push_back({v, 1});
        graph_adj[v].push_back({u, 1});
    }

    dijkstra_parity_shortest_path(1); // 从节点1开始

    for (int i = 0; i < Q_parity_queries; ++i) {
        int target_node, required_L_stage;
        std::cin >> target_node >> required_L_stage;
        // 检查是否存在到 target_node 的最短路径,其步数与required_L_stage奇偶性相同
        // 且步数小于等于 required_L_stage
        if (min_steps[target_node][required_L_stage % 2] <= required_L_stage) {
            std::cout << "Yes\n";
        } else {
            std::cout << "No\n";
        }
    }

    return 0;
}
*/
  

3. 点集中任意两点的最短路径

给定一个图(可能是有向图)和一个包含 k 个特殊节点的集合,目标是找出这个集合中任意两个不同节点之间的最短路径的最小值。由于直接对每对特殊节点运行最短路径算法(如k次Dijkstra)可能效率低下,特别是当k很大时,我们需要更优化的方法。

方法一:二进制分组结合超级源汇点

思路: 这种方法通过巧妙地将特殊节点分组,并引入"超级源点"和"超级汇点"来加速计算。其核心思想是,任何一对特殊节点(x, y),其ID的二进制表示至少有一位不同。我们可以利用这一点来设计分组策略。

实现步骤:

  1. 迭代所有可能的二进制位(例如,对于18位整数,从0到17)。
  2. 在每次迭代中,根据特殊节点ID的当前二进制位是0还是1,将其分入两个集合A和B。
  3. 构建临时图:
    • 创建一个"超级源点"S(例如节点ID 0),并从S向集合A中的所有特殊节点连接权重为0的边。
    • 创建一个"超级汇点"T(例如节点ID N+1),并从集合B中的所有特殊节点向T连接权重为0的边。
  4. 在构建好的图上运行一次Dijkstra算法,计算从超级源点S到超级汇点T的最短路径。这个距离即为从集合A到集合B的最短路径。
  5. 对于有向图,由于路径方向性,还需要反转分组角色(即源点连B,A连汇点),再运行一次Dijkstra,以覆盖所有可能的B到A路径。
  6. 每次迭代结束后,清除超级源点和超级汇点的临时边,为下一次迭代做准备。
  7. 通过遍历所有二进制位,可以确保任意一对不同的特殊节点,至少有一次会被分到不同的集合中,从而其最短路径能够被计算到。全局答案取所有Dijkstra结果的最小值。

#include <iostream>
#include <vector>
#include <queue>
#include <limits>
#include <algorithm> // For std::min

const long long INF_PATH = std::numeric_limits<long long>::max();
const int MAX_NODES_GROUP = 100005; // 最大节点数,额外预留给超级源汇点

struct AdjEntry {
    int target;
    int weight;
};

// 优先队列中的状态:节点ID和当前累积距离
struct PriorityQueueState {
    int node_idx;
    long long distance_val;

    bool operator<(const PriorityQueueState& other) const {
        return distance_val > other.distance_val;
    }
};

std::vector<AdjEntry> graph_adj_list[MAX_NODES_GROUP + 2]; // +2 for super source/sink
int designated_special_nodes[MAX_NODES_GROUP];
long long overall_min_path_len = INF_PATH;
int N_group_nodes, M_group_edges, K_group_special_nodes;

long long run_dijkstra_with_virtual_nodes(int super_source, int super_sink) {
    std::vector<long long> current_distances(N_group_nodes + 2, INF_PATH);
    std::priority_queue<PriorityQueueState> pq;

    current_distances[super_source] = 0;
    pq.push({super_source, 0});

    while (!pq.empty()) {
        PriorityQueueState current = pq.top();
        pq.pop();

        int u = current.node_idx;
        long long d_u = current.distance_val;

        if (d_u > current_distances[u]) { // 已找到更短路径
            continue;
        }

        for (const auto& entry : graph_adj_list[u]) {
            int v = entry.target;
            int weight = entry.weight;

            if (current_distances[v] > d_u + weight) {
                current_distances[v] = d_u + weight;
                pq.push({v, current_distances[v]});
            }
        }
    }
    return current_distances[super_sink];
}

void solve_binary_grouping() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    std::cin >> N_group_nodes >> M_group_edges >> K_group_special_nodes;
    for (int i = 0; i < M_group_edges; ++i) {
        int u, v, w;
        std::cin >> u >> v >> w;
        graph_adj_list[u].push_back({v, w});
    }
    for (int i = 1; i <= K_group_special_nodes; ++i) {
        std::cin >> designated_special_nodes[i];
    }

    int virtual_super_source = 0; // 使用0作为超级源点
    int virtual_super_sink = N_group_nodes + 1; // 使用 N+1 作为超级汇点

    // 遍历二进制位的每一位 (通常到17或18位,取决于节点ID的最大值)
    for (int bit_pos = 0; bit_pos < 18; ++bit_pos) {
        // 清除上一轮添加的虚拟边
        graph_adj_list[virtual_super_source].clear();
        // 需要清除从特殊节点到超级汇点的虚拟边。
        // 原代码通过pop_back实现,这里也遵循此逻辑,但需注意其潜在风险。
        // 更健壮的做法是使用单独的容器存储虚拟边,并在Dijkstra中特殊处理。

        // ====== 第一方向:从二进制位为0的特殊节点到二进制位为1的特殊节点 ======
        // 构建虚拟边
        for (int i = 1; i <= K_group_special_nodes; ++i) {
            int current_node_id = designated_special_nodes[i];
            if ((current_node_id >> bit_pos) & 1) { // 如果当前位是1,连接到超级汇点
                graph_adj_list[current_node_id].push_back({virtual_super_sink, 0});
            } else { // 如果当前位是0,从超级源点连接
                graph_adj_list[virtual_super_source].push_back({current_node_id, 0});
            }
        }
        overall_min_path_len = std::min(overall_min_path_len, 
                                         run_dijkstra_with_virtual_nodes(virtual_super_source, virtual_super_sink));

        // 清除虚拟边(第一方向)
        graph_adj_list[virtual_super_source].clear();
        for (int i = 1; i <= K_group_special_nodes; ++i) {
            int current_node_id = designated_special_nodes[i];
            if ((current_node_id >> bit_pos) & 1) {
                // 移除从 current_node_id 到 virtual_super_sink 的虚拟边
                graph_adj_list[current_node_id].pop_back(); 
            }
        }

        // ====== 第二方向:从二进制位为1的特殊节点到二进制位为0的特殊节点 (针对有向图) ======
        // 重新构建虚拟边(角色反转)
        for (int i = 1; i <= K_group_special_nodes; ++i) {
            int current_node_id = designated_special_nodes[i];
            if ((current_node_id >> bit_pos) & 1) { // 如果当前位是1,现在从超级源点连接
                graph_adj_list[virtual_super_source].push_back({current_node_id, 0});
            } else { // 如果当前位是0,现在连接到超级汇点
                graph_adj_list[current_node_id].push_back({virtual_super_sink, 0});
            }
        }
        overall_min_path_len = std::min(overall_min_path_len, 
                                         run_dijkstra_with_virtual_nodes(virtual_super_source, virtual_super_sink));

        // 清除虚拟边(第二方向)
        graph_adj_list[virtual_super_source].clear();
        for (int i = 1; i <= K_group_special_nodes; ++i) {
            int current_node_id = designated_special_nodes[i];
            if (!((current_node_id >> bit_pos) & 1)) { // 如果当前位是0,移除连接到超级汇点的虚拟边
                graph_adj_list[current_node_id].pop_back();
            }
        }
    }

    std::cout << overall_min_path_len << std::endl;

    // 清理所有图数据,以防多组测试用例
    for (int i = 0; i <= N_group_nodes + 1; ++i) {
        graph_adj_list[i].clear();
    }
}

/*
int main() {
    // 调用 solve_binary_grouping()
    solve_binary_grouping();
    return 0;
}
*/
  

方法二:正反图结合多源Dijkstra与染色

思路: 这种方法的核心是利用两次多源Dijkstra算法,分别在原图和反向图上运行,并记录每个节点的最短路径来源(或去向)的特殊节点,然后通过遍历所有边来合并结果。

实现步骤:

  1. 正向Dijkstra(从特殊点出发):
    • 初始化一个距离数组dist_from_special[node_id]和一个来源标记数组source_node_origin[node_id]。
    • 将所有特殊节点以距离0压入优先队列。
    • 运行Dijkstra算法。当松弛操作更新dist_from_special[v]时,同时将source_node_origin[v]设置为当前路径的来源特殊节点source_node_origin[u]。
  2. 反向Dijkstra(到特殊点结束):
    • 构建原图的反向图(即所有边方向反转)。
    • 初始化一个距离数组dist_to_special[node_id]和一个目标标记数组target_node_origin[node_id]。
    • 将所有特殊节点以距离0压入优先队列(在反向图上相当于从它们出发)。
    • 运行Dijkstra算法。当松弛操作更新dist_to_special[v]时,同时将target_node_origin[v]设置为当前路径的终点特殊节点target_node_origin[u]。
  3. 合并答案:
    • 遍历原图中的所有边(u, v),权重为w。
    • 对于每一条边,检查条件source_node_origin[u] != target_node_origin[v]。这个条件确保了从特殊节点source_node_origin[u]到u,再通过边(u, v),最后从v到达特殊节点target_node_origin[v],路径的起点和终点是两个不同的特殊节点。
    • 如果条件满足,则这条路径的总长度是dist_from_special[u] + w + dist_to_special[v]。用这个值更新全局最小答案。

#include <iostream>
#include <vector>
#include <queue>
#include <limits>
#include <algorithm> // For std::min

const long long INF_DIST_COL = std::numeric_limits<long long>::max();
const int MAX_NODES_COLORED = 100005;

struct StoredEdge {
    int from;
    int to;
    int weight;
};

struct GraphNeighbor {
    int neighbor_node;
    int edge_weight;
};

// 优先队列状态:节点ID和当前累积距离
struct PriorityState {
    int node_id;
    long long accumulated_distance;

    bool operator<(const PriorityState& other) const {
        return accumulated_distance > other.accumulated_distance;
    }
};

std::vector<GraphNeighbor> adj_forward_graph[MAX_NODES_COLORED];
std::vector<GraphNeighbor> adj_backward_graph[MAX_NODES_COLORED]; // 反向图
StoredEdge all_original_edges[MAX_NODES_COLORED]; // 存储所有原始边,用于最后遍历

long long dist_from_special[MAX_NODES_COLORED]; // 从特殊节点出发的最短距离
long long dist_to_special[MAX_NODES_COLORED];   // 到特殊节点的最短距离 (在反向图上计算)
int source_node_origin[MAX_NODES_COLORED];      // 记录路径的原始特殊起点
int target_node_origin[MAX_NODES_COLORED];      // 记录路径的最终特殊终点
int list_of_key_special_nodes[MAX_NODES_COLORED];

int N_colored_nodes, M_colored_edges, K_colored_special_nodes;

void multi_source_dijkstra_forward() {
    std::priority_queue<PriorityState> pq;

    for (int i = 1; i <= N_colored_nodes; ++i) {
        dist_from_special[i] = INF_DIST_COL;
        source_node_origin[i] = 0; // 0 表示尚未被任何特殊节点触达
    }

    for (int i = 1; i <= K_colored_special_nodes; ++i) {
        int s_node = list_of_key_special_nodes[i];
        pq.push({s_node, 0});
        dist_from_special[s_node] = 0;
        source_node_origin[s_node] = s_node; // 自身是特殊起点
    }

    while (!pq.empty()) {
        PriorityState current = pq.top();
        pq.pop();

        int u = current.node_id;
        long long d_u = current.accumulated_distance;

        if (d_u > dist_from_special[u]) {
            continue;
        }

        for (const auto& neighbor : adj_forward_graph[u]) {
            int v = neighbor.neighbor_node;
            int weight = neighbor.edge_weight;

            if (dist_from_special[v] > d_u + weight) {
                dist_from_special[v] = d_u + weight;
                source_node_origin[v] = source_node_origin[u]; // 传播原始特殊起点
                pq.push({v, dist_from_special[v]});
            }
        }
    }
}

void multi_source_dijkstra_backward() {
    std::priority_queue<PriorityState> pq;

    for (int i = 1; i <= N_colored_nodes; ++i) {
        dist_to_special[i] = INF_DIST_COL;
        target_node_origin[i] = 0; // 0 表示尚未被任何特殊节点触达
    }

    for (int i = 1; i <= K_colored_special_nodes; ++i) {
        int s_node = list_of_key_special_nodes[i];
        pq.push({s_node, 0});
        dist_to_special[s_node] = 0;
        target_node_origin[s_node] = s_node; // 自身是特殊终点
    }

    while (!pq.empty()) {
        PriorityState current = pq.top();
        pq.pop();

        int u = current.node_id;
        long long d_u = current.accumulated_distance;

        if (d_u > dist_to_special[u]) {
            continue;
        }

        for (const auto& neighbor : adj_backward_graph[u]) { // 使用反向图
            int v = neighbor.neighbor_node;
            int weight = neighbor.edge_weight;

            if (dist_to_special[v] > d_u + weight) {
                dist_to_special[v] = d_u + weight;
                target_node_origin[v] = target_node_origin[u]; // 传播原始特殊终点
                pq.push({v, dist_to_special[v]});
            }
        }
    }
}

void solve_multi_source_coloring() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    std::cin >> N_colored_nodes >> M_colored_edges >> K_colored_special_nodes;
    for (int i = 1; i <= M_colored_edges; ++i) {
        int u, v, w;
        std::cin >> u >> v >> w;
        all_original_edges[i] = {u, v, w}; // 存储原始边
        adj_forward_graph[u].push_back({v, w});
        adj_backward_graph[v].push_back({u, w}); // 构建反向图
    }
    for (int i = 1; i <= K_colored_special_nodes; ++i) {
        std::cin >> list_of_key_special_nodes[i];
    }

    multi_source_dijkstra_forward();
    multi_source_dijkstra_backward();

    long long overall_min_path_result = INF_DIST_COL;

    for (int i = 1; i <= M_colored_edges; ++i) {
        const auto& edge = all_original_edges[i];
        int u = edge.from;
        int v = edge.to;
        int w = edge.weight;

        // 检查路径是否合法:
        // 1. u和v都必须可达自/达至特殊节点 (即 source_node_origin[u] 和 target_node_origin[v] 不为0)
        // 2. 路径的起始特殊节点和终止特殊节点必须不同
        // 3. 避免无穷大距离相加导致溢出
        if (source_node_origin[u] != 0 && target_node_origin[v] != 0 &&
            source_node_origin[u] != target_node_origin[v]) {
            
            if (dist_from_special[u] != INF_DIST_COL && dist_to_special[v] != INF_DIST_COL) {
                 overall_min_path_result = std::min(overall_min_path_result, dist_from_special[u] + w + dist_to_special[v]);
            }
        }
    }

    std::cout << overall_min_path_result << std::endl;

    // 清理所有图数据,以防多组测试用例
    for (int i = 0; i <= N_colored_nodes; ++i) {
        adj_forward_graph[i].clear();
        adj_backward_graph[i].clear();
    }
}

/*
int main() {
    // 调用 solve_multi_source_coloring()
    solve_multi_source_coloring();
    return 0;
}
*/
  

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。