这篇文章上次修改于 355 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

算法学习笔记(2): 基环树&字典树

基环树

若一个图有n个节点,n-1条边,那么这个图一定无环,此时若再增加一条边,必定会出现一条环。

由此可得 n个节点,n条边,必定有个环,去掉这个环的每个边,剩余的就是多颗树。

对于一个这样的图我们就称为基环树

基环树实际上是图而不是树

无向图上的基环树

graph LR
1((1))
2((2))
3((3))
4((4))
5((5))
6((6))
7((7))

3 --- 4
4 --- 5
5 --- 3

2 --- 4
1 --- 2
6 --- 2
7 --- 5

图中的3 4 5形成一个环,这就是基环树的基本特征

同时,如果图不一定全联通,那就有多个基环树组成,有几个联通分量就有几个基环树

有向图上的基环树

同样是n个节点,n条边作为前提条件

内向基环树

若每个点都只有一条出度,该图为内向基环树

graph LR
1((1))
2((2))
3((3))
4((4))
5((5))
6((6))
7((7))

3 ---> 4
4 ---> 5
5 ---> 3

2 ---> 4
1 ---> 2
6 ---> 2
7 ---> 5

除了环以外的树枝上的节点都指向其树的父节点

外向基环树

若每个点都只有一条入度,该图为内向基环树

graph RL
1((1))
2((2))
3((3))
4((4))
5((5))
6((6))
7((7))

3 ---> 4
4 ---> 5
5 ---> 3


2 ---> 1
2 ---> 6
5 ---> 7
4 ---> 2

除了环以外的树枝上的节点都是树的父节点指向子节点。

寻找环

tarjan 算法

搜索每个节点,标记节点的 DFN 值,并记录每个结点的前驱;

若搜索到一条返祖边,则从当前节点沿前驱节点回溯至边的另一端点,则回溯路上的所有节点均为环上的节点;

vector <edge> g[MAXN];
int dfn[MAXN], cnt, fa[MAXN], loop[MAXN], len;
void dfs_loop(int i) {
	dfn[i] = ++cnt;
	for (int t = 0; t < g[i].size(); t++){
		int v = g[i][t];
		if (v == fa[i]) continue;
		if (!dfn[v]) {
			fa[v] = i;
			dfs_loop(v);
		} else {
			if (dfn[v] < dfn[i]) continue; 
			loop[++len] = v;
			for (; v != i; v = fa[v]) loop[++len] = fa[v];
		}
	}
}

拓扑排序

记录每个节点的度数,进行拓扑排序;

则入度 ≥ 2 的点即为环上的点;

则拓扑排序后度数不为 0 的节点即为环上的点;

void topo_loop() {
	queue <int> q;  
	for (int i = 1; i <= n; i++) {
		if (de[i] == 1) q.push(i);
	}   
    while (!q.empty()) {
        int i = q.front();
		q.pop();
        for (int t = 0; t < g[i].size(); t++) {
            int v = g[i][t];
			if (de[v] > 1) {
            	de[v]--;
            	if (de[v] == 1) q.push(v);
			}
        }
    }
    return;
}

字典树

字典树,又称Trie树、前缀树,是一种树形结构,用于统计、排序、和保存大量字符串。所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较。

// Trie节点
struct TrieNode {
    unordered_map<char, TrieNode*> children;
    bool isEndOfWord;

    TrieNode() : isEndOfWord(false) {}

    // 递归释放内存
    ~TrieNode() {
        for (auto& pair : children) {
            delete pair.second;
        }
    }
};

// Trie类
class Trie {
private:
    TrieNode* root;

public:
    Trie() {
        root = new TrieNode();
    }

    // 插入单词
    void insert(const string& word) {
        TrieNode* node = root;
        for (char ch : word) {
            if (node->children.find(ch) == node->children.end()) {
                node->children[ch] = new TrieNode();
            }
            node = node->children[ch];
        }
        node->isEndOfWord = true;
    }

    // 搜索单词
    bool search(const string& word) {
        TrieNode* node = root;
        for (char ch : word) {
            if (node->children.find(ch) == node->children.end()) {
                return false;
            }
            node = node->children[ch];
        }
        return node != nullptr && node->isEndOfWord;
    }

    // 判断是否有单词以给定前缀开头
    bool startsWith(const string& prefix) {
        TrieNode* node = root;
        for (char ch : prefix) {
            if (node->children.find(ch) == node->children.end()) {
                return false;
            }
            node = node->children[ch];
        }
        return node != nullptr;
    }

    // 释放内存
    ~Trie() {
        delete root;
    }
};

每一个节点就是一个字符,可以储存多个字符的前缀情况。