题目链接

物流网络优化

题目描述

给定一个由 个配送中心和 条单向运输线路组成的物流网络。你需要回答两个问题:

  1. 在现有网络中,最多能选出多少个配送中心,使得它们之间任意一对都可以互相调度货物(即相互可达)?
  2. 为了让整个网络成为一个强连通网络(即任意两个配送中心之间都相互可达),最少需要新增多少条运输线路?

解题思路

这是一个典型的图论问题,两个问题都可以通过强连通分量 (Strongly Connected Component, SCC) 来解决。我们可以将物流网络抽象为一个有向图,其中配送中心为顶点,运输线路为有向边。

第一问:最大互通配送中心数量

“任意一对配送中心都可以互相调度货物”的定义,正是图论中强连通分量 (SCC) 的定义。一个 SCC 是有向图中的一个最大的顶点子集,其中任意两个顶点都是相互可达的。

因此,这个问题实际上是在问:图中最大的 SCC 包含多少个顶点?

算法步骤如下:

  1. 使用 Tarjan 算法Kosaraju 算法 找出图中的所有 SCC。
  2. 计算每个 SCC 中包含的顶点数量。
  3. 找出所有 SCC 中顶点数量的最大值,即为第一问的答案。

第二问:最少新增线路

这个问题询问的是,如何用最少的边使一个有向图变得强连通。这同样可以利用 SCC 来解决。

  1. 缩点:我们将每个 SCC “压缩”成一个单一的超级节点。原图中连接不同 SCC 的边,在缩点后就成为连接这些超级节点的边。这样,原图就被转换成了一个有向无环图 (DAG),我们称之为缩点图

  2. 分析 DAG:现在问题转化为:如何用最少的边使一个 DAG 变成强连通的?

    • 一个强连通的 DAG 只能包含一个节点。如果我们的缩点图只有一个节点,说明原图已经强连通,答案为 0。
    • 如果 DAG 中有多个节点,我们需要添加边来消除所有的“起点”和“终点”。在 DAG 中,入度为 0 的节点称为源点 (Source),出度为 0 的节点称为汇点 (Sink)
    • 为了连接整个图,我们需要从汇点连边到源点,将所有分离的路径串成一个大环。
    • 设缩点图中有 个源点和 个汇点。要消除所有源点,至少需要 条指向它们的入边。要消除所有汇点,至少需要 条从它们出发的出边。
    • 我们可以通过连接汇点和源点来同时满足这两个需求。所需的最少边数是

算法整体流程

  1. 构建图:根据输入构建邻接表。
  2. 求 SCC:运行 Tarjan 算法,得到每个顶点所属的 SCC 编号 (scc_id) 和 SCC 的总数 (scc_count)。
  3. 计算第一问
    • 统计每个 SCC 的大小。
    • 找出最大的 SCC 大小。
  4. 计算第二问
    • 如果 scc_count == 1,答案为 0。
    • 否则,根据 SCC 建立缩点图,并计算每个 SCC(超级节点)的入度和出度。
    • 统计入度为 0 和出度为 0 的 SCC 数量,分别为
    • 答案为

代码

#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>

using namespace std;

vector<vector<int>> adj;
vector<int> dfn, low, scc_id;
stack<int> st;
vector<bool> on_stack;
int timer, scc_count;

void tarjan(int u) {
    dfn[u] = low[u] = ++timer;
    st.push(u);
    on_stack[u] = true;

    for (int v : adj[u]) {
        if (dfn[v] == 0) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if (on_stack[v]) {
            low[u] = min(low[u], dfn[v]);
        }
    }

    if (dfn[u] == low[u]) {
        ++scc_count;
        int node;
        do {
            node = st.top();
            st.pop();
            on_stack[node] = false;
            scc_id[node] = scc_count;
        } while (node != u);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(NULL);

    int n, m;
    cin >> n >> m;

    adj.resize(n + 1);
    for (int i = 0; i < m; ++i) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
    }

    dfn.assign(n + 1, 0);
    low.assign(n + 1, 0);
    scc_id.assign(n + 1, 0);
    on_stack.assign(n + 1, false);
    timer = 0;
    scc_count = 0;

    for (int i = 1; i <= n; ++i) {
        if (dfn[i] == 0) {
            tarjan(i);
        }
    }

    // Part 1: Find the size of the largest SCC
    vector<int> scc_size(scc_count + 1, 0);
    int max_scc_size = 0;
    for (int i = 1; i <= n; ++i) {
        scc_size[scc_id[i]]++;
    }
    for (int i = 1; i <= scc_count; ++i) {
        max_scc_size = max(max_scc_size, scc_size[i]);
    }
    cout << max_scc_size << endl;

    // Part 2: Find min edges to make the graph strongly connected
    if (scc_count == 1) {
        cout << 0 << endl;
        return 0;
    }

    vector<int> scc_in_degree(scc_count + 1, 0);
    vector<int> scc_out_degree(scc_count + 1, 0);

    for (int u = 1; u <= n; ++u) {
        for (int v : adj[u]) {
            if (scc_id[u] != scc_id[v]) {
                scc_out_degree[scc_id[u]]++;
                scc_in_degree[scc_id[v]]++;
            }
        }
    }

    int sources = 0;
    int sinks = 0;
    for (int i = 1; i <= scc_count; ++i) {
        if (scc_in_degree[i] == 0) {
            sources++;
        }
        if (scc_out_degree[i] == 0) {
            sinks++;
        }
    }

    cout << max(sources, sinks) << endl;

    return 0;
}
import java.util.*;

class Main {
    static List<List<Integer>> adj;
    static int[] dfn, low, sccId;
    static boolean[] onStack;
    static Stack<Integer> stack;
    static int timer, sccCount;

    static void tarjan(int u) {
        dfn[u] = low[u] = ++timer;
        stack.push(u);
        onStack[u] = true;

        for (int v : adj.get(u)) {
            if (dfn[v] == 0) {
                tarjan(v);
                low[u] = Math.min(low[u], low[v]);
            } else if (onStack[v]) {
                low[u] = Math.min(low[u], dfn[v]);
            }
        }

        if (dfn[u] == low[u]) {
            sccCount++;
            int node;
            do {
                node = stack.pop();
                onStack[node] = false;
                sccId[node] = sccCount;
            } while (node != u);
        }
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int m = sc.nextInt();

        adj = new ArrayList<>();
        for (int i = 0; i <= n; i++) {
            adj.add(new ArrayList<>());
        }
        for (int i = 0; i < m; i++) {
            int u = sc.nextInt();
            int v = sc.nextInt();
            adj.get(u).add(v);
        }

        dfn = new int[n + 1];
        low = new int[n + 1];
        sccId = new int[n + 1];
        onStack = new boolean[n + 1];
        stack = new Stack<>();
        timer = 0;
        sccCount = 0;

        for (int i = 1; i <= n; i++) {
            if (dfn[i] == 0) {
                tarjan(i);
            }
        }

        // Part 1
        int[] sccSize = new int[sccCount + 1];
        int maxSccSize = 0;
        for (int i = 1; i <= n; i++) {
            if (sccId[i] > 0) {
                sccSize[sccId[i]]++;
            }
        }
        for (int i = 1; i <= sccCount; i++) {
            maxSccSize = Math.max(maxSccSize, sccSize[i]);
        }
        // Handle case where n > 0 but m = 0
        if (n > 0 && m == 0) {
             maxSccSize = 1;
        } else if (n > 0 && maxSccSize == 0) {
            maxSccSize = 1;
        }

        System.out.println(maxSccSize);

        // Part 2
        if (sccCount <= 1) {
            System.out.println(0);
            return;
        }

        int[] sccInDegree = new int[sccCount + 1];
        int[] sccOutDegree = new int[sccCount + 1];

        for (int u = 1; u <= n; u++) {
            for (int v : adj.get(u)) {
                if (sccId[u] != sccId[v]) {
                    sccOutDegree[sccId[u]]++;
                    sccInDegree[sccId[v]]++;
                }
            }
        }

        int sources = 0;
        int sinks = 0;
        for (int i = 1; i <= sccCount; i++) {
            if (sccInDegree[i] == 0) {
                sources++;
            }
            if (sccOutDegree[i] == 0) {
                sinks++;
            }
        }
        
        System.out.println(Math.max(sources, sinks));
    }
}
import sys

sys.setrecursionlimit(200005)

adj = {}
dfn, low, scc_id = [], [], []
on_stack = []
stack = []
timer, scc_count = 0, 0

def tarjan(u):
    global timer, scc_count
    
    timer += 1
    dfn[u] = low[u] = timer
    stack.append(u)
    on_stack[u] = True

    for v in adj.get(u, []):
        if dfn[v] == 0:
            tarjan(v)
            low[u] = min(low[u], low[v])
        elif on_stack[v]:
            low[u] = min(low[u], dfn[v])

    if dfn[u] == low[u]:
        scc_count += 1
        while True:
            node = stack.pop()
            on_stack[node] = False
            scc_id[node] = scc_count
            if node == u:
                break

def main():
    global adj, dfn, low, scc_id, on_stack, stack, timer, scc_count
    
    try:
        n, m = map(int, sys.stdin.readline().split())
    except ValueError:
        print(0)
        print(0)
        return
        
    adj = {i: [] for i in range(1, n + 1)}
    for _ in range(m):
        u, v = map(int, sys.stdin.readline().split())
        adj[u].append(v)
        
    dfn = [0] * (n + 1)
    low = [0] * (n + 1)
    scc_id = [0] * (n + 1)
    on_stack = [False] * (n + 1)
    stack = []
    timer, scc_count = 0, 0

    for i in range(1, n + 1):
        if dfn[i] == 0:
            tarjan(i)
    
    # Part 1
    if n == 0:
        print(0)
    else:
        scc_size = [0] * (scc_count + 1)
        for i in range(1, n + 1):
            if scc_id[i] > 0:
                scc_size[scc_id[i]] += 1
        max_scc_size = max(scc_size) if scc_size else 1
        # if n > 0 and m == 0, every node is an SCC of size 1
        if n > 0 and max_scc_size == 0:
            max_scc_size = 1
        print(max_scc_size)

    # Part 2
    if scc_count <= 1:
        print(0)
        return
        
    scc_in_degree = [0] * (scc_count + 1)
    scc_out_degree = [0] * (scc_count + 1)
    
    for u in range(1, n + 1):
        for v in adj.get(u, []):
            if scc_id[u] != scc_id[v]:
                scc_out_degree[scc_id[u]] += 1
                scc_in_degree[scc_id[v]] += 1
                
    sources = sum(1 for i in range(1, scc_count + 1) if scc_in_degree[i] == 0)
    sinks = sum(1 for i in range(1, scc_count + 1) if scc_out_degree[i] == 0)
        
    print(max(sources, sinks))

if __name__ == "__main__":
    main()

算法及复杂度

  • 算法:强连通分量 (Tarjan 算法) + 缩点
  • 时间复杂度:,其中 是配送中心数量, 是运输线路数量。算法的核心是 Tarjan 算法和对图的遍历,均为线性时间复杂度。
  • 空间复杂度:,用于存储图的邻接表以及 Tarjan 算法所需的各种辅助数组。