题意简述

有一个集合,初始为空,你需要写一个数据结构,支持:

  1. 0 x 表示将 \(x\) 加入该集合,其中 \(x\) 为一由 \(\texttt{0} \sim \texttt{9}\) 组成的数字串,长度 \(\leq 50\)。
  2. 1 x 表示查询 \(x\) 是否存在于该集合中,长度总和 \(\leq 8 \times 10^6\)。
  3. 2 x y 表示令数字串 \(x\) 和 \(y\) 纠缠。不保证 \(x\) 和 \(y\) 在集合中。如果 \(\texttt{A}\) 与 \(\texttt{B}\) 纠缠,并且 \(\texttt{BT}\) 在集合中,则认为 \(\texttt{AT}\) 也在集合中。

注意集合中可能有无穷多个数字串。

题目分析

发现,询问时,字符串总是通过不断通过“纠缠”改变前缀,最后来到一个已经插入的字符串。所以,所谓“纠缠”,就是令两个前缀相等的过程。分析到这,如果没有“纠缠”操作,做法是什么?别跟我说字符串哈希。对,看到前缀和字符串匹配,用个 Trie 树匹配就行了。可是有“纠缠”,怎么解决呢?

一个很直接的想法是,分别在字典树上找到这两个前缀对应的结点,然后在结点之间连一条边。在匹配时可以往下匹配,也可以在额外这些边上走。轻松写出如下(赛时)代码:

unordered_map<int, bool> vis[805010];

bool dfs(int now, char str[], int cur) {
if (vis[cur][now]) return false;
vis[cur][now] = true;
for (auto to: tree[now].trans) {
if (dfs(to, str, cur)) return true;
}
if (!str[cur]) {
if (tree[now].ed) return true;
return false;
}
if (tree[now].son[str[cur] ^ 48]) {
if (dfs(tree[now].son[str[cur] ^ 48], str, cur + 1)) {
return true;
}
}
return false;
}

先抛开时间空间不谈,这个算法还是错的,(没拿到一点点分),为什么?

考虑如下数据:

4
2 0 2
2 02 5
0 22
1 5

显然,匹配的过程可以被表示为:\(\texttt{5} \rightarrow \texttt{02} \rightarrow \texttt{22}\)。但是上述代码给出了无解。原因就是我们无法更改已经匹配好的前缀的前缀。

这是一个棘手的问题,但也让我们意识到,字典树上,两个前缀相等,其后代也是等价的。难道我们还要在后代上连边?!当然不是。因为你可能还要不断插入,非常难解决。

不妨换个角度思考,两个前缀相等,以后就不会改变了,不妨把这两个结点以及子树合并起来。也即,后续访问到任意一个点的时候,在合并之后的版本上面操作,这样就可以影响所有合法的节点了。合并起来,做一个映射关系,想到并查集。

这就是并查集合并集合的优势了。当结点之间完全没有区别,与其插入的时候分别插入或查询的时候都扫一遍,不如将其合并起来,后续查询、修改,都在合并出来的节点上进行。

时间复杂度分析

插入查询和并查集都很好分析。合并的时候,每个节点只会被合并一次,并且合并复杂度就是这些结点个数,总和是字典树结点个数,所以是正确的。

代码

#include <cstdio>
#include <iostream>
using namespace std; const int N = 1000010;
const int L = 8000010; struct node {
int son[10];
bool ed;
} tree[N];
int fa[N], tot; inline int newNode() { return ++tot, fa[tot] = tot; } int get(int x) { return fa[x] == x ? x : fa[x] = get(fa[x]); } int insert(char str[], bool real) { // 在线段树上找到 str 对应的结点
int now = 0;
for (int i = 1; str[i]; ++i) {
int &son = tree[now].son[str[i] ^ 48];
if (!son)
son = newNode();
now = son = get(son); // 注意这里访问是合并之后的版本
}
tree[now].ed |= real;
return now;
} bool query(char str[]) { // 查询也是普通地查询
int now = 0;
for (int i = 1; str[i]; ++i) {
int &son = tree[now].son[str[i] ^ 48];
if (!son)
return false;
now = son = get(son); // 同理
}
return tree[now].ed;
} int combine(int u, int v) {
if (!u || !v || u == v) // 合并过、或者有一个不存在了就返回
return u | v;
fa[u] = v;
for (int i = 0; i < 10; ++i) {
int &su = tree[u].son[i];
int &sv = tree[v].son[i];
su = get(su), sv = get(sv);
sv = combine(su, sv), v = get(v); // 注意,可能会影响到 v,所以注意时刻操作合并后的结点
}
tree[v].ed |= tree[u].ed;
return v;
} int n; signed main() {
#ifndef XuYueming
freopen("tarjan.in", "r", stdin);
freopen("tarjan.out", "w", stdout);
#endif
scanf("%d", &n);
for (int i = 1, op; i <= n; ++i) {
static char str[L];
scanf("%d%s", &op, str + 1);
if (op == 0) {
insert(str, true);
} else if (op == 1) {
puts(query(str) ? "1" : "0");
} else {
int x = insert(str, false);
scanf("%s", str + 1);
int y = insert(str, false);
combine(x, y);
}
}
return 0;
}

后记 & 反思

看到前缀和匹配,想到 Trie 树。(谁让它有个名字叫前缀树呢。)

遇到两个结点在之后完全等价,可以使用并查集加速。

Analysis of Set Union Algorithms 题解的更多相关文章

  1. UVa 1329 - Corporative Network Union Find题解

    UVa的题目好多,本题是数据结构的运用,就是Union Find并查集的运用.主要使用路径压缩.甚至不须要合并树了,由于没有反复的连线和改动单亲节点的操作. 郁闷的就是不太熟悉这个Oj系统,竟然使用库 ...

  2. 康复计划#4 快速构造支配树的Lengauer-Tarjan算法

    本篇口胡写给我自己这样的老是证错东西的口胡选手 以及那些想学支配树,又不想啃论文原文的人- 大概会讲的东西是求支配树时需要用到的一些性质,以及构造支配树的算法实现- 最后讲一下把只有路径压缩的并查集卡 ...

  3. 遗传编程(GA,genetic programming)算法初探,以及用遗传编程自动生成符合题解的正则表达式的实践

    1. 遗传编程简介 0x1:什么是遗传编程算法,和传统机器学习算法有什么区别 传统上,我们接触的机器学习算法,都是被设计为解决某一个某一类问题的确定性算法.对于这些机器学习算法来说,唯一的灵活性体现在 ...

  4. 机器学习算法之旅A Tour of Machine Learning Algorithms

    In this post we take a tour of the most popular machine learning algorithms. It is useful to tour th ...

  5. ICLR 2013 International Conference on Learning Representations深度学习论文papers

    ICLR 2013 International Conference on Learning Representations May 02 - 04, 2013, Scottsdale, Arizon ...

  6. Training Deep Neural Networks

    http://handong1587.github.io/deep_learning/2015/10/09/training-dnn.html  //转载于 Training Deep Neural ...

  7. 【Repost】A Practical Intro to Data Science

    Are you a interested in taking a course with us? Learn about our programs or contact us at hello@zip ...

  8. 使用Apriori算法和FP-growth算法进行关联分析

    系列文章:<机器学习实战>学习笔记 最近看了<机器学习实战>中的第11章(使用Apriori算法进行关联分析)和第12章(使用FP-growth算法来高效发现频繁项集).正如章 ...

  9. 313. Super Ugly Number

    题目: Write a program to find the nth super ugly number. Super ugly numbers are positive numbers whose ...

  10. Awesome (and Free) Data Science Books[转]

    Post Date: September 3, 2014By: Stephanie Miller Marty Rose, Data Scientist in the Acxiom Product an ...

随机推荐

  1. 时间戳,mysql 秒数,毫秒数与时间之间的相互转换

    时间戳,mysql 秒数,毫秒数与时间之间的相互转换 时间戳是指格林威治时间自1970年1月1日(00:00:00 GMT)至当前时间的总秒数.通俗的讲,时间戳是一份能够表示一份数据在一个特定时间点已 ...

  2. IDEA安装配置

    1.安装IDEA选择免费体验 2.下载对应版本的破解补丁 agent.jar -2.1 将agent.jar补丁和important.txt放置到idea安装目录 3.修改VMoption javaa ...

  3. python_8 拆包、内置函数和高阶函数

    一.查缺补漏 1. \t 子表符,用于对其二.拆包 1. 拆包:顾名思义就是将可迭代的对象如元组,列表,字符串,集合,字典,拆分出相对应的元素 2. 形式:拆包一般分两种方式,一种是以变量的方式来接收 ...

  4. 树莓派4B-PCA9685驱动舵机

    前言 不知道你们有没有遇到过这么一种情况,直接用树莓派的引脚输出PWM控制舵机,舵机是会出现抖动的.就算代码进行一定的时延迟优化还是会有影响的. 现在我们可以使用PCA9685这个模块去驱动舵机. P ...

  5. Codeforces Round 935 (Div. 3)

    A. Setting up Camp 题目描述 The organizing committee plans to take the participants of the Olympiad on a ...

  6. VUEX - 手稿

  7. 移动WEB开发之 -- 流式布局

    浏览器现状 视口 视口标签 二倍图 手机端和pc端像素比例不一样 物理像素&物理像素比 背景缩放background-size 背景图片二倍图 移动端开发选择 移动端技术解决方案 特殊样式 常 ...

  8. mac navicat免激活版

    Navicat 12 第一步:终端执行 sudo spctl --master-disable 第二步:下载安装即可使用 https://pan.baidu.com/s/1tHq-wqAIggD0Fo ...

  9. 【算法】在vue3的ts代码中分组group聚合源数据列表

    有一个IList<any>()对象列表, 示例数据为[{id:'1',fieldName:'field1',value:'1'},{id:'1',fieldName:'field2',va ...

  10. 网易数帆开源贡献获业界肯定,轻舟API网关获OSCAR尖峰开源技术创新奖

    2020年10月16日,由中国信息通信研究院主办的"2020开源产业大会"在北京线下与线上同步召开,主办方在会上公布了"OSCAR尖峰开源奖项"各个奖项的评选结 ...