拓扑算法学习

最近力扣刷题感觉遇到了瓶颈,首先是每日一题来了一个猫和老鼠II,我想了2个小时,硬是不知道这题该以一个什么样的解法去完成比较好。最后只能偷偷点开官方题解,这一次官方题解给的方法不同于以前的动态规划,递归,贪心,什么什么的。这一次官方给的算法是拓扑算法,我一看这名字都高大上,Top算法?但是把题解看完依旧看不懂,于是我就换了一道题目,外星文字典。这道题目我也是想了很久,没有做出来,最后在他人的题解中看到的最多的解法就是拓扑算法。这一次我便去网上搜寻了拓扑算法的资料以及解法。


这篇文章一直放在草稿箱里,没时间写,没想到今天的每日一题就是这道外星文字典,赶快更完!!!


网上已经有很多关于拓扑算法的资料了,这里还是说一下我当时的学习拓扑算法的时候还是受到了一些阻碍的。首先拓扑算法理解起来是比较好理解,简而言之就像是打游戏里面的一个技能树。比如你1技能点完才能点1技能之后的拓展技能2,有的是1,2,3技能点完你才能点后续的技能。这里算法也是一样的,必须要完成前置条件才能做后面的事情。这就和排序类似了,比如排2之前先排1和3,那顺序就是1,3,2(或者3,1,2,这里1,3的顺序没有要求,所以拓扑排序的结果也可能不止一种)。所以当我们遇到这类题目的时候该如何下手呢?(例如外星文字典)

首先我们要做到的就是建图,拓扑排序是有向无环图,有向很好理解,无环是什么呢?就是数字与数字之间的关系不能是相互的,比如你解锁3技能的条件是先点2技能,但是2技能又告诉你需要点完3技能才能点,这就像互相推脱一样,形成了一个死循环,这里是排不了序的,这也对应着题目不合法顺序的那一种情况。这里我用了两个字典,一个来构造度,也就是技能需要点前面多少个技能才能解锁,另一个来构造前置值,也就是点了这个技能,有多少技能的解锁条件少了一个。有了这两个字典,构图就需要看具体需求了,以下是外星文字典的代码。

static Dictionary> val = new Dictionary>();//前置值
    static Dictionary du = new Dictionary();//度
    public static string AlienOrder(string[] words)
    {
        int n = words.Length;
        if (n == 0) return "";
        AlienOrderM1(words[0]);
        for (int i = 1; i < n; i++)
        {
            AlienOrderM1(words[i]);
            if (!AlienOrderM1(words[i - 1], words[i])) return "";
        }
        var q = new Queue();
        foreach (var a in du)
        {
            if (a.Value == 0) q.Enqueue(a.Key);
        }
        var ans = new StringBuilder();
        while (q.Count > 0)
        {
            char a = q.Dequeue();
            ans.Append(a);
            foreach (var item in val[a])
            {
                du[item]--;
                if (du[item] == 0) q.Enqueue(item);
            }
        }
        return ans.Length == du.Count ? ans.ToString() : "";
    }
    /// 
    /// 初始化
    /// 
    /// 
    public static void AlienOrderM1(string a)
    {
        foreach (var item in a)
        {
            if (!val.ContainsKey(item)) { val.Add(item, new HashSet()); du.Add(item, 0); }
        }
    }
    /// 
    /// 构图
    /// 
    /// 
    /// 
    /// 
    public static bool AlienOrderM1(string a, string b)
    {
        for (int i = 0; i < a.Length && i < b.Length; i++)
        {
            if (a[i] != b[i])
            {
                if (val[a[i]].Add(b[i]))
                {
                    du[b[i]]++;
                }
                return true;
            }
        }
        if (a.Length > b.Length) return false;
        return true;
    }

最后建完图之后我们就要做的是看一下这是不是闭环图,是否为闭环图就看最后构建出来的字符串长度和题目中出现过的字符的数量相比较,如果是相同的说明每个字符都取到了说明是无环,如果不同说明有的字符出现了闭环,无法全部取出,这里也不符合题目条件,返回空字符串就可以了。 


这里我贴一下力扣的官方解答:

这道题是拓扑排序问题。外星文字典中的字母和字母顺序可以看成有向图,字典顺序即为所有字母的一种排列,满足每一条有向边的起点字母和终点字母的顺序都和这两个字母在排列中的顺序相同,该排列即为有向图的拓扑排序。

只有当有向图中无环时,才有拓扑排序,且拓扑排序可能不止一种。如果有向图中有环,则环内的字母不存在符合要求的排列,因此没有拓扑排序。

使用拓扑排序求解时,将外星文字典中的每个字母看成一个节点,将字母之间的顺序关系看成有向边。对于外星文字典中的两个相邻单词,同时从左到右遍历,当遇到第一个不相同的字母时,该位置的两个字母之间即存在顺序关系。

以下两种情况不存在合法字母顺序:

字母之间的顺序关系存在由至少 2个字母组成的环,例如 words=[“a",“b",“a"];

相邻两个单词满足后面的单词是前面的单词的前缀,且后面的单词的长度小于前面的单词的长度,例如 words=[“ab",“a"]。

其余情况下都存在合法字母顺序,可以使用拓扑排序得到字典顺序。

拓扑排序可以使用深度优先搜索或广度优先搜索实现,以下分别介绍两种实现方法。

方法一:拓扑排序 + 深度优先搜索
使用深度优先搜索实现拓扑排序的总体思想是:对于一个特定节点,如果该节点的所有相邻节点都已经搜索完成,则该节点也会变成已经搜索完成的节点,在拓扑排序中,该节点位于其所有相邻节点的前面。一个节点的相邻节点指的是从该节点出发通过一条有向边可以到达的节点。

由于拓扑排序的顺序和搜索完成的顺序相反,因此需要使用一个栈存储所有已经搜索完成的节点。深度优先搜索的过程中需要维护每个节点的状态,每个节点的状态可能有三种情况:「未访问」、「访问中」和「已访问」。初始时,所有节点的状态都是「未访问」。

每一轮搜索时,任意选取一个「未访问」的节点 uu,从节点 uu 开始深度优先搜索。将节点 uu 的状态更新为「访问中」,对于每个与节点 uu 相邻的节点 vv,判断节点 vv 的状态,执行如下操作:

如果节点 vv 的状态是「未访问」,则继续搜索节点 vv;

如果节点 vv 的状态是「访问中」,则找到有向图中的环,因此不存在拓扑排序;

如果节点 vv 的状态是「已访问」,则节点 vv 已经搜索完成并入栈,节点 uu 尚未入栈,因此节点 uu 的拓扑顺序一定在节点 vv 的前面,不需要执行任何操作。

当节点 uu 的所有相邻节点的状态都是「已访问」时,将节点 uu 的状态更新为「已访问」,并将节点 uu 入栈。

当所有节点都访问结束之后,如果没有找到有向图中的环,则存在拓扑排序,所有节点从栈顶到栈底的顺序即为拓扑排序。

实现方面,由于每个节点是一个字母,因此可以使用字符数组代替栈,当节点入栈时,在字符数组中按照从后往前的顺序依次填入每个字母。当所有节点都访问结束之后,将字符数组转成字符串,即为字典顺序。

力扣官方解答https://leetcode.cn/problems/Jf1JuT/solution/wai-xing-wen-zi-dian-by-leetcode-solutio-to66/

你可能感兴趣的:(蒜到牛累,算法,学习,排序算法,c#)