Skip to content

数据结构与算法

其中许多有用知识在: 算法

拓扑排序

在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足下面两个条件:

  1. 每个顶点出现且只出现一次。
  2. 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。

它是一个 DAG 图,那么如何写出它的拓扑排序呢?这里说一种比较常用的方法:

  1. 从 DAG 图中选择一个 没有前驱(即入度为0)的顶点并输出。
  2. 从图中删除该顶点和所有以它为起点的有向边。
  3. 重复 1 和 2 直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。
#include<iostream>
#include <list>
#include <queue>
using namespace std;

/************************类声明************************/
class Graph
{
    int V;             // 顶点个数
    list<int> *adj;    // 邻接表
    queue<int> q;      // 维护一个入度为0的顶点的集合
    int* indegree;     // 记录每个顶点的入度
public:
    Graph(int V);                   // 构造函数
    ~Graph();                       // 析构函数
    void addEdge(int v, int w);     // 添加边
    bool topological_sort();        // 拓扑排序
};

/************************类定义************************/
Graph::Graph(int V)
{
    this->V = V;
    adj = new list<int>[V];

    indegree = new int[V];  // 入度全部初始化为0
    for(int i=0; i<V; ++i)
        indegree[i] = 0;
}

Graph::~Graph()
{
    delete [] adj;
    delete [] indegree;
}

void Graph::addEdge(int v, int w)
{
    adj[v].push_back(w); 
    ++indegree[w];
}

bool Graph::topological_sort()
{
    for(int i=0; i<V; ++i)
        if(indegree[i] == 0)
            q.push(i);         // 将所有入度为0的顶点入队

    int count = 0;             // 计数,记录当前已经输出的顶点数 
    while(!q.empty())
    {
        int v = q.front();      // 从队列中取出一个顶点
        q.pop();

        cout << v << " ";      // 输出该顶点
        ++count;
        // 将所有v指向的顶点的入度减1,并将入度减为0的顶点入栈
        list<int>::iterator beg = adj[v].begin();
        for( ; beg!=adj[v].end(); ++beg)
            if(!(--indegree[*beg]))
                q.push(*beg);   // 若入度为0,则入栈
    }

    if(count < V)
        return false;           // 没有输出全部顶点,有向图中有回路
    else
        return true;            // 拓扑排序成功
}

并查集:

并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。

顾名思义,并查集支持两种操作:

合并(Union):合并两个元素所属集合(合并对应的树)

查询(Find):查询某个元素所属集合(查询对应的树的根节点),这可以用于判断两个元素是否属于同一集合

并查集在经过修改后可以支持单个元素的删除、移动;使用动态开点线段树还可以实现可持久化并查集。

const int  N=1005                   //指定并查集所能包含元素的个数(由题意决定)
int pre[N];                         //存储每个结点的前驱结点 
int rank[N];                        //树的高度 
void init(int n)                    //初始化函数,对录入的 n个结点进行初始化 
{
    for(int i = 0; i < n; i++){
        pre[i] = i;                 //每个结点的上级都是自己 
        rank[i] = 1;                //每个结点构成的树的高度为 1 
    } 
}
int find(int x)                     //查找结点 x的根结点 
{
    if(pre[x] == x) return x;       //递归出口:x的上级为 x本身,则 x为根结点 
    return find(pre[x]);            //递归查找 
} 

int find(int x)                     //改进查找算法:完成路径压缩,将 x的上级直接变为根结点,那么树的高度就会大大降低 
{
    if(pre[x] == x) return x;       //递归出口:x的上级为 x本身,即 x为根结点 
    return pre[x] = find(pre[x]);   //此代码相当于先找到根结点 rootx,然后 pre[x]=rootx 
} 

bool isSame(int x, int y)           //判断两个结点是否连通 
{
    return find(x) == find(y);      //判断两个结点的根结点(即代表元)是否相同 
}

bool join(int x,int y)
{
    x = find(x);                        //寻找 x的代表元
    y = find(y);                        //寻找 y的代表元
    if(x == y) return false;            //如果 x和 y的代表元一致,说明他们共属同一集合,则不需要合并,返回 false,表示合并失败;否则,执行下面的逻辑
    if(rank[x] > rank[y]) pre[y]=x;     //如果 x的高度大于 y,则令 y的上级为 x
    else                                //否则
    {
        if(rank[x]==rank[y]) rank[y]++; //如果 x的高度和 y的高度相同,则令 y的高度加1
        pre[x]=y;                       //让 x的上级为 y
    }
    return true;                        //返回 true,表示合并成功
}

模版:

class UnionFind {
public:
    std::vector<int> parent;  // 存储每个元素的父节点
    std::vector<int> size;    // 存储每个集合的大小
    int n;                    // 元素的数量
    int component_count;      // 当前连通分量的数量

public:
    // 构造函数,初始化并设置每个元素的父节点为自己,初始时每个集合的大小为1
    UnionFind(int num_elements) : n(num_elements), component_count(num_elements), 
                                  parent(num_elements), size(num_elements, 1) {
        // 每个元素的父节点初始化为它自己
        std::iota(parent.begin(), parent.end(), 0);
    }

    // 查找元素x所在集合的代表元素(根节点),并进行路径压缩优化
    int find(int x) {
        // 如果x是集合的代表元素(根节点),直接返回
        if (parent[x] == x) {
            return x;
        }
        // 否则递归找到x的根节点,并在回溯过程中进行路径压缩
        return parent[x] = find(parent[x]);
    }

    // 合并两个元素x和y所在的集合
    void unite(int x, int y) {
        // 找到x和y的代表元素
        int root_x = find(x);
        int root_y = find(y);

        // 如果x和y已经在同一个集合中,不需要合并
        if (root_x == root_y) {
            return;
        }

        // 合并时,确保较小的集合并入较大的集合
        if (size[root_x] < size[root_y]) {
            std::swap(root_x, root_y);
        }

        // 将root_y的根节点指向root_x
        parent[root_y] = root_x;
        // 更新合并后集合的大小
        size[root_x] += size[root_y];

        // 合并操作减少了一个连通分量
        --component_count;
    }

    // 判断x和y是否在同一个集合中
    bool connected(int x, int y) {
        return find(x) == find(y);
    }

    // 获取当前的连通分量数
    int getComponentCount() const {
        return component_count;
    }
};

单调栈

单调栈(Monotone Stack):一种特殊的栈。在栈的「先进后出」规则基础上, 要求「从 栈顶 到 栈底 的元素是单调递增(或者单调递减)」。其中满足从栈顶到栈底的元素是单调递增的栈,叫做「单调递增栈」。满足从栈顶到栈底的元素是单调递减的栈,叫做「单调递减栈」。

单调递增栈模板

def monotoneIncreasingStack(nums):
    stack = []
    for num in nums:
        while stack and num >= stack[-1]:
            stack.pop()
        stack.append(num)

回溯算法

回溯算法是对树形或者图形结构执行一次深度优先遍历,实际上类似枚举的搜索尝试过程,在遍历的过程中寻找问题的解。

深度优先遍历有个特点:当发现已不满足求解条件时,就返回,尝试别的路径。此时对象类型变量就需要重置成为和之前一样,称为「状态重置」。

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

from typing import List

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        n = len(nums)
        # 结果列表
        ans = []
        # 当前路径
        path = [0] * n
        # 是否已在路径中
        on_path = [False] * n

        # 对 nums 排序
        nums.sort()

        def DFS(c):
            # 递归结束条件
            if c == n:
                ans.append(path.copy())
                return
            for x in range(n):
                # 如果当前数字已经使用过,跳过
                if on_path[x]:
                    continue
                # 如果当前数字与前一个数字相同,且前一个数字还未被使用,跳过(避免重复)
                if x > 0 and nums[x] == nums[x - 1] and not on_path[x - 1]:
                    continue
                # 选择当前数字
                on_path[x] = True
                path[c] = nums[x]
                DFS(c + 1)
                # 撤销选择
                on_path[x] = False

        DFS(0)
        return ans

字典树

概念:字典树(TrieTree),是一种树形结构,典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串,如01字典树)。主要思想是利用字符串的公共前缀来节约存储空间。很好地利用了串的公共前缀,节约了存储空间。字典树主要包含两种操作,插入和查找。

class trie:
    def __init__(self):
        self.nex = [[0 for i in range(26)] for j in range(100000)]
        self.cnt = 0
        self.exist = [False] * 100000  # 该结点结尾的字符串是否存在

    def insert(self, s):  # 插入字符串
        p = 0
        for i in s:
            c = ord(i) - ord("a")
            if not self.nex[p][c]:
                self.cnt += 1
                self.nex[p][c] = self.cnt  # 如果没有,就添加结点
            p = self.nex[p][c]
        self.exist[p] = True

    def find(self, s):  # 查找字符串
        p = 0
        for i in s:
            c = ord(i) - ord("a")
            if not self.nex[p][c]:
                return False
            p = self.nex[p][c]
        return self.exist[p]