ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Leetcode 1436旅行终点站 拓扑排序 并查集与队列

2021-10-02 14:03:02  阅读:166  来源: 互联网

标签:1436 node fset parent paths 查集 节点 path Leetcode


题目介绍

题目难度: easy

给你一份旅游线路图,该线路图中的旅行线路用数组 paths 表示,其中 paths[i] = [cityAi, cityBi] 表示该线路将会从 cityAi 直接前往 cityBi 。请你找出这次旅行的终点站,即没有任何可以通往其他城市的线路的城市。

题目数据保证线路图会形成一条不存在循环的线路,因此恰有一个旅行终点站。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/destination-city
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

题目样例

示例 1:

输入:paths = [[“London”,“New York”],[“New York”,“Lima”],[“Lima”,“Sao Paulo”]]
输出:“Sao Paulo”
解释:从 “London” 出发,最后抵达终点站 “Sao Paulo” 。本次旅行的路线是 “London” -> “New York” -> “Lima” -> “Sao Paulo” 。
示例 2:

输入:paths = [[“B”,“C”],[“D”,“B”],[“C”,“A”]]
输出:“A”
解释:所有可能的线路是:
“D” -> “B” -> “C” -> “A”.
“B” -> “C” -> “A”.
“C” -> “A”.
“A”.
显然,旅行终点站是 “A” 。
示例 3:

输入:paths = [[“A”,“Z”]]
输出:“Z”

####题目解析

本题就是典型的拓扑排序题
拓扑排序是将有向无环图 ( V , E ) (V,E) (V,E)排列成一个有序排列

显然对于边 ( i , j ) ∈ E (i,j) \in E (i,j)∈E 则在有序排列中,顶点i必然位于顶点j之前。
因此拓扑排序的方法就是每次找到一个没有入度的点,并将所有的出边删除。重复n次后则得到有序排列。

算法思路 1

当然本题并不要求拓扑排序,而只是要终点,所以像力扣君的题解,找到入边集中不存在鱼出边集的点即可。

算法实现

1.官方题解的思路是记录所有的出边
然后对所有的入边进行查询
set.cout复杂度是(logn+an)
所以实际复杂度接近于(nlogn)

class Solution {
    private:
    unordered_set<string> citiesA;
public:
    string destCity(vector<vector<string>> &paths) {
        
        for (auto &path : paths) {
            citiesA.insert(path[0]);
        }
        for (auto &path : paths) {
            if (!citiesA.count(path[1])) {
                return path[1];
            }
        }
        return "";
    }
};

2.这里我的实现略有不同
相同在于都保存了出点集
不同在于对于入点集使用了map也进行了保存
因此遍历出点集将入点集对应元素进行删除(erase是O(logn)复杂度高 而使用map 增加了value值只要O(1))
然后遍历入点集找到value为true做返回
map的下标访问复杂度为O(logn)
实际复杂度还是O(nlogn)

class Solution {
public:
    string destCity(vector<vector<string>> &paths) {
        unordered_set<string> citiesA;
        //出边集
        unordered_map<string,bool> citiesB;
        //入边集
        for (auto &path : paths) {
            citiesA.insert(path[0]);
            citiesB[path[1]] = 1;
        }
        for(auto &path:citiesA){
            citiesB[path] = false;
        }
        for(auto&path:citiesB){
            if(path.second)
                return path.first;
        }
        return "";
    }
};

并查集

对于拓扑排序问题并查集是一种很好的数据结构。
并查集结构其实是一种多链表结构,就像森林一样,分为不同的集合。或者不同的树。
然后通过一定的方式组合起来。
不同的是,对于并查集我们关心的是属于哪个集合,所以体现的是节点与根的关系。
(根则表明了是哪个集合),因此实际并查集与树是完全相反的两种结构,一种向下。一种向上。
那么因此我们的并查集其实指针域只有一个指针,就是父指针。

当我们进行判定两个节点是否属于一个时,我们就可以通过findroot()函数找到两个节点的根是否相同。

同时由于这种前序关系,我们发现很适合来解决拓扑排序的问题。(因为图不是树,是很难确定根在哪里的,而这里每个节点都必然有一个自己的根)

并查集支持两种操作,findroot()和union();

并查集ADT

struct listnode{
    type data;
    listnode *pre;
}
class unionfind{
    private:
    Listnode elem[max];
    public:
    Listnode find();
    void union(listnode p,listnode q);
}

  1. find()
     find(Listnode p){
         while(p!=p->pre)
             p = p->pre;
         return p;
     }
    
  2. union
    union(listnode p,listnode q){
        p = find(p);
        q = find(q);
        p->pre = q;
        //q->pre = p也可
        //if p = q, p ->pre = p = q
    }
    
  3. del
    由于本题是想要将所有前序节点删除找到终点因此构造了一个新的del函数,del函数会将node的所有前序节点删除。
    del(listnode node){
        if(node!=node->pre){
            listnode parent = node->pre;
            do{
                const listnode i = parent;
                //保存当前节点地址
                parent = parent->pre;
                //更新parent;
                i->pre = i;
                //从集合中删除i
                del i;
                //删除节点i
            }while(parent!=parent->pre);
            del parent;
            //parent指向原集合的根因此也需要删除
        }// 如果是根节点则没有前序则不进行操作
        node->pre = node;
        //前序删除变成集合根节点
    }
    
    
    

算法实现

为了实现节点的删除此处使用bool记录是否存在
查询复杂度是logn的
实际删除次数为n次
时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

#define x first
#define y second
class Solution {
    private:
    unordered_map<string ,pair<string ,bool>> fset;
    void init(vector<vector<string>>& paths) {
        int &&size = paths.size();
        for(int i = 0;i<size;i++){
            string &in = paths[i][1];//入点
            string &out = paths[i][0];//出点
            pair<string,bool> &&node = make_pair(paths[i][0],true);
            fset[in] = node;
            //入点的前置是出点
            fset[out] = fset[out].x.length()?fset[out]:node;
            //出点如果没有保存过则进行保存
        }
    }
    void del(string s){
        string parent = fset[s].x;
        if(s!=parent){//s!=s.next
            do{
                const string i = parent;
                parent = fset[parent].x;
                fset[i].y = false;
                //删除parent的后置节点
                fset[i].x = i;
                //将节点从集合中删除

            }while(parent!=fset[parent].x);
            fset[parent].y = false;
            //删除s所属集合的根
        }
        fset[s].x = s;
    }
public:
    string destCity(vector<vector<string>>& paths) {
       // fset.clear();
        //slist.clear();
        
        init(paths);
        for(auto i = fset.begin();i!=fset.end();i++){
            del(i->first);
        }
        for(auto i:fset){
            auto &node = i.y;
            if(node.y)
                return node.x;
        }
        return "";
    }
};
};

队列实现拓扑排序

本题队列实现比较困难的一点在于入度和边表的存储。
由于map对于离散化数据存储的优点
因此使用两个map分别存储入度和边表并且做初始化即可
每次查询入度表内入度为零且存在的节点入队,并做删点和删边操作
删除所有节点后s记录的一定是终点

算法实现


#define indeg first
#define exist second
class Solution {
    private:
    queue<string> q;
    unordered_map<string ,pair<int,bool>> inlist;
    unordered_map<string,vector<string>> edge;
    void init(vector<vector<string>>& paths) {
        pair<int,bool> &&empty = make_pair(0,true);
        for(auto node:paths){
            const string &in  = node[1];
            const string &out = node[0];
            edge[out].push_back(in);
            //out顶点push出边
            inlist[out] = inlist[out].indeg?inlist[out]:empty;
            //初始化out点的入度
            inlist[in] = inlist[in].indeg?inlist[in]:empty;
            
            //初始化in点的入度
            inlist[in].indeg++;
            //入度更新
        }
    }
public:
    string destCity(vector<vector<string>>& paths) {
       // fset.clear();
        //slist.clear();
        string s;
        init(paths);
        int size = inlist.size();
        while(size){
            for(auto i:inlist){
                const auto node = i.second;
                if(!node.indeg&&node.exist){
                    //如果入度为零且存在则入队进行删边和删点
                    q.push(i.first);
                }
            }
            while(!q.empty()){
                string node = q.front();
                s = node;
                //总是更新为当前无前置的节点
                q.pop();
                inlist[node].exist = 0;
                //删除节点
                size--;
                //节点少一
                if(edge[node].size())
                for(auto i:edge[node]){
                    const string next = i;
                    inlist[next].indeg--; 
                    //删掉node的所有出边
                }
            }
        }
        return s;
    }
};

标签:1436,node,fset,parent,paths,查集,节点,path,Leetcode
来源: https://blog.csdn.net/qq_41439406/article/details/120585426

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有