题意简述

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

  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. kali更换apt镜像

    kali更换apt镜像 vim /etc/apt/sources.list 进来之后按i进入编辑模式,把其他的镜像#注释掉之后,加上新的镜像,然后esc退出编辑,按:输入wq保存并退出! 上面的办法不 ...

  2. ChatGPT应用与实践初探

    近期,长江商学院EMBA38期&甄知科技开展了题为"ChatGPT应用与实践初探"的线下沙龙活动,由上海甄知科技创始合伙人兼CTO张礼军主讲,主要给大家解密最近很火的Cha ...

  3. Json字符串转换处理html编码格式,= \u003d 处理

    Json字符串转换处理html编码格式,=  \u003d 处理 import com.alibaba.fastjson.annotation.JSONField; import com.faster ...

  4. Asp.net core Swashbuckle Swagger 的常用配置

    背景 .net core Swashbuckle Swagger 官方文档:https://github.com/domaindrivendev/Swashbuckle.AspNetCore 我们发现 ...

  5. php不使用Office包实现上万条数据导出表格

    经过上传客户要求主副表迁出,又提出可以将某张表的数据导出excel,听着很简单,实际看数据表发现上万条数据,并且需要关联表查询相关字段,导出的表格才可以被客户看明白. 要是使用office包目前后台内 ...

  6. 高通mm-camera平台 Camera bring up基本调试思路

    原文:https://www.cnblogs.com/thjfk/p/4086001.html 确定硬件 1.首先对照原理图,检查camera module的pin脚连接是否正确. 2.用示波器量Ca ...

  7. SpringMVC原理(1)-文件上传请求

    我们文件上传接口只需要在方法参数上写MultipartFile类,mvc就可以帮我们把上传的文件封装为这个类的对 象供我们非常方便的操作,那它是怎么做的呢?我们一起来看看 我们发的请求默认都是由Dis ...

  8. 阿里云日志Nginx日志分析

    每分钟接口访问次数的前200条统计 not request_uri : "/heartbeat.html" | SELECT time_series(time, '1m', '%H ...

  9. 【论文阅读】ICRA2021: VDB-EDT An Efficient Euclidean Distance Transform Algorithm Based on VDB Data Struct

    参考与前言 Summary: 浩哥推荐的一篇 无人机下的建图 and planning实验 Type: ICRA Year: 2021 论文链接:https://arxiv.org/abs/2105. ...

  10. DarkHole_1靶机渗透流程

    VulnHub_DarkHole1靶机渗透流程 注意:部署时,靶机的网络连接模式必须和kali一致,让靶机跟kali处于同一网段,这用kali才能扫出靶机的主机 1. 信息收集 1.1 探测IP 使用 ...