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

题目介绍

题目难度: 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;
    }
};
上一篇:JS--继承,多种继承方式


下一篇:fatal: Not a git repository (or any of the parent directories): .git