【算法】【学习笔记】最短路问题
偷懒得很,大部分都使用了AI精修。
最短路问题一般有3种解法:
- 无权图直接用 BFS;
- 有权正边图用 Dijkstra;
- 动态规划;
- 游戏大地图/寻路系统优先选 A*。
解题算法
Dijkstra算法
Dijkstra 算法的核心思想是贪心 + 边松弛:每次选出当前未确定最短路中累积距离最小的节点,利用它去更新其邻居节点的距离,直到所有节点都处理完毕。
核心要素与机制
- 距离数组
dist[]:记录从起点到每个节点的当前已知最短距离。起点设为0,其余节点初始化为无穷大。 - 小顶堆 / 优先队列(Min-Heap):用于在 \(O(\log V)\) 时间内快速取出当前
dist值最小的节点。 - 松弛操作(Relaxation):如果通过节点 \(u\) 到达邻居 \(v\) 的距离更短,即
dist[u] + weight < dist[v],则更新dist[v] = dist[u] + weight并将 \((dist[v], v)\) 存入堆中。
具体执行步骤
- 初始化:
dist[start] = 0,将(0, start)压入小顶堆。 - 弹出当前最近节点:从小顶堆弹出当前累计距离最小的节点 \(u\) 及其距离 \(d\)。
- 去重检查:如果 \(d > dist[u]\),说明它是一条已经被更短路径替代的“过期数据”,直接跳过。
- 松弛更新:遍历 \(u\) 的所有邻居 \(v\),若
dist[u] + weight(u, v) < dist[v],更新dist[v]并将(dist[v], v)入堆。 - 循环:重复步骤 2~4,直到堆为空。
C++模板
#include <vector>
#include <queue>
using namespace std;
struct Edge {
int to;
int weight;
};
vector<int> dijkstra(int n, const vector<vector<Edge>>& graph, int start) {
vector<int> dist(n, 1e9); // 初始化为无穷大
// 小顶堆:存储 pair<当前距离, 节点编号>
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
dist[start] = 0;
pq.push({0, start});
while (!pq.empty()) {
auto [d, u] = pq.top();
pq.pop();
// 懒删除:如果弹出的距离已经大于当前记录的最短距离,直接跳过
if (d > dist[u]) continue;
for (const auto& edge : graph[u]) {
int v = edge.to;
int weight = edge.weight;
// 松弛操作
if (dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
pq.push({dist[v], v});
}
}
}
return dist;
}
复杂度与致命限制
- 时间复杂度:\(O((V + E) \log V)\),其中 \(V\) 是节点数,\(E\) 是边数。
- 空间复杂度:\(O(V + E)\),邻接表及堆空间。
- 致命限制:严禁存在负权边。如果图中有负权边,贪心选择的最短节点后续可能被负权拉得更小,导致算法失效,遇到负权边需改用 Bellman-Ford / SPFA 算法。
❔题目
1. 网络延迟时间
解题思路
这个就是要找每个节点,到k的最短距离,所以使用Dijkstra算法。
- 单源最短路:信号从单一固定的起点 \(K\) 发出。
- 边权非负:传递时间 \(w_i\) 为非负数,完全符合 Dijkstra 的使用条件。
- 问题转化:所有节点都收到信号所需要的总时间,等价于信号到达离起点 \(K\) 最远的那个节点所消耗的时间。
即用 Dijkstra 求出 \(K\) 到各个节点的最短距离 dist 数组后,取其中的最大值 \(\max(\text{dist})\)。如果有任何节点无法到达(dist 仍为无穷大),则返回 -1。
注意:节点编号是从 \(1\) 到 \(n\),建图和分配数组大小时需开到 n + 1。
实现
function networkDelayTime(times: number[][], n: number, k: number): number {
// 1. 建图:邻接表 graph[u] = [[v, w], ...]
const graph: [number, number][][] = Array.from({ length: n + 1 }, () => []);
for (const [u, v, w] of times) {
graph[u].push([v, w]);
}
// 2. 初始化距离数组为无穷大
const dist = new Array(n + 1).fill(Infinity);
dist[k] = 0;
// 3. 朴素/小顶堆 Dijkstra
// 队列中存 [节点编号, 当前距离]
const pq = new MinPriorityQueue<[number, number]>({
compare: (a, b) => a[1] - b[1]
});
pq.enqueue([k, 0]);
while (!pq.isEmpty()) {
const [u, d] = pq.dequeue();
if (d > dist[u]) continue;
for (const [v, w] of graph[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.enqueue([v, dist[v]]);
}
}
}
// 4. 获取 1 到 n 节点中最长的时间
let maxDist = 0;
for (let i = 1; i <= n; i++) {
if (dist[i] === Infinity) return -1;
maxDist = Math.max(maxDist, dist[i]);
}
return maxDist;
}
public class Solution {
public int NetworkDelayTime(int[][] times, int n, int k) {
// 1. 建图:邻接表
var graph = new List<(int to, int weight)>[n + 1];
for (int i = 1; i <= n; i++) {
graph[i] = new List<(int, int)>();
}
foreach (var t in times) {
graph[t[0]].Add((t[1], t[2]));
}
// 2. 初始化距离数组
int[] dist = new int[n + 1];
Array.Fill(dist, int.MaxValue / 2); // 避免加法溢出
// 3. 优先队列 (小顶堆):PriorityQueue<Element, Priority>
var pq = new PriorityQueue<int, int>();
dist[k] = 0;
pq.Enqueue(k, 0);
while (pq.Count > 0) {
pq.TryDequeue(out int u, out int d);
if (d > dist[u]) continue;
foreach (var edge in graph[u]) {
int v = edge.to;
int w = edge.weight;
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.Enqueue(v, dist[v]);
}
}
}
// 4. 寻找最大延时
int maxDist = 0;
for (int i = 1; i <= n; i++) {
if (dist[i] >= int.MaxValue / 2) return -1;
maxDist = Math.Max(maxDist, dist[i]);
}
return maxDist;
}
}
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
// 1. 建图:邻接表
vector<vector<pair<int, int>>> graph(n + 1);
for (const auto& t : times) {
graph[t[0]].push_back({t[1], t[2]});
}
// 2. 初始化距离数组与小顶堆
vector<int> dist(n + 1, 1e9);
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
dist[k] = 0;
pq.push({0, k}); // pair<当前距离, 节点编号>
// 3. Dijkstra 过程
while (!pq.empty()) {
auto [d, u] = pq.top();
pq.pop();
if (d > dist[u]) continue;
for (const auto& edge : graph[u]) {
int v = edge.first;
int w = edge.second;
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
// 4. 统计结果
int maxDist = 0;
for (int i = 1; i <= n; ++i) {
if (dist[i] == 1e9) return -1;
maxDist = max(maxDist, dist[i]);
}
return maxDist;
}
};
复杂度分析
- 时间复杂度:\(O((V + E) \log V)\)
其中 \(V = n\),\(E\) 为 times 数组的长度。 - 空间复杂度:\(O(V + E)\)
用于存储邻接表、dist 数组以及优先队列。
2. 包含要求路径的最小带权子图
解题思路
原始思路:
用广度搜索,为src1和scr2搜索路,然后结合成图。
但是这是不行的,广度搜索无法处理带权最短路,这里应该使用Dijkstra算法。
为了让总边权最小,从 \(src1\) 和 \(src2\) 出发的两条路径应该尽可能多地重叠。
我们可以假设它们在某个中间节点 \(m\)(\(m\) 可以是图中任意节点,包括起点或终点本身)相遇,然后合并为一条路径共同前往 \(dest\)。
此时总开销公式为:
只需要遍历所有可能的汇合点 \(m \in [0, n-1]\),求出上述三项之和的最小值即可。
可以看出我们要搜索三项距离,所以使用 3 次 Dijkstra。
- 正向图 \(G\):运行 Dijkstra 从 \(src1\) 出发,得到数组 \(d_1\),其中 \(d_1[m]\) 表示 \(src1 \to m\) 的最短距离。
- 正向图 \(G\):运行 Dijkstra 从 \(src2\) 出发,得到数组 \(d_2\),其中 \(d_2[m]\) 表示 \(src2 \to m\) 的最短距离。
- ⭐反向图 \(G^R\):将原图的所有有向边反向(\(u \to v\) 变成 \(v \to u\)),然后运行 Dijkstra 从 \(dest\) 出发,得到数组 \(d_{dest}\)。其中 \(d_{dest}[m]\) 刚好等于原图中 \(m \to dest\) 的最短距离!
注意:由于边权可能累加很大(最大可达 \(10^{10}\)),C# 和 C++ 中必须使用 64位整数(long / long long) 存储距离,防止数据溢出。
实现
function minimumWeight(n: number, edges: number[][], src1: number, src2: number, dest: number): number {
// 1. 建正向图 g 和反向图 rg
const g: [number, number][][] = Array.from({ length: n }, () => []);
const rg: [number, number][][] = Array.from({ length: n }, () => []);
for (const [u, v, w] of edges) {
g[u].push([v, w]);
rg[v].push([u, w]); // 边的方向反转
}
// 通用 Dijkstra 函数
function dijkstra(start: number, graph: [number, number][][]): number[] {
const dist = new Array(n).fill(Infinity);
dist[start] = 0;
const pq = new MinPriorityQueue<[number, number]>({ compare: (a, b) => a[1] - b[1] });
pq.enqueue([start, 0]);
while (!pq.isEmpty()) {
const [u, d] = pq.dequeue();
if (d > dist[u]) continue;
for (const [v, w] of graph[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.enqueue([v, dist[v]]);
}
}
}
return dist;
}
// 2. 跑三次 Dijkstra
const d1 = dijkstra(src1, g);
const d2 = dijkstra(src2, g);
const dDest = dijkstra(dest, rg);
// 3. 枚举所有可能的汇合点 m
let ans = Infinity;
for (let m = 0; m < n; m++) {
if (d1[m] !== Infinity && d2[m] !== Infinity && dDest[m] !== Infinity) {
ans = Math.min(ans, d1[m] + d2[m] + dDest[m]);
}
}
return ans === Infinity ? -1 : ans;
}
public class Solution {
public long MinimumWeight(int n, int[][] edges, int src1, int src2, int dest) {
var g = new List<(int to, int weight)>[n];
var rg = new List<(int to, int weight)>[n];
for (int i = 0; i < n; i++) {
g[i] = new List<(int, int)>();
rg[i] = new List<(int, int)>();
}
foreach (var e in edges) {
g[e[0]].Add((e[1], e[2]));
rg[e[1]].Add((e[0], e[2]));
}
long[] Dijkstra(int start, List<(int to, int weight)>[] graph) {
long[] dist = new long[n];
Array.Fill(dist, long.MaxValue / 2);
var pq = new PriorityQueue<int, long>();
dist[start] = 0;
pq.Enqueue(start, 0);
while (pq.Count > 0) {
pq.TryDequeue(out int u, out long d);
if (d > dist[u]) continue;
foreach (var edge in graph[u]) {
int v = edge.to;
int w = edge.weight;
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.Enqueue(v, dist[v]);
}
}
}
return dist;
}
long[] d1 = Dijkstra(src1, g);
long[] d2 = Dijkstra(src2, g);
long[] dDest = Dijkstra(dest, rg);
long INF = long.MaxValue / 2;
long ans = INF;
for (int m = 0; m < n; m++) {
if (d1[m] < INF && d2[m] < INF && dDest[m] < INF) {
ans = Math.Min(ans, d1[m] + d2[m] + dDest[m]);
}
}
return ans >= INF ? -1 : ans;
}
}
class Solution {
public:
long long minimumWeight(int n, vector<vector<int>>& edges, int src1, int src2, int dest) {
vector<vector<pair<int, int>>> g(n), rg(n);
for (const auto& e : edges) {
g[e[0]].push_back({e[1], e[2]});
rg[e[1]].push_back({e[0], e[2]});
}
const long long INF = 1e18;
auto dijkstra = [&](int start, const vector<vector<pair<int, int>>>& graph) {
vector<long long> dist(n, INF);
priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<pair<long long, int>>> pq;
dist[start] = 0;
pq.push({0, start});
while (!pq.empty()) {
auto [d, u] = pq.top();
pq.pop();
if (d > dist[u]) continue;
for (const auto& [v, w] : graph[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
return dist;
};
auto d1 = dijkstra(src1, g);
auto d2 = dijkstra(src2, g);
auto dDest = dijkstra(dest, rg);
long long ans = INF;
for (int m = 0; m < n; ++m) {
if (d1[m] < INF && d2[m] < INF && dDest[m] < INF) {
ans = min(ans, d1[m] + d2[m] + dDest[m]);
}
}
return ans == INF ? -1 : ans;
}
};
复杂度分析
- 时间复杂度:\(O((V + E) \log V)\)
三次 Dijkstra 的时间复杂度均为 \(O((V + E) \log V)\),后续遍历 \(n\) 个汇合点仅需 \(O(V)\),完全能通过大数据量。 - 空间复杂度:\(O(V + E)\)
用于构建正反向邻接表与保存三次 Dijkstra 的结果数组。
3. 设计可以求最短路径的图类
解题思路
原始思路:
直接每次找最短路时使用dijkstra算法。但这个方法并不快。
使用 Floyd-Warshall 算法(弗洛伊德算法),并结合了动态增量更新(Incremental Update)技巧,用于求解全源最短路径(All-Pairs Shortest Path)。
其核心机制分为三部分:
- 全局预计算(constructor):
- 使用 \(O(n^3)\) 的三重循环动态规划,求出图中任意两点之间的最短距离矩阵 g。
- 代码里的外层循环 i 充当中间过渡节点(即经典的 \(k\) 节点),状态转移方程为:
\[g[j][k] = \min(g[j][k], g[j][i] + g[i][k]) \] - 动态加边增量更新(addEdge):
- 当插入一条新边 \(u \to v\)(权重为 \(w\))时,无需重新执行 \(O(n^3)\) 的完整算法,而是只需 \(O(n^2)\) 时间检查所有点对 \((i, j)\)。
- 若从 \(i \to u \to v \to j\) 的路径比原有路径更短,则直接更新:$$g[i][j] = \min(g[i][j], g[i][u] + w + g[v][j])$$
- 常数级查询(shortestPath):
- 由于之前已经计算并维护好了所有点对的最短距离,查询时直接查表返回 g[node1][node2],时间复杂度为 \(O(1)\)。
适用场景:节点数量较小(如 \(n \le 100\))、需要频繁添加边,且需要极速响应两点间最短路查询的情况。
实现
class Graph {
private n: number;
private dist: number[][];
private readonly INF = 1e9;
constructor(n: number, edges: number[][]) {
this.n = n;
this.dist = Array.from({ length: n }, () => new Array(n).fill(this.INF));
for (let i = 0; i < n; i++) {
this.dist[i][i] = 0;
}
for (const [u, v, w] of edges) {
this.dist[u][v] = Math.min(this.dist[u][v], w);
}
// Floyd-Warshall O(n^3)
for (let k = 0; k < n; k++) {
for (let i = 0; i < n; i++) {
if (this.dist[i][k] === this.INF) continue; // 此时i到k还没有路,剪枝
for (let j = 0; j < n; j++) {
this.dist[i][j] = Math.min(this.dist[i][j], this.dist[i][k] + this.dist[k][j]);
}
}
}
}
addEdge(edge: number[]): void {
const [u, v, w] = edge;
if (w >= this.dist[u][v]) return; // 新边权不优,直接忽略
for (let i = 0; i < this.n; i++) {
// 剪枝 1:如果 i 到不了 u,直接跳过内层对 j 的所有循环
if (this.dist[i][u] === this.INF) continue;
for (let j = 0; j < this.n; j++) {
// 剪枝 2:如果 v 到不了 j,跳过本次更新
if (this.dist[v][j] === this.INF) continue;
// 安全更新
if (this.dist[i][u] + w + this.dist[v][j] < this.dist[i][j]) {
this.dist[i][j] = this.dist[i][u] + w + this.dist[v][j];
}
}
}
}
shortestPath(node1: number, node2: number): number {
const res = this.dist[node1][node2];
return res >= this.INF ? -1 : res; // O(1) 查询
}
}
public class Graph {
private int n;
private int[,] dist;
private const int INF = 1_000_000_000;
public Graph(int n, int[][] edges) {
this.n = n;
dist = new int[n, n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dist[i, j] = (i == j) ? 0 : INF;
}
}
foreach (var edge in edges) {
int u = edge[0], v = edge[1], w = edge[2];
dist[u, v] = Math.Min(dist[u, v], w);
}
// Floyd-Warshall O(n^3)
for (int k = 0; k < n; k++) {
for (int i = 0; i < n; i++) {
if (dist[i, k] == INF) continue; // 剪枝
for (int j = 0; j < n; j++) {
dist[i, j] = Math.Min(dist[i, j], dist[i, k] + dist[k, j]);
}
}
}
}
public void AddEdge(int[] edge) {
int u = edge[0], v = edge[1], w = edge[2];
if (w >= dist[u, v]) return;
// 动态增量更新 O(n^2)
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dist[i, j] = Math.Min(dist[i, j], dist[i, u] + w + dist[v, j]);
}
}
}
public int ShortestPath(int node1, int node2) {
return dist[node1, node2] >= INF ? -1 : dist[node1, node2]; // O(1) 查询
}
}
class Graph {
private:
int n;
vector<vector<int>> dist;
const int INF = 1e9;
public:
Graph(int n, vector<vector<int>>& edges) : n(n) {
dist.assign(n, vector<int>(n, INF));
for (int i = 0; i < n; ++i) {
dist[i][i] = 0;
}
for (const auto& edge : edges) {
int u = edge[0], v = edge[1], w = edge[2];
dist[u][v] = min(dist[u][v], w);
}
// Floyd-Warshall O(n^3)
for (int k = 0; k < n; ++k) {
for (int i = 0; i < n; ++i) {
if (dist[i][k] == INF) continue; // 剪枝
for (int j = 0; j < n; ++j) {
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
}
void addEdge(vector<int> edge) {
int u = edge[0], v = edge[1], w = edge[2];
if (w >= dist[u][v]) return;
// 动态增量更新 O(n^2)
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
dist[i][j] = min(dist[i][j], dist[i][u] + w + dist[v][j]);
}
}
}
int shortestPath(int node1, int node2) {
return dist[node1][node2] >= INF ? -1 : dist[node1][node2]; // O(1) 查询
}
};
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
constructor |
\(O(n^3)\) | \(O(n^2)\) | 三重循环遍历 \((k, i, j)\) 预计算全图最短路;开辟 \(n \times n\) 距离矩阵。 |
addEdge |
\(O(n^2)\) | \(O(1)\) | 只需要双重循环遍历所有节点对 \((i, j)\),尝试用新边做松弛更新,与当前有多少条边无关。 |
shortestPath |
\(O(1)\) | \(O(1)\) | 直接查表 dist[node1][node2],常数级响应。 |
4. 阈值距离内邻居最少的城市
解题思路
原思路:
需要一个dist[][]数组,存放每个城市在某个距离里可以到的城市数啊,用动态规划。
但这很明显写不动。
dist[i][j] 存城市 \(i\) 到城市 \(j\) 的最短路径距离。
建立出全图的最短距离矩阵后,统计每行满足 dist[i][j] <= distanceThreshold 的数量,就能找出目标城市。
因为题目限制 \(n \le 100\),用动态规划思想的 Floyd-Warshall 算法 预处理全图最短路是最方便的做法。
解题步骤
- 构建距离矩阵:
初始化 \(n \times n\) 的 dist 数组,对角线 dist[i][i] = 0,其余设为无穷大 INF。 - 填充双向边:
遍历 edges,填入双向权值 dist[u][v] = dist[v][u] = w。 - Floyd 动态规划:
三重循环求任意两点间的最短路:dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])。 - 统计与筛选:
遍历每个城市 \(i\),统计满足 dist[i][j] <= distanceThreshold 的城市 \(j\) 数量(\(j \neq i\))。更新邻居数量最少的城市;若数量相等,更新为编号更大的城市(即遍历时取 count <= minCount)。
实现
function findTheCity(n: number, edges: number[][], distanceThreshold: number): number {
const INF = 1e9;
const dist: number[][] = Array.from({ length: n }, () => new Array(n).fill(INF));
for (let i = 0; i < n; i++) dist[i][i] = 0;
// 填充无向图邻接矩阵
for (const [u, v, w] of edges) {
dist[u][v] = w;
dist[v][u] = w;
}
// 1. Floyd-Warshall DP 求任意两点最短路
for (let k = 0; k < n; k++) {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
// 2. 统计每个城市在阈值内的邻居数
let minCount = INF;
let ans = -1;
for (let i = 0; i < n; i++) {
let count = 0;
for (let j = 0; j < n; j++) {
if (i !== j && dist[i][j] <= distanceThreshold) {
count++;
}
}
// count <= minCount 确保了数量相同时,优先选择编号更大的城市 i
if (count <= minCount) {
minCount = count;
ans = i;
}
}
return ans;
}
public class Solution {
public int FindTheCity(int n, int[][] edges, int distanceThreshold) {
const int INF = 1_000_000_000;
int[,] dist = new int[n, n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dist[i, j] = (i == j) ? 0 : INF;
}
}
foreach (var e in edges) {
dist[e[0], e[1]] = e[2];
dist[e[1], e[0]] = e[2];
}
// Floyd-Warshall
for (int k = 0; k < n; k++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dist[i, j] = Math.Min(dist[i, j], dist[i, k] + dist[k, j]);
}
}
}
int minCount = INF;
int ans = -1;
for (int i = 0; i < n; i++) {
int count = 0;
for (int j = 0; j < n; j++) {
if (i != j && dist[i, j] <= distanceThreshold) {
count++;
}
}
if (count <= minCount) {
minCount = count;
ans = i;
}
}
return ans;
}
}
class Solution {
public:
int findTheCity(int n, vector<vector<int>>& edges, int distanceThreshold) {
const int INF = 1e9;
vector<vector<int>> dist(n, vector<int>(n, INF));
for (int i = 0; i < n; ++i) dist[i][i] = 0;
for (const auto& e : edges) {
dist[e[0]][e[1]] = e[2];
dist[e[1]][e[0]] = e[2];
}
// Floyd-Warshall
for (int k = 0; k < n; ++k) {
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
int minCount = INF;
int ans = -1;
for (int i = 0; i < n; ++i) {
int count = 0;
for (int j = 0; j < n; ++j) {
if (i != j && dist[i][j] <= distanceThreshold) {
count++;
}
}
if (count <= minCount) {
minCount = count;
ans = i;
}
}
return ans;
}
};
复杂度分析
- 时间复杂度:\(O(n^3)\)
三重循环预处理最短距离。 - 空间复杂度:\(O(n^2)\)
构建 \(n \times n\) 的距离矩阵。
引用
[1] 力扣探索模式
注:本文为个人学习与刷题笔记,部分文本结构与排版格式由 AI 辅助整理。

浙公网安备 33010602011771号