LeetCode - 2699.修改图中的边权

362 阅读4分钟

1.题目简述

假设有 nn 个顶点的无向带权连通图,节点编号为 00n1n-1。有一个整数数组 edgesedges,其中 edges[i]=[ai,bi,wi]edges[i]=[a_i,b_i,w_i],表示节点 aia_ibib_i 之间有一条边权为 wiw_i 的边。其中部分边权为 1-1,其他边权都为正整数。

要求是将所有边权为 1-1 的边都修改为 [1,2109][1,2*10^9] 范围内的正整数,使得从源点 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 。

图.png

详情请查看 LeetCode 官网:2699. 修改图中的边权 - 力扣(LeetCode)

2.分析

  • 假设从 st 的最短路径为 dmin(s,t)d_{min}(s,t),此时某一条边的权值增加 11;如果该边在最短路径中,则最短路径值变为 dmin(s,t)+1d_{min}(s,t)+1;如果该边不在最短路径中,则最短路径值仍然为 dmin(s,t)d_{min}(s,t)
  • 根据题意可知,当某条边的权值大于 DD 时,该边一定不在最短路径范围内,因为这样的路径的权值和必大于 DD,不符合题目要求;因此对于边权的修改范围可以缩小至 [1,D][1,D]
  • 不难理解,当将所有边权为 1-1 的边的权值修改为 11 时,所能找到的最短路径一定是最小的;当修改为 targettarget 时,所能找到的最短路径一定是边权范围内所能找到的最大的

3.方法

3.1 计算最短路径

计算最短路径的方法主要有两种: 迪杰斯特拉(Dijkstra)算法弗洛伊德(Floyd)算法。本方法采用 Dijkstra 方法。

关于该方法的详解,请看:数据结构(Java版) - 图 - 掘金 (juejin.cn)

3.2 遍历修改情况

假设图中边权为 1-1 的边一共有 kk 条,根据修改范围 [1,D][1,D],我们可以得到所有的修改情况:

  • [1,1,..,1][1,1,..,1]
  • [2,1,..,1][2,1,..,1]
  • \dots
  • [D,1,..,1][D,1,..,1]
  • [D,2,..,1][D,2,..,1]
  • \dots
  • [D,D,..,D][D,D,..,D]

共计 D+(k1)(D1)=k(D1)+1D+(k-1)(D-1)=k(D-1)+1 种。从上到下分别编号 idxidx012k(D1)0,1,2,\dots,k(D-1)

根据例子中的图,按上面的顺序的边权情况,分别计算最短路径,可以得到下面的趋势图:

结果.png

根据以上的趋势图可以知道遍历上面的边权情况,其最短路径的值是以递增的方式变化的,因此为了提高计算效率,可以通过使用 二分查找 的方法寻找答案。

3.3 构造邻接矩阵

  • 如果节点 uuvv 之间不存在边,则 matrix[u][v]=matrix[v][u]=1matrix[u][v] = matrix[v][u] = -1
  • 如果节点 uuvv 之间存在边,且权值 w1w \neq -1,则 matrix[u][v]=matrix[v][u]=wmatrix[u][v] = matrix[v][u] = w
  • 如果节点 uuvv 之间存在边,且权值 w=1w = -1,则根据 idxidx 计算权值
    • 如果 idxidx 小于 D1D-1,则权值为 idx+1idx+1,然后 idx=0idx=0
    • 如果 idxidx 大于等于 DD,则权值为 DD,然后 idx=idx(D1)idx = idx - (D-1)

例子解析: 根据例子中的图及其条件,可知其边权情况和对应编号如下所示:

边权情况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

假设 D=5D=5k=4k=4,此时的 idx=10idx=10

  • 由于 idxD1idx \ge D-1,所以 w=D=5w=D=5idx=idx(D1)=6idx = idx - (D-1) = 6
  • 由于 idxD1idx \ge D-1,所以 w=D=5w=D=5idx=idx(D1)=2idx = idx - (D-1) = 2
  • 由于 idx<D1idx < D-1,所以 w=idx+1=3w=idx+1=3idx=0idx=0
  • 由于 idx<D1idx < D-1,所以 w=idx+1=1w=idx+1=1

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;
}