图的问题算是比较复杂的类型的题目了,我觉得需要把握住的有几点。
首先是要学会构建图,如何从题目给你的信息中,抽象出图的关系。
其次是要会表示图,邻接表,邻接矩阵至少要会(特定题目条件下要使用特殊的数据机构)
然后是要会遍历图,DFS, BFS是最基本的算法,很多图的题目都是从遍历而来的。
最后,要回一些特定问题的特定解决方法。图的问题,使用遍历方法,基本都可以得到简单问题的解,但是当数据规模上去以后,这种遍历问题很容易变成NP问题,此时就要考虑使用特定的算法或者思想,达到优化的目的。
给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。
这个问题反应的是两个最基本的图算法。因为是需要深拷贝,所以需要构造原图节点和新图节点的对应关系,所以可以使用map来构造映射。然后使用遍历方法从原图中获得信息补充到新图上。直接上代码
class Node {
public int val;
public List<Node> neighbors;
};
// DFS算法
unordered_map<Node*,Node*> mpp;
Node* cloneGraph(Node* node)
{
if(node == NULL) return NULL;
if(mpp.count(node)) return mpp[node];
Node temp = new Node(node->val); //获得当前节点的拷贝
mpp[node] = temp;
for(auto &l:node->neighbors)
{
// 插入临界节点的拷贝
temp->neighbors.push_back(cloneGraph(l));
}
return temp;
}
// BFS算法
Node* cloneGraph(Node* node)
{
if (!node) return NULL;
unordered_map<Node*, Node*> mp; //一一对应的节点
queue<Node*> que({ node });
const auto new_node = new Node(node->val);
mp[node] = new_node;
while (!que.empty()) {
auto temp = que.front();
que.pop();
for (const auto&e : temp->neighbors) { //从原图中找邻居
if (!mp.count(e)) {
que.push(e);
mp[e] = new Node(e->val);
}
mp[temp]->neighbors.push_back(mp[e]); //拷贝的节点插入拷贝的节点的邻居
}
}
return mp[node];
}
在一个有向图中,节点分别标记为 0, 1, …, n-1。这个图中的每条边不是红色就是蓝色,且存在自环或平边。
red_edges 中的每一个 [i, j] 对表示从节点 i 到节点 j 的红色有向边。
blue_edges 中的每一个 [i, j] 对表示从节点 i 到节点 j 的蓝色有向边。
返回长度为 n 的数组 answer,其中 answer[X] 是从节点 0 到节点 X 的最短路径的长度,且路径上红色边和蓝色边交替出现。如果不存在这样的路径,那么 answer[x] = -1。
这也是一个图的遍历问题。不过这一题要求是红蓝边交替出现,并且两个节点之间可能有多条边(红和蓝)。因此在构图的过程中,需要将颜色信息记录进去,相当于是构建两个图。这里使用三维vector来表示图的邻接表。
// DFS解法
vector<int> shortestAlternatingPaths1(int n, vector<vector<int>> &red_edges, vector<vector<int>> &blue_edges)
{
vector<vector<int>> rg(n);
vector<vector<int>> bg(n);
// 分别构造红蓝图
for (auto &e : red_edges)
rg[e[0]].push_back(e[1]);
for (auto &e : blue_edges)
bg[e[0]].push_back(e[1]);
// 将两个图合并为一个大图
vector<vector<vector<int>>> g{rg, bg};
vector<vector<int>> res(n, {INF, INF});
res[0] = {0, 0};
// 开始两次DFS
dfs(g, 0, 0, res);
dfs(g, 1, 0, res);
vector<int> out(n);
for (int i = 0; i < n; ++i)
{
out[i] = min(res[i][0], res[i][1]);
if (out[i] == INF)
out[i] = -1;
}
return out;
}
void dfs(vector<vector<vector<int>>> &g, int col, int i, vector<vector<int>> &res)
{
for (auto j : g[col][i])
{
// i->j异色 距离更小
if (res[i][col] + 1 < res[j][!col])
{
res[j][!col] = res[i][col] + 1;
dfs(g, !col, j, res);
}
}
}
// BFS解法
vector<int> shortestAlternatingPaths2(int n, vector<vector<int>> &red_edges, vector<vector<int>> &blue_edges)
{
unordered_map<int, vector<int>> graphBlue;
unordered_map<int, vector<int>> graphRed;
int visit[100][100][2];
int step = 0;
vector<int> res(n, INT_MAX);
queue<pair<int, int>> qu;
for (auto re : red_edges)
{
graphRed[re[0]].push_back(re[1]);
}
for (auto be : blue_edges)
{
graphBlue[be[0]].push_back(be[1]);
}
/*first blue,second red*/
step = 0;
memset(visit, 0, sizeof(visit));
qu.push(make_pair(0, 1)); //从0开始走蓝色边
qu.push(make_pair(0, 0)); //从0开始走红色边
while (!qu.empty())
{
int sz = qu.size(); // 记录本层次需要处理的节点个数 保证不会处理到下一节点
step++;
for (int i = 0; i < sz; ++i)
{
int curr = qu.front().first;
int color = qu.front().second;
qu.pop();
if (color) //根据边选择 当前走的是蓝色边
{
for (auto next : graphBlue[curr])
{
// 遍历寻找红色边加入队列
if (!visit[curr][next][0])
{
res[next] = min(res[next], step);
visit[curr][next][0] = true;
qu.push(make_pair(next, 0));
}
}
}
else
{
for (auto next : graphRed[curr])
{
if (!visit[curr][next][1])
{
res[next] = min(res[next], step);
visit[curr][next][1] = true;
qu.push(make_pair(next, 1));
}
}
}
}
}
res[0] = 0;
for (int i = 0; i < n; ++i)
{
if (res[i] == INT_MAX)
{
res[i] = -1;
}
}
return res;
}
有向图的先行排序称之为拓扑排序。图本身也是相互具有依赖关系组成的集合,拓扑排序就是把这一复杂集合线性化的过程。实现拓扑排序有两种方式,一种是基于DFS和栈结构的模式,另一种是基于BFS的统计出/入度数的模式。个人比较偏向于使用后者,感觉更靠谱一些。
对于有明显先后关系的题目,可以往拓扑排序的方面进行思考。
你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]
给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?
这是一道很经典的拓扑排序题目,如果所有课程可以构成一个拓扑序列,那么证明是满足条件的。
bool canFinish(int numCourses, vector<vector<int>>& prerequisites)
{
vector<int> indegree(numCourses);
vector<vector<int>> graph(numCourses);//构建临接表(用vector储存临接点,方便访问)
vector<int> v;
for (int i = 0; i < numCourses; i++)
{
indegree[i] = 0;
graph.push_back(v);
}
for (int i = 0; i < prerequisites.size(); i++)
{
indegree[prerequisites[i][0]]++;
graph[prerequisites[i][1]].push_back(prerequisites[i][0]);//存的是出边
}
//将入度为0的顶点入队
queue<int> myqueue;
for (int i = 0; i < numCourses; i++)
{
if (indegree[i] == 0)
myqueue.push(i);
}
int cnt = 0;
while (!myqueue.empty())
{
int temp = myqueue.front();
myqueue.pop();
cnt++;
//更新:
for (int i = 0; i < graph[temp].size(); i++)
{
indegree[graph[temp][i]]--;
if (indegree[graph[temp][i]] == 0)//放在这里做!只判断邻接点。
myqueue.push(graph[temp][i]);
}
}
return cnt == numCourses;
}
在有向图中, 我们从某个节点和每个转向处开始, 沿着图的有向边走。 如果我们到达的节点是终点 (即它没有连出的有向边), 我们停止。
现在, 如果我们最后能走到终点,那么我们的起始节点是最终安全的。 更具体地说, 存在一个自然数 K, 无论选择从哪里开始行走, 我们走了不到 K 步后必能停止在一个终点。
哪些节点最终是安全的? 结果返回一个有序的数组。
上一个题是计算入度数,然后求拓扑排序。这个题是计算出度数来得到拓扑排序。一个节点如果没有出度,那么就是安全的,同样所有跟他相连的点也是安全的。凭借这个分析,我们从没有出度的节点入手,得到安全的数组。
vector<int> eventualSafeNodes(vector<vector<int>>& graph)
{
int n = graph.size();
vector<int> outDegree(n,0);
vector<vector<int>> revGraph(n, vector<int>{});
vector<int> ans;
for (int i =0; i < n; i++){
outDegree[i] = graph[i].size();
for (auto &end : graph[i]){
revGraph[end].push_back(i);
}
}
queue<int> q;
for (int i = 0; i < n; i++)
{
if (outDegree[i] == 0)
q.push(i);
}
while (!q.empty())
{
int f = q.front();
ans.push_back(f);
q.pop();
for (auto start : revGraph[f])
{
outDegree[start]--;
if (outDegree[start] == 0)
q.push(start);
}
}
sort(ans.begin(), ans.end());
return ans;
}
有 N 个网络节点,标记为 1 到 N。
给定一个列表 times,表示信号经过有向边的传递时间。 times[i] = (u, v, w),其中 u 是源节点,v 是目标节点, w 是一个信号从源节点传递到目标节点的时间。
现在,我们从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1
本题求解的是从一个节点到其他所有节点的最短路径的最大值,本质上就是最短路径问题
// dijkstar算法 一般形式
int networkDelayTime(vector<vector<int>> ×, int n, int K)
{
vector<vector<int>> g(n + 1, vector<int>(n + 1, INT_MAX));
for (auto &v : times)
{
g[v[0]][v[1]] = v[2];
}
vector<bool> book(n + 1, INT_MAX);
vector<int> dist(n + 1, INT_MAX);
dist[K] = 0;
for (int i = 0; i < n - 1; i++)
{
int t = -1;
for (int j = 1; j <= n; j++) // 找到当前没有遍历过的 最小的距离
{
if (!book[i] && (t == -1 || dist[j] > dist[t]))
{
t = j;
}
}
book[t] = true;
for (int j = 0; i < g[t].size(); j++)
{
if (dist[j] > dist[t] + g[t][j]) //路径压缩
{
dist[j] = dist[t] + g[t][j];
}
}
}
int ans = *max_element(dist.begin() + 1, dist.end());
return ans == INT_MAX ? -1 : ans;
}
// dijkstar堆优化
typedef pair<int, int> PII; //first距离 second节点
int networkDelayTime1(vector<vector<int>> ×, int N, int K)
{
vector<bool> st(N + 1, false); // 是否已得到最短距离
vector<int> dist(N + 1, INT_MAX); // 距离起始点的最短距离
unordered_map<int, vector<PII>> graph; // 邻接表;u->v,权重w
priority_queue<PII, vector<PII>, greater<PII>> heap; // 小顶堆;维护到起始点的最短距离和点
for (auto &t : times)
{ // 初始化邻接表
graph[t[0]].push_back({t[2], t[1]});
}
heap.push({0, K});
dist[K] = 0;
while (!heap.empty())
{
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) continue; //当前节点被遍历过
st[ver] = true;
for (auto &p : graph[ver])
{
if (dist[p.second] > distance + p.first)
{ // 用t去更新其他点到起始点的最短距离
dist[p.second] = distance + p.first;
heap.push({dist[p.second], p.second});
}
}
}
int ans = *max_element(dist.begin() + 1, dist.end());
return ans == INT_MAX ? -1 : ans;
}
// Floyd算法
int networkDelayTime(vector<vector<int>> ×, int N, int K)
{
const int INF = 0x3f3f3f3f;
vector<vector<int>> d(N + 1, vector<int>(N + 1, INF));
for (int i = 1; i <= N; i++)
d[i][i] = 0;
for (auto &t : times)
{
d[t[0]][t[1]] = min(d[t[0]][t[1]], t[2]);
}
for (int k = 1; k <= N; k++)
{
for (int i = 1; i <= N; i++)
{
for (int j = 1; j <= N; j++)
{
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
}
}
int ans = 0;
for (int i = 1; i <= N; i++)
{
ans = max(ans, d[K][i]);
}
return ans > INF / 2 ? -1 : ans;
}
// Bellman_Ford算法
int networkDelayTime3(vector<vector<int>> ×, int N, int K)
{
vector<int> dist(N + 1, INT_MAX);
vector<int> backup(N + 1);
dist[K] = 0;
for (int i = 0; i <= N; i++)
{
backup.assign(dist.begin(), dist.end()); // 重用一个vector 将dist内容复制到back
for (auto &t : times)
{ // 枚举所有边
dist[t[1]] = min(dist[t[1]], backup[t[0]] + t[2]); // 更新最短路
}
}
int ans = *max_element(dist.begin() + 1, dist.end());
return ans > INT_MAX / 2 ? -1 : ans; // INF/2 是因为可能有负权边;这个题没有负权边,可以用INF
}
//SPFA算法
int networkDelayTime(vector<vector<int>> ×, int N, int K)
{
const int INF = 0x3f3f3f3f;
vector<int> dist(N + 1, INF); // 保存到起点的距离
vector<bool> st(N + 1, false); // 是否最短
typedef pair<int, int> PII;
unordered_map<int, vector<PII>> edges; // 邻接表
queue<int> q;
q.push(K);
dist[K] = 0;
st[K] = true; // 是否在队列中
for (auto &t : times)
{
edges[t[0]].push_back({t[1], t[2]});
}
while (!q.empty())
{ // 当没有点可以更新的时候,说明得到最短路
auto t = q.front();
q.pop();
st[t] = false;
for (auto &e : edges[t])
{ // 更新队列中的点出发的 所有边
int v = e.first, w = e.second;
if (dist[v] > dist[t] + w)
{
dist[v] = dist[t] + w;
if (!st[v])
{
q.push(v);
st[v] = true;
}
}
}
}
int ans = *max_element(dist.begin() + 1, dist.end());
return ans == INF ? -1 : ans;
}
并查集本身也是描述相关关系的数据结构,也很适合处理图的相关问题。
在本问题中, 树指的是一个连通且无环的无向图。
输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, …, N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。
返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v。
输入: [[1,2], [1,3], [2,3]] 输出: [2,3] 解释: 给定的无向图为: 1 / \ 2 - 3
如果新插入的一条边的两个点属于同一棵树(同一个集合),那么这条边就可以删去。
class Solution {
public:
int f[1001];
int find(int x)
{
if(x == f[x]) return x;
f[x] = find(f[x]);
return f[x];
}
int findfather(int x)
{
if(x==f[x]) return x;
while(x!=f[x])
{
x=f[x];
}
return x;
}
void merge(int x,int y)
{
int X = find(x);
int Y = find(y);
if(X<Y)
{
f[Y] = X;
}
else
{
f[X] = Y;
}
}
vector<int> findRedundantConnection(vector<vector<int>>& edges)
{
vector<int> result;
for(int i=0;i<1001;i++)
{
f[i] = i;
}
int len = edges.size();
for(int i=0;i<len;i++)
{
int l1 = edges[i][0];
int l2 = edges[i][1];
if(findfather(l2)==findfather(l1))
{
result.push_back(l1);
result.push_back(l2);
break;
}
else
{
merge(l1,l2);
}
}
return result;
}
};