Leetcode刷题总结--基本图算法

Leetcode刷题总结

基本图算法

图的问题算是比较复杂的类型的题目了,我觉得需要把握住的有几点。

首先是要学会构建图,如何从题目给你的信息中,抽象出图的关系。

其次是要会表示图,邻接表,邻接矩阵至少要会(特定题目条件下要使用特殊的数据机构)

然后是要会遍历图,DFS, BFS是最基本的算法,很多图的题目都是从遍历而来的。

最后,要回一些特定问题的特定解决方法。图的问题,使用遍历方法,基本都可以得到简单问题的解,但是当数据规模上去以后,这种遍历问题很容易变成NP问题,此时就要考虑使用特定的算法或者思想,达到优化的目的。

1. 遍历问题(DFS,BFS)

Leetcode133

给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。

这个问题反应的是两个最基本的图算法。因为是需要深拷贝,所以需要构造原图节点和新图节点的对应关系,所以可以使用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];
}
Leetcode1129

在一个有向图中,节点分别标记为 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;
}

2. 拓扑排序

有向图的先行排序称之为拓扑排序。图本身也是相互具有依赖关系组成的集合,拓扑排序就是把这一复杂集合线性化的过程。实现拓扑排序有两种方式,一种是基于DFS和栈结构的模式,另一种是基于BFS的统计出/入度数的模式。个人比较偏向于使用后者,感觉更靠谱一些。

对于有明显先后关系的题目,可以往拓扑排序的方面进行思考。

Leetcode207

你这个学期必须选修 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;

}
Leetcode802

在有向图中, 我们从某个节点和每个转向处开始, 沿着图的有向边走。 如果我们到达的节点是终点 (即它没有连出的有向边), 我们停止。

现在, 如果我们最后能走到终点,那么我们的起始节点是最终安全的。 更具体地说, 存在一个自然数 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;
}

3. 最短路径算法

Leetcode743

有 N 个网络节点,标记为 1 到 N。

给定一个列表 times,表示信号经过有向边的传递时间。 times[i] = (u, v, w),其中 u 是源节点,v 是目标节点, w 是一个信号从源节点传递到目标节点的时间。

现在,我们从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1

本题求解的是从一个节点到其他所有节点的最短路径的最大值,本质上就是最短路径问题

// dijkstar算法 一般形式
int networkDelayTime(vector<vector<int>> &times, 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>> &times, 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>> &times, 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>> &times, 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>> &times, 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;
}

4. 并查集

并查集本身也是描述相关关系的数据结构,也很适合处理图的相关问题。

Leetcode684

在本问题中, 树指的是一个连通且无环的无向图。

输入一个图,该图由一个有着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;
    }
};
  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值