1.题目简述
假设有 个顶点的无向带权连通图,节点编号为 到 。有一个整数数组 ,其中 ,表示节点 和 之间有一条边权为 的边。其中部分边权为 ,其他边权都为正整数。
要求是将所有边权为 的边都修改为 范围内的正整数,使得从源点 source 到 终点 destination 的最短距离为整数 D。
如果存在该方案,则返回包含所有边的数组;否则返回一个空数组。
例子:
输入:n = 5, edges = [[4,1,-1],[2,0,-1],[0,3,-1],[4,3,-1]], source = 0, destination = 1, target = 5
输出:[[4,1,1],[2,0,1],[0,3,3],[4,3,1]]
解释:上图展示了一个满足题意的修改方案,从 0 到 1 的最短距离为 5 。
详情请查看 LeetCode 官网:2699. 修改图中的边权 - 力扣(LeetCode)
2.分析
- 假设从 s 到 t 的最短路径为 ,此时某一条边的权值增加 ;如果该边在最短路径中,则最短路径值变为 ;如果该边不在最短路径中,则最短路径值仍然为
- 根据题意可知,当某条边的权值大于 时,该边一定不在最短路径范围内,因为这样的路径的权值和必大于 ,不符合题目要求;因此对于边权的修改范围可以缩小至
- 不难理解,当将所有边权为 的边的权值修改为 时,所能找到的最短路径一定是最小的;当修改为 时,所能找到的最短路径一定是边权范围内所能找到的最大的
3.方法
3.1 计算最短路径
计算最短路径的方法主要有两种: 迪杰斯特拉(Dijkstra)算法 和 弗洛伊德(Floyd)算法。本方法采用 Dijkstra 方法。
关于该方法的详解,请看:数据结构(Java版) - 图 - 掘金 (juejin.cn)
3.2 遍历修改情况
假设图中边权为 的边一共有 条,根据修改范围 ,我们可以得到所有的修改情况:
共计 种。从上到下分别编号 为 。
根据例子中的图,按上面的顺序的边权情况,分别计算最短路径,可以得到下面的趋势图:
根据以上的趋势图可以知道遍历上面的边权情况,其最短路径的值是以递增的方式变化的,因此为了提高计算效率,可以通过使用 二分查找 的方法寻找答案。
3.3 构造邻接矩阵
- 如果节点 和 之间不存在边,则
- 如果节点 和 之间存在边,且权值 ,则
- 如果节点 和 之间存在边,且权值 ,则根据 计算权值
- 如果 小于 ,则权值为 ,然后
- 如果 大于等于 ,则权值为 ,然后
例子解析: 根据例子中的图及其条件,可知其边权情况和对应编号如下所示:
| 边权情况 | idx | 边权情况 | idx | |
|---|---|---|---|---|
| [1,1,1,1] | 0 | [2,1,1,1] | 1 | |
| [3,1,1,1] | 2 | [4,1,1,1] | 3 | |
| [5,1,1,1] | 4 | [5,2,1,1] | 5 | |
| [5,3,1,1] | 6 | [5,4,1,1] | 7 | |
| [5,5,1,1] | 8 | [5,5,2,1] | 9 | |
| [5,5,3,1] | 10 | [5,5,4,1] | 11 | |
| [5,5,5,1] | 12 | [5,5,5,2] | 13 | |
| [5,5,5,3] | 14 | [5,5,5,4] | 15 | |
| [5,5,5,5] | 16 |
假设 ,,此时的 :
- 由于 ,所以 ,
- 由于 ,所以 ,
- 由于 ,所以 ,
- 由于 ,所以
4.代码
public int[][] modifiedGraphEdges(int n, int[][] edges, int source, int destination, int D) {
// 计算权为 -1 的边数
int k = 0;
for (int[] e : edges) {
if (e[2] == -1) {
++k;
}
}
// 可修改边都为 1 时,其最短路径都大于 D,则不存在答案
if (dijkstra(source, destination, construct(n, edges, 0, D)) > D) {
return new int[0][];
}
// 可修改边都为 D 时,其最短路径都小于 D,则所有路径都小于 D,不存在答案
if (dijkstra(source, destination, construct(n, edges, (long) k * (D - 1), D)) < D) {
return new int[0][];
}
// 二分查找遍历边权情况
long left = 0, right = (long) k * (D - 1), ans = 0;
while (left <= right) {
long mid = (left + right) / 2;
if (dijkstra(source, destination, construct(n, edges, mid, D)) >= D) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
// 重新构造边表
for (int[] e : edges) {
if (e[2] == -1) {
if (ans >= D - 1) {
e[2] = D;
ans -= D - 1;
} else {
e[2] = (int) (1 + ans);
ans = 0;
}
}
}
return edges;
}
/**
* Dijkstra 算法
* @param source 源点
* @param destination 终点
* @param matrix 邻接矩阵
* @return 最短路径
*/
public long dijkstra(int source, int destination, int[][] matrix) {
int n = matrix.length;
long[] dist = new long[n];
Arrays.fill(dist, Integer.MAX_VALUE / 2);
boolean[] used = new boolean[n];
dist[source] = 0;
for (int round = 0; round < n - 1; ++round) {
int u = -1;
// 找到一个未访问的,并且从源点到该点距离最小的点
for (int i = 0; i < n; ++i) {
if (!used[i] && (u == -1 || dist[i] < dist[u])) {
u = i;
}
}
used[u] = true;
// 从 u 点到其他各个点的路径
for (int v = 0; v < n; ++v) {
if (!used[v] && matrix[u][v] != -1) {
dist[v] = Math.min(dist[v], dist[u] + matrix[u][v]);
}
}
}
return dist[destination];
}
/**
* 构造邻接矩阵
* @param n 节点数
* @param edges 边表
* @param idx 编号
* @param D 目标路径和
* @return 邻接矩阵
*/
public int[][] construct(int n, int[][] edges, long idx, int D) {
// 需要构造出第 idx 种不同的边权情况,返回一个邻接矩阵
// 修改的边权范围为 [1,D];因为 D 以上的边权不可能出现在要求最短路径为 D 的路径中
// 根据 idx 构造 k*(D-1)+1 种情况
int[][] adjMatrix = new int[n][n];
for (int i = 0; i < n; ++i) {
Arrays.fill(adjMatrix[i], -1);
}
for (int[] e : edges) {
int u = e[0], v = e[1], w = e[2];
if (w != -1) {
adjMatrix[u][v] = adjMatrix[v][u] = w;
} else {
// 修改边权为 -1 的边
if (idx >= D - 1) {
adjMatrix[u][v] = adjMatrix[v][u] = D;
idx -= (D - 1);
} else {
adjMatrix[u][v] = adjMatrix[v][u] = (int) (1 + idx);
idx = 0;
}
}
}
return adjMatrix;
}