自动机模型+字典树v2.0

有时候我们要维护一个字符串集合,然后支持插入、删除、查询某个字符串出现次数和查询某个字符串作为前缀的出现次数。

显然的,暴力肯定 T 飞。

hash:我来!(非常好数据,使我的 hash WA)

所以我们需要字典树。

字典树有三大两大优点:

  • 速度快

  • 无失误(hash 有一定概率会冲突)

  • 支持多模式串匹配

但是,字典树的内存特别庞大,所以数据规模庞大的时候还是乖乖用 hash 吧。

不过,字典树似乎不支持快速判断区间回文串(hash + 树状数组可以 \(O(\log N)\) 完成)。

字典树的结构

下图是一个由 \(\{ \text{am}, \text{are}, \text{be}, \text{been}, \text{can} \}\) 构成的 Trie:

trie.bmp

可以看到,字典树是一个有向树,其中红色节点是“接收状态”。

解释 至于为什么,首先需要理解自动机的概念。

自动机用于判断一组信号序列是否合法(很像图灵机是不是)。

比如说,家门口-韶山路-东塘-中雅培粹对面,就是一个可以去学校的信号序列。

但是,又比如家门口-砂子塘社区-砂子塘小学,也是一个可以去学校的信号序列,尽管它会去到另一个学校。自动机上的接收状态不是唯一的。

以下是一个判断某个数是否是偶数(以二进制输入)的自动机:

12.bmp

而 Trie 树,就是在给定一个字符串后,如果这个字符串是字符串集合的一个串,那么就接受(上面的“查询某个字符串出现次数和查询某个字符串作为前缀的出现次数”需要两个 Trie,但是可以压成一个)

字典树的操作

插入

很简单,遇到不存在的节点创造一个。

具体来说,我们把字符一个一个插入进去。发现节点不存在之后,我们新建一个节点,然后继续插入。

下图是上面的字典树插入 \(\text{argue}\) 的效果:

trie-insert-argue.bmp

绿色的节点是新来的节点,棕色的是新来的接受节点(红+绿=棕)

在插入的时候,记得维护信息。当维护的信息过于复杂的时候,循环写法可能会特别复杂,此时推荐递归。

void _insert(node *cur, int id, const string &x) {
  if(id == x.size()) {
    cur -> cnt++;
    cur -> sb++;
    return ;
  }
  cur -> sb++;
  if(cur -> son[x[id]] == NULL) {
    cur -> son[x[id]] = new node;
  }
  _insert(cur -> son[x[id]], id + 1, x);
}
void insert(const string &s) {
  _insert(root, 0, s);
}

删除

像插入一样,我们从最下面的节点开始进行,在维护信息的时候,如果这个节点不需要了,就将它删除。

下图是删除 \(\text{been}\) 的效果:

trie-erase-been.bmp

由于类似于“迭代器失效”的问题,所以删除的递归会比循环好写。

bool _erase(node *cur, int id, const string &x) {
  if(id == x.size()) {
    cur -> cnt--;
    cur -> sb--;
    return 1;
  }
  if(cur == NULL) {
    return 0;
  }
  cur -> sb--;
  bool tmp = _erase(cur -> son[x[id]], id + 1, x);
  if((!(cur -> sb)) && cur != root) {
    delete cur;
  }
  return tmp;
}
//返回值代表是否成功删除
bool erase(const string &s) {
  return _erase(root, 0, s);
}

查找某个字符串|数某个字符串的出现次数

我们顺着字典树的边往下走,如果到达的状态是接受状态,那就表明找到了。

如下图(绿色的边代表走过的边,蓝色的点代表走到的点)展示了查找 \(\text{are}\)

trie-find-are.bmp

查找某个前缀

还是往下走,但是到达的状态不必是接受状态,只要存在就行了。但是,为了这个任务,我们需要额外维护子树和。

如图展示了查找 \(\text{ar}\) 的前缀的效果,表示法同上:

trie-prefix-ar.bmp

node *_find(node *cur, int id, const string &x) {
  if(id == x.size()) {
    return cur;
  }
  if(cur == NULL) {
    return cur;
  }
  return _find(cur -> son[x[id]], id + 1, x);
}
//维护了子树和和出现次数,所以一个 find 就足够做上面的所有任务
node *find(const string &s) {
  return _find(root, 0, s);
}

完整版

写的指针+封装。

#include <cstddef>
#include <vector>

#undef NULL
#define NULL nullptr

struct trie {
  struct node {
    node *son[256];
    int cnt, sb;

    node() : cnt(0), sb(0) {
      std::fill(son, son + 256, NULL);
    }
    node(const node &b) : cnt(b.cnt), sb(b.sb) {
      std::copy(b.son, b.son + 256, son);
    }
    ~node() {
      for(int i = 0; i < 256; i++) {
        delete son[i];
      }
    }
  };

  node *root;

  trie() : root(NULL) {
    root = new node;
  }
  ~trie() {
    delete root;
  }

  void _insert(node *cur, int id, const string &x) {
    if(id == x.size()) {
      cur -> cnt++;
      cur -> sb++;
      return ;
    }
    cur -> sb++;
    if(cur -> son[x[id]] == NULL) {
      cur -> son[x[id]] = new node;
    }
    _insert(cur -> son[x[id]], id + 1, x);
  }
  void insert(const string &s) {
    _insert(root, 0, s);
  }

  bool _erase(node *cur, int id, const string &x) {
    if(id == x.size()) {
      cur -> cnt--;
      cur -> sb--;
      return 1;
    }
    if(cur == NULL) {
      return 0;
    }
    cur -> sb--;
    bool tmp = _erase(cur -> son[x[id]], id + 1, x);
    if((!(cur -> sb)) && cur != root) {
      delete cur;
    }
    return tmp;
  }
  //返回值代表是否成功删除
  bool erase(const string &s) {
    return _erase(root, 0, s);
  }

  node *_find(node *cur, int id, const string &x) {
    if(id == x.size()) {
      return cur;
    }
    if(cur == NULL) {
      return cur;
    }
    return _find(cur -> son[x[id]], id + 1, x);
  }
  //维护了子树和和出现次数,所以一个 find 就足够做上面的所有任务
  node *find(const string &s) {
    return _find(root, 0, s);
  }
};
posted @ 2024-04-02 12:48  hhc0001  阅读(5)  评论(0编辑  收藏  举报