题意简述

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

  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. Spring扩展———自定义bean组件注解

    引言 Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制. Java 语言中的类.方法.变量.参数和包等都可以被标注.和 Javadoc 不同,Java ...

  2. C#/.NET/.NET Core拾遗补漏合集(24年6月更新)

    前言 在这个快速发展的技术世界中,时常会有一些重要的知识点.信息或细节被忽略或遗漏.<C#/.NET/.NET Core拾遗补漏>专栏我们将探讨一些可能被忽略或遗漏的重要知识点.信息或细节 ...

  3. CSS3随机背景图片切换特效

    Tips:当你看到这个提示的时候,说明当前的文章是由原emlog博客系统搬迁至此的,文章发布时间已过于久远,编排和内容不一定完整,还请谅解` CSS3随机背景图片切换特效 日期:2018-5-16 阿 ...

  4. C# .NET core Avalonia 11.0版本,发布linux和MAC的简单记录

    .net core 7.0+centos 7.0 cetnos目前运行在hyper V虚拟机里 虚拟机部署的注意事项 1 需要配置网络环境, 确保在同一局域网下 如果sftp无法连接 ctrl+shi ...

  5. Kotlin 变量详解:声明、赋值与最佳实践指南

    Kotlin 变量 变量是用于存储数据值的容器. 要创建一个变量,使用 var 或 val,然后使用等号(=)给它赋值: 语法 var 变量名 = 值 val 变量名 = 值 示例 var name ...

  6. 用cvCvtColor转化RGB彩色图像为灰度图像时发生的小失误

    版本信息 MAC版本:10.10.5 Xcode版本:7.2 openCV版本:2.4.13 在运行程序的时候发现cvCvtColor的地方程序报错 error: (-215) src.depth() ...

  7. arm linux 移植 e2fsprogs

    背景 之前在zynq平台下处理系统分区,用到了SPI-FLASH以及EMMC. 根据ZYNQ平台的特性以及产品升级需要,规划了 SPI-FLASH放置BootLoader EMMC中分为2个区,一个F ...

  8. GGTalk 开源即时通讯系统源码剖析之:聊天消息防错漏机制

    继上篇<GGTalk 开源即时通讯系统源码剖析之:客户端全局缓存及本地存储>GGTalk客户端的全局缓存以及客户端的本地持久化存储.接下来我们将介绍GGTalk的聊天消息防错漏机制. GG ...

  9. require模块化 AMD和CMD

    在CommonJS中,有一个全局性方法require(),用于加载模块.假定有一个数学模块math.js,就可以像下面这样加载. 1 var math = require('math'); 然后,就可 ...

  10. java 高效递归查询树 find_in_set 处理递归树

    建表语句 DROP TABLE IF EXISTS `sys_dept`; CREATE TABLE `sys_dept` ( `id` bigint(20) NOT NULL AUTO_INCREM ...