题目描述
(中等)n 座城市,从 0 到 n-1 编号,其间共有 n-1 条路线。因此,要想在两座不同城市之间旅行只有唯一一条路线可供选择(路线网形成一颗树)。去年,交通运输部决定重新规划路线,以改变交通拥堵的状况。
路线用 connections 表示,其中 connections[i] = [a, b] 表示从城市 a 到 b 的一条有向路线。
今年,城市 0 将会举办一场大型比赛,很多游客都想前往城市 0 。
请你帮助重新规划路线方向,使每个城市都可以访问城市 0 。返回需要变更方向的最小路线数。
题目数据保证每个城市在重新规划路线方向后都能到达城市 0 。
示例:
输入:n = 6, connections = [[0,1],[1,3],[2,3],[4,0],[4,5]]
输出:3
解释:更改以红色显示的路线的方向,使每个城市都可以到达城市 0 。
解题思路
最重要的是理解题意,尤其这种经过长篇幅文字包装的题,更要静下心来掌握题意!
容易分析得到以下结论:
- n个结点,n - 1条边且所有结点均能到达 ‘0’ 结点,即每个结点的出度(图论中的知识点)为1, ‘0’ 结点除外;
- 进一步分析,若将题目中的有向图视为无向图,则此图一定是以 ‘0’ 结点为根节点的树结构;
- 重新规划路径方向,即结点间的连结关系不改,只改连接方向;
- 最后,以 ‘0’ 结点为起点,依循连接关系遍历所有结点,对错误方向计数即可。
代码实现
先看深度优先(DFS):
class Solution {
public:
int minReorder(int n, vector<vector<int>>& connections) {
//统计记录入度的邻接链表
vector<vector<int>> in(n);
//统计记录出度的邻接链表
vector<vector<int>> out(n);
//邻接链表初始化
for(auto conn : connections){
in[conn[1]].push_back(conn[0]);
out[conn[0]].push_back(conn[1]);
}
//访问数组,避免边重复访问:对于一条边上的两个顶点,A是入边,B是出边。导致重复访问使结果大于正确值。
vector<bool> visited(n, false);
//初始化规划次数
int ans = 0;
//以‘0’结点为起点
dfs(0, ans, in, out, visited);
return ans;
}
//深度优先搜索
void dfs(int node, int& ans, vector<vector<int>>& in, vector<vector<int>>& out, vector<bool>& visited){
//访问过的结点不进行下一步递归
if(visited[node]) return;
visited[node] = true;
//遍历当前结点入边的弧头结点
for(int i : in[node]){
dfs(i, ans, in, out, visited);
}
//遍历当前结点入边的弧尾结点,此时必为错误方向,计数+ 1!!!
for(int i : out[node]){
if(!visited[i]) ans++;
dfs(i, ans, in, out, visited);
}
}
};
运行结果:
深度优先的另一种实现思路:
class Solution {
public:
int minReorder(int n, vector<vector<int>>& connections) {
//有向图邻接矩阵
vector<vector<int>> withDire(n);
//无向图邻接矩阵
vector<vector<int>> withOutDire(n);
for(auto conn : connections){
withDire[conn[0]].push_back(conn[1]);
withOutDire[conn[0]].push_back(conn[1]);
withOutDire[conn[1]].push_back(conn[0]);
}
vector<bool> visited(n, false);
int ans = 0;
dfs(withDire, withOutDire, visited, 0, ans);
return ans;
}
void dfs(vector<vector<int>>& withDire, vector<vector<int>> &withOutDire, vector<bool>& visited, int node, int& ans){
if(visited[node]) return;
visited[node] = true;
for(int i : withOutDire[node]){
if(visited[i]) continue;
//无需分出、入度考虑,代码逻辑统一
if(find(withDire[node].begin(), withDire[node].end(), i) != withDire[node].end()) ans++;
dfs(withDire, withOutDire, visited, i, ans);
}
}
};
再看广度优先(BFS):
通过建立边的索引目录,并且相较于上述DFS,以边为中心,极大减少了时间、内存开销。
class Solution {
public:
int minReorder(int n, vector<vector<int>>& connections) {
int ans = 0;
//创建顶点--边的关系的索引
vector<vector<int>> edgeIndex(n);
//初始化索引
for(int i = 0; i < connections.size(); i++){
edgeIndex[connections[i][0]].push_back(i);
edgeIndex[connections[i][1]].push_back(i);
}
//以边为核心,创建访问数组
vector<bool> visited(n - 1);
//广度优先
queue<int> que;
que.push(0);
while(!que.empty()){
int node = que.front();
que.pop();
for(int i : edgeIndex[node]){
//重复访问边,直接跳过当前循环
if(visited[i]) continue;
visited[i] = true;
//当前结点为边的弧头,必为错误方向!
if(node == connections[i][0]){
ans++;
que.push(connections[i][1]);
}else{
que.push(connections[i][0]);
}
}
}
return ans;
}
};
广度优先的代码里,创建索引是亮点,也是性能提升的关键。避免了像深度优先开辟额外空间创建各种类型的邻接矩阵,且避免在递归中使用std::find()
这种时间复杂度为O(n)的操作。