一些常用的算法

发布于 2020-05-17  527 次阅读


这些算法和技巧都是我从其他地方看到的,在这里整理了一下。

1. 拓扑排序

一个有向图G的拓扑排序:
存在对于图G的节点编号的一种排列,满足任意一条有向边 [latex](u, v)[/latex],[latex]u[/latex] 在排列中都出现在 [latex] v [/latex] 的前面。

根据以上定义,可以得出两个结论:

  • 如果有向图G存在环,那么图G不存在拓扑排序
  • 如果图 G 是有向无环图,那么它的拓扑排序可能不止一种。举一个最极端的例子,如果图 G 只有节点没有边,那么任意一种编号的排列都可以作为拓扑排序。

求有向图的拓扑排序有两种,我是从LeetCode上学来的。出处点这里

1. 深度搜索优先:

设计:

  • 相邻节点:从一个节点 [latex]u[/latex] 出发,通过一条有向边可以到达的所有节点。
  • 对于一个节点 [latex]u[/latex],如果它的所有相邻节点都已经搜索完成,那么在搜索回溯到 [latex]u[/latex] 的时候,[latex]u[/latex] 本身也变成一个搜索完成的节点。
  • 将已搜索完成的节点存入一个栈中。
  • 假设当前搜索到了节点 [latex]u[/latex],若其所有相邻节点都已经搜索完成,那么这些节点都已被压入栈中了。此时,可以把节点 [latex]u[/latex] 压入栈中。
  • 从栈顶往栈低看,节点 [latex]u[/latex] 处于栈顶的位置,故 [latex]u[/latex] 出现在所有与 [latex]u[/latex] 相邻的节点的前面。因此节点 [latex]u[/latex] 是满足拓扑排序的。

实现:

对于图中的节点,在搜索过程中三种状态:

  1. 未搜索:还未接触(reach)这个节点
  2. 搜素中:已经接触到,但还没有回溯到该节点——当前节点还未入栈,正在搜索其相邻的节点
  3. 搜索完成:接触并回溯过这个节点,即这点节点已入栈,并且该节点的所有节点都在栈中比其更底部的位置(满足拓扑排序的要求)。

通过上述三种状态,就可给出使用深度优先搜索算法得出拓扑排序的流程。

1. 在每一轮搜索开始时,任意选取一个未曾搜索的节点开始进行深度优先搜索。

2. 将当前搜索的节点 [latex]u[/latex] 标记为搜索中,遍历该节点的每一个相邻节点 [latex]v[/latex]

  1. 若 [latex]v[/latex] 为未搜索:搜索节点[latex]u[/latex],待搜索完成回溯到[latex]u[/latex]
  2. 若 [latex]v[/latex] 为搜索中:那么图中存在环,故不存在拓扑排序
  3. 若 [latex]v[/latex] 为搜索完成:说明 [latex]v[/latex] 已经在栈中了,而 [latex]u[/latex] 还不在栈中。因此,[latex]u[/latex] 一定比 [latex]v[/latex] 晚入栈,不会影响到 [latex](u,v)[/latex] 之前的拓扑关系,所以不用做任何操作。

3. 当 [latex]u[/latex] 所有相邻的节点都为已完成时,将 [latex]u[/latex] 压入栈中,并标记其为已完成状态。

整个深度优先搜索完成后,如果图中没有环,那么从栈顶到栈底的节点排序即为一种拓扑排序。

class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        edges.resize(numCourses);
        status.resize(numCourses);
        vector<int> ans;

        for(auto temp : prerequisites)
            edges[temp[1]].push_back(temp[0]);

        for(int u=0; u<numCourses && !existLoop; u++)
        {
            if(status[u] == 0)
                dfs(u);
        }

        if(existLoop)
            return(ans);

        ans.resize(numCourses);
        int index = 0;

        while(!record.empty())
        {
            ans[index] = record.top();
            index++;
            record.pop();
        }

        return(ans);
    }

    void dfs(int u) {
        status[u] = 1;

        for(int v : edges[u])
        {
            if(status[v] == 0)
            {
                dfs(v);
                if(existLoop)
                    return;
            }               
            else if(status[v] == 1)
            {
                existLoop = true;
                return;
            }
            else
                continue;
        }

        status[u] = 2;
        record.push(u);
    }

private:
    vector<vector<int>> edges;
    vector<int> status;
    bool existLoop;
    stack<int> record;
};

时间复制度:[latex] O(No. nodes + No. edges) [/latex]
空间复杂度:[latex] O(No. nodes + No. edges) [/latex]

2. 广度搜索优先

设计:

上面的方法是逆向思维——最先放入的节点是拓扑排序中最后的节点。我们可以使用正向思维,按照拓扑排序的顺序放入节点。

对于拓扑排序后的第一个节点,它一定不会有入边。当我们把这个节点放入到栈中后,就可以移除它的所有出边。那么与它相邻的节点就少了一个入边,要是有相邻的节点少了一个入边后就没有入边了,这种情况下就可以把这个相邻的节点放入栈中,重复之前的操作。

实现:

1. 将所有入度为0的节点放入到一个队列(queue)中

2. 从队列的首部取出一个节点

  • 将该节点压入栈中
  • 移除该节点的所有出边,即与其相邻的节点的入度减1;若出现入度为0的相邻节点,将其放入到队列中去
  • 重复第2步,知道队列为空

3. 如果栈中包含了有向图的所有节点,那么从栈底到栈顶的顺序就是有向图的拓扑排序的顺序。否则,该有向图不存在拓扑排序。

class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        inputDegree.resize(numCourses);

        for(auto temp : prerequisites)
        {
            edges[temp[1]].push_back(temp[0]);
            inputDegree[temp[0]]++;
        }

        queue<int> q;

        for(int i=0; i<numCourses; i++)
        {
            if(inputDegree[i] == 0)
                q.push(i);
        }

        vector<int> ans;

        while(!q.empty())
        {
            int u = q.front();
            q.pop();
            ans.push_back(u);

            for(int v : edges[u])
            {
                inputDegree[v]--;
                if(inputDegree[v] == 0)
                    q.push(v);
            }
        }

        if(ans.size() == numCourses)
            return(ans);
        
        vector<int> e;
        return(e);
    }

private:
    unordered_map<int,vector<int>> edges;
    vector<int> inputDegree;
};

2. 前缀和 + 状态压缩

今天(5/20/2020)我做 LeetCode 每日一题时,除了暴力法之外没有想到其他方法。题目请点这里。我曾以为自己在第一层,官方题解在第三层。直到我看了官方题解后,发现官方题解在第五层,而我还是在第一层。

在求子串或连续子数组中某些元素的出现的次数时,可以使用前缀和。这是一种以空间换时间的方法——将之前储存的结果保存下来,避免重复计算。

这一题的结果有5个字母的状态,我们可以定义一个 struct 去储存,但这里官方题解给出了一种更妙的解法(第五层的解法)。由于每个字母只有两种情况——出现了偶数次(包括0次)和奇数次,那我们就可以用整数的二进制表示来当作这5个字母的状态位。在32位程序中,一个 unsigned 整数是32位,可以表示32个元素的状态。在此题中,这五个字母的状态分别用 0~4 来表示。那么,状态的取值范围就是 [0, 31]。

更强的是,官方题解中并不是储存每个下标的状态,而是储存最早出现此状体的下标,大大节省了空间和时间。

int findTheLongestSubstring(string s) {
	int ans = 0;
	int status = 0;

	vector<int> previous(1 << 5, -1);
	previous[0] = 0;

	for (int i = 0; i < s.size(); i++)
	{
		if (s[i] == 'a')
			status ^= 1 << 0;
		else if (s[i] == 'e')
			status ^= 1 << 1;
		else if (s[i] == 'i')
			status ^= 1 << 2;
		else if (s[i] == 'o')
			status ^= 1 << 3;
		else if (s[i] == 'u')
			status ^= 1 << 4;

		if (~previous[status])
			ans = max(ans, i + 1 - previous[status]);
		else
			previous[status] = i + 1;
	}

	return(ans);
}