Analysis of Set Union Algorithms 题解
题意简述
有一个集合,初始为空,你需要写一个数据结构,支持:
0 x
表示将 \(x\) 加入该集合,其中 \(x\) 为一由 \(\texttt{0} \sim \texttt{9}\) 组成的数字串,长度 \(\leq 50\)。1 x
表示查询 \(x\) 是否存在于该集合中,长度总和 \(\leq 8 \times 10^6\)。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 题解的更多相关文章
- UVa 1329 - Corporative Network Union Find题解
UVa的题目好多,本题是数据结构的运用,就是Union Find并查集的运用.主要使用路径压缩.甚至不须要合并树了,由于没有反复的连线和改动单亲节点的操作. 郁闷的就是不太熟悉这个Oj系统,竟然使用库 ...
- 康复计划#4 快速构造支配树的Lengauer-Tarjan算法
本篇口胡写给我自己这样的老是证错东西的口胡选手 以及那些想学支配树,又不想啃论文原文的人- 大概会讲的东西是求支配树时需要用到的一些性质,以及构造支配树的算法实现- 最后讲一下把只有路径压缩的并查集卡 ...
- 遗传编程(GA,genetic programming)算法初探,以及用遗传编程自动生成符合题解的正则表达式的实践
1. 遗传编程简介 0x1:什么是遗传编程算法,和传统机器学习算法有什么区别 传统上,我们接触的机器学习算法,都是被设计为解决某一个某一类问题的确定性算法.对于这些机器学习算法来说,唯一的灵活性体现在 ...
- 机器学习算法之旅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 ...
- ICLR 2013 International Conference on Learning Representations深度学习论文papers
ICLR 2013 International Conference on Learning Representations May 02 - 04, 2013, Scottsdale, Arizon ...
- Training Deep Neural Networks
http://handong1587.github.io/deep_learning/2015/10/09/training-dnn.html //转载于 Training Deep Neural ...
- 【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 ...
- 使用Apriori算法和FP-growth算法进行关联分析
系列文章:<机器学习实战>学习笔记 最近看了<机器学习实战>中的第11章(使用Apriori算法进行关联分析)和第12章(使用FP-growth算法来高效发现频繁项集).正如章 ...
- 313. Super Ugly Number
题目: Write a program to find the nth super ugly number. Super ugly numbers are positive numbers whose ...
- Awesome (and Free) Data Science Books[转]
Post Date: September 3, 2014By: Stephanie Miller Marty Rose, Data Scientist in the Acxiom Product an ...
随机推荐
- SDL3 入门(2):第一个窗口
在上一篇文章中我们已经利用 SDL 的日志接口实现了简单的字符串输出,实际上是解决了开发环境搭建问题,接下来我们将在已有代码的基础上继续开发,实现第一个窗口的创建和背景色绘制. 初始化 首先设置日志输 ...
- maven和gradle环境变量配置及idea相关的设置
1.maven 环境变量添加之后,重新打开cmd窗口,验证是否配置成功. idea File >> Settings idea配置之后的验证: 2.gradle 环境变量添加之后,重新 ...
- excel计算日期天数和表格冻结首行
excel计算日期天数和表格冻结首行 1.在单元格E35中输入公式DATEDIF(A35.B35."MD")MD表起始日期.结束日期天数差."Y" 时间段中的整 ...
- 交互式转化批处理工具 expect
交互式转化批处理工具 expect expect中相关命令 spawn 启动新的进程 expect 从进程接收字符串 send 用于向进程发送字符串 interact 允许用户交互 exp_conti ...
- ecnuoj 5042 龟速飞行棋
5042. 龟速飞行棋 题目链接:5042. 龟速飞行棋 赛中没过,赛后补题时由于题解有些抽象,自己写个题解. 可以发现每次转移的结果只跟后面两个点的胜负状态有关. 不妨设 \(f_{u,a,b}\) ...
- 全志A40i+Logos FPGA开发板(4核ARM Cortex-A7)硬件说明书(下)
前 言 本文档主要介绍板卡硬件接口资源以及设计注意事项等内容,测试板卡为创龙科技旗下的全志A40i+Logos FPGA开发板. 核心板的ARM端和FPGA端的IO电平标准一般为3.3V,上拉电源一般 ...
- Redis缓存满了,如何存放数据?缓存淘汰策略
我们的redis使用的是内存空间来存储数据的,但是内存空间毕竟有限,随着我们存储数据的不断增长,当超过了我们的内存大小时,即在redis中设置的缓存大小(maxmeory 4GB),redis会怎么处 ...
- SQL注入方法
目录 前言 如何测试与利用注入点 手工 注入思路 工具 sqlmap -r -u -m --level --risk -v -p --threads -batch-smart --os-shell - ...
- mac 安装homebrew 报443
描述 macOS安装Homebrew时总是报错(Failed to connect to raw.githubusercontent.com port 443: Connection refused) ...
- SafeLine Web 安全网关保护你的网站不受黑客攻击
SafeLine 简介 今天,推荐给大家的是一款在社区广受好评的网站防护工具 -- SafeLine Web 安全网关. 简单来说这是一个自带安全 buf 的 Nginx,它基于业界领先的语义分析检测 ...