http://codeforces.com/contest/427/problem/D

题目是找出两个串的最短公共子串,并且在两个串中出现的次数只能是1次。

正解好像是dp啥的,但是用sam可以方便很多,复杂度n^2

首先对两个串建立sam,拓扑dp出endpos集合的大小,然后枚举第二个串的所有子串,在两个sam中跑就行了。

很无脑。从[i, j] 递推到[i, j + 1]这个子串,是可以O(1)转移的。

#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
using namespace std;
#define inf (0x3f3f3f3f)
typedef long long int LL;
const int maxn = * + , N = ;
struct Node {
int mxCnt; //mxCnt表示后缀自动机中当前节点识别子串的最大长度
int miCnt; //miCnt表示后缀自动机中当前节点识别子串的最小长度
int id; //表示它是第几个后缀自动机节点,指向了它,但是不知道是第几个,用id判断
int pos; //pos表示它在原串中的位置。
bool flag; //表示当前节点是否能识别前缀
struct Node *pNext[N], *fa;
} suffixAutomaton[maxn * ], sam[maxn * ], *root, *last; //大小需要开2倍,因为有一些虚拟节点
int t; //用到第几个节点
struct Node *create(int mxCnt = -, struct Node *node = NULL) { //新的节点
if (mxCnt != -) {
suffixAutomaton[t].mxCnt = mxCnt, suffixAutomaton[t].fa = NULL;
for (int i = ; i < N; ++i) suffixAutomaton[t].pNext[i] = NULL;
} else {
suffixAutomaton[t] = *node; //保留了node节点所有的指向信息。★全部等于node
//可能需要注意下pos,在原串中的位置。现在pos等于原来node的pos
}
suffixAutomaton[t].id = t; //必须要有的,不然id错误
suffixAutomaton[t].flag = false; //默认不是前缀节点
return &suffixAutomaton[t++];
}
void addChar(int x, int pos) { //pos表示在原串的位置
struct Node *p = last, *np = create(p->mxCnt + , NULL);
np->flag = true;
np->pos = pos, last = np; //last是最尾那个可接收后缀字符的点。
for (; p != NULL && p->pNext[x] == NULL; p = p->fa) p->pNext[x] = np;
if (p == NULL) {
np->fa = root;
np->miCnt = ; // 从根节点引一条边过来
return;
}
struct Node *q = p->pNext[x];
if (q->mxCnt == p->mxCnt + ) { //中间没有任何字符
np->fa = q;
np->miCnt = q->mxCnt + ; // q是7-->8的那些"ab",np是"bab"长度是2+1
return;
}
// p: 当前往上爬到的可以接受后缀的节点
// np:当前插入字符x的新节点
// q: q = p->pNext[x],q就是p中指向的x字符的节点
// nq:因为q->cnt != p->cnt + 1而新建出来的模拟q的节点
struct Node *nq = create(-, q); // 新的q节点,用来代替q,帮助np接收后缀字符
nq->mxCnt = p->mxCnt + ; //就是需要这样,这样中间不包含任何字符
q->miCnt = nq->mxCnt + , np->miCnt = nq->mxCnt + ;
q->fa = nq, np->fa = nq; //现在nq是包含了本来q的所有指向信息
for (; p && p->pNext[x] == q; p = p->fa) {
p->pNext[x] = nq;
}
}
void init() {
t = ;
root = last = create(, NULL);
}
void build(char str[], int lenstr) {
init();
for (int i = ; i <= lenstr; ++i) addChar(str[i] - 'a', i);
}
char str[maxn], sub[maxn];
int in[maxn], que[maxn], dp[][maxn];
unsigned long long int sum[maxn], po[maxn];
bool ok(int en, int len, int lensub) {
unsigned long long int val = sum[en] - sum[en - len] * po[len];
int tim = ;
for (int i = len; i <= lensub; ++i) {
if (val == sum[i] - sum[i - len] * po[len]) tim++;
}
return tim == ;
}
void init(int t, struct Node * suffixAutomaton, int dp[]) {
memset(in, false, sizeof in);
for (int i = ; i < t; ++i) {
if (suffixAutomaton[i].flag) dp[i] = ;
in[suffixAutomaton[i].fa->id]++;
}
int head = , tail = ;
for (int i = ; i < t; ++i) {
if (in[i] == ) que[tail++] = i;
}
while (head < tail) {
int cur = que[head++];
if (cur == ) break;
dp[suffixAutomaton[cur].fa->id] += dp[cur];
in[suffixAutomaton[cur].fa->id]--;
if (in[suffixAutomaton[cur].fa->id] == )
que[tail++] = suffixAutomaton[cur].fa->id;
}
}
void work() {
scanf("%s%s", str + , sub + );
int lenstr = strlen(str + ), lensub = strlen(sub + );
build(str, lenstr);
int sam_t = t;
memcpy(sam, suffixAutomaton, sizeof suffixAutomaton);
build(sub, lensub);
init(sam_t, sam, dp[]);
init(t, suffixAutomaton, dp[]);
// printf("%d\n", dp[1][5]);
int mi = inf;
for (int i = ; i <= lensub; ++i) {
int strnow = , subnow = ;
for (int j = i; j <= lensub; ++j) {
int id = sub[j] - 'a';
if (sam[strnow].pNext[id] == NULL) break;
strnow = sam[strnow].pNext[id]->id;
subnow = suffixAutomaton[subnow].pNext[id]->id;
if (dp[][strnow] == && dp[][subnow] == ) {
mi = min(mi, j - i + );
}
}
}
if (mi == inf) mi = -;
printf("%d\n", mi);
} int main() {
#ifdef local
freopen("data.txt", "r", stdin);
// freopen("data.txt", "w", stdout);
#endif
work();
return ;
}

这题有一个O(n)的算法,那就是,把两个串合并。中间用一个字符分割,这是为了不产生多余的子串。

主要思想就是,要找到一个串,出现的次数为2,并且是在两个不同的串中分别出现的。

出现次数是2,那么就是拓扑dp的时候,endpos集合的大小是2即可。那么怎么限制在两个不同的串中出现过?

记在第一个串出现的时候,id是1 << 0,第二个串出现的时候,id是1 << 1

然后在dp出endpos集合大小的时候,顺便也维护一下在那里出现过即可。

每一个状态,都可能包含了若干个子串,那么需要取最短的子串。

#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
using namespace std;
#define inf (0x3f3f3f3f)
typedef long long int LL;
const int maxn = * + , N = ;
struct Node {
int mxCnt; //mxCnt表示后缀自动机中当前节点识别子串的最大长度
int miCnt; //miCnt表示后缀自动机中当前节点识别子串的最小长度
int id; //表示它是第几个后缀自动机节点,指向了它,但是不知道是第几个,用id判断
int pos; //pos表示它在原串中的位置。
bool flag; //表示当前节点是否能识别前缀
struct Node *pNext[N], *fa;
}suffixAutomaton[maxn * ], *root, *last; //大小需要开2倍,因为有一些虚拟节点
int t; //用到第几个节点
struct Node *create(int mxCnt = -, struct Node *node = NULL) { //新的节点
if (mxCnt != -) {
suffixAutomaton[t].mxCnt = mxCnt, suffixAutomaton[t].fa = NULL;
for (int i = ; i < N; ++i) suffixAutomaton[t].pNext[i] = NULL;
} else {
suffixAutomaton[t] = *node; //保留了node节点所有的指向信息。★全部等于node
//可能需要注意下pos,在原串中的位置。现在pos等于原来node的pos
}
suffixAutomaton[t].id = t; //必须要有的,不然id错误
suffixAutomaton[t].flag = false; //默认不是前缀节点
return &suffixAutomaton[t++];
}
void addChar(int x, int pos) { //pos表示在原串的位置
struct Node *p = last, *np = create(p->mxCnt + , NULL);
np->flag = true;
np->pos = << pos, last = np; //last是最尾那个可接收后缀字符的点。
for (; p != NULL && p->pNext[x] == NULL; p = p->fa) p->pNext[x] = np;
if (p == NULL) {
np->fa = root;
np->miCnt = ; // 从根节点引一条边过来
return;
}
struct Node *q = p->pNext[x];
if (q->mxCnt == p->mxCnt + ) { //中间没有任何字符
np->fa = q;
np->miCnt = q->mxCnt + ; // q是7-->8的那些"ab",np是"bab"长度是2+1
return;
}
// p: 当前往上爬到的可以接受后缀的节点
// np:当前插入字符x的新节点
// q: q = p->pNext[x],q就是p中指向的x字符的节点
// nq:因为q->cnt != p->cnt + 1而新建出来的模拟q的节点
struct Node *nq = create(-, q); // 新的q节点,用来代替q,帮助np接收后缀字符
nq->mxCnt = p->mxCnt + ; //就是需要这样,这样中间不包含任何字符
q->miCnt = nq->mxCnt + , np->miCnt = nq->mxCnt + ;
q->fa = nq, np->fa = nq; //现在nq是包含了本来q的所有指向信息
for (; p && p->pNext[x] == q; p = p->fa) {
p->pNext[x] = nq;
}
}
void init() {
t = ;
root = last = create(, NULL);
}
void build(char str[], int lenstr) {
init();
for (int i = ; i <= lenstr; ++i) addChar(str[i] - 'a', i);
}
char str[maxn], sub[maxn];
int que[maxn * ], in[maxn], dp[maxn], is[maxn];
void work() {
scanf("%s%s", str + , sub + );
init();
for (int i = ; str[i]; ++i) addChar(str[i] - 'a', );
addChar(, );
for (int i = ; sub[i]; ++i) addChar(sub[i] - 'a', );
for (int i = ; i < t; ++i) {
is[i] = suffixAutomaton[i].pos;
if (suffixAutomaton[i].flag) dp[i] = ;
in[suffixAutomaton[i].fa->id]++;
}
int head = , tail = ;
for (int i = ; i < t; ++i) {
if (in[i] == ) que[tail++] = i;
}
while (head < tail) {
int cur = que[head++];
if (!cur) break;
is[suffixAutomaton[cur].fa->id] |= is[cur];
dp[suffixAutomaton[cur].fa->id] += dp[cur];
in[suffixAutomaton[cur].fa->id]--;
if (in[suffixAutomaton[cur].fa->id] == ) que[tail++] = suffixAutomaton[cur].fa->id;
}
int mi = inf;
for (int i = ; i < t; ++i) {
if (is[i] == && dp[i] == ) {
mi = min(mi, suffixAutomaton[i].miCnt); //最短
}
}
if (mi == inf) mi = -;
printf("%d\n", mi);
} int main() {
#ifdef local
freopen("data.txt", "r", stdin);
// freopen("data.txt", "w", stdout);
#endif
work();
return ;
}

也可以直接用广义后缀自动机。

广义后缀自动机能识别多个主串的所有子串,并且在拓扑dp的时候能识别到是在那个串出现的。

广义后缀自动机就是把多个主串统一弄起来,每次都从root开始插入

这就带来一个问题就是已经存在了该节点。

那么就不需要np了。如果该节点能够代替新插入的节点接受后缀,也就是p->mxCnt + 1 == q->mxCnt,中间不含有任何字符。那么last直接去到q就好了,否则就要新建节点nq来弄个节点代替q接受后缀。和后缀自动机一个意思。

ps: 这个节点就是当前id的前缀节点。是属于id的。

#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
using namespace std;
#define inf (0x3f3f3f3f)
typedef long long int LL;
const int MOD = 1e9 + ;
const int maxn = 1e5 + , N = ;
struct Node {
int mxCnt; //mxCnt表示后缀自动机中当前节点识别子串的最大长度
int miCnt; //miCnt表示后缀自动机中当前节点识别子串的最小长度
int id; //表示它是第几个后缀自动机节点,指向了它,但是不知道是第几个,用id判断
int pos; //pos表示它在原串中的位置。
bool flag; //表示当前节点是否能识别前缀
bool R[]; // 广义后缀自动机识别此状态是否在第R[i]个主串中出现过
struct Node *pNext[N], *fa;
}suffixAutomaton[maxn * ], *root, *last; //大小需要开2倍,因为有一些虚拟节点
int t; //用到第几个节点
struct Node *create(int mxCnt = -, struct Node *node = NULL) { //新的节点
if (mxCnt != -) {
suffixAutomaton[t].mxCnt = mxCnt, suffixAutomaton[t].fa = NULL;
for (int i = ; i < N; ++i) suffixAutomaton[t].pNext[i] = NULL;
} else {
suffixAutomaton[t] = *node; //保留了node节点所有的指向信息。★全部等于node
//可能需要注意下pos,在原串中的位置。现在pos等于原来node的pos
}
suffixAutomaton[t].id = t; //必须要有的,不然id错误
suffixAutomaton[t].flag = false; //默认不是前缀节点
return &suffixAutomaton[t++];
}
void addChar(int x, int pos, int id) { //pos表示在原串的位置
struct Node *p = last;
if (p->pNext[x] != NULL) { // 有了,就不需要np
struct Node *q = p->pNext[x];
if (p->mxCnt + == q->mxCnt) {
last = q; //用来接收后缀字符
q->R[id] = true;
q->flag = true;
return;
}
//现在的q没办法成为接受后缀的点
//那么就开一个节点模拟它,所以这个节点是id的前缀节点
struct Node * nq = create(-, q);
for (int i = ; i < ; ++i) nq->R[i] = false;
nq->mxCnt = p->mxCnt + ;
nq->R[id] = true;
nq->flag = true; //这个点是属于id的。是id的前缀节点
q->fa = nq; //这里是没有np的
q->miCnt = nq->mxCnt + ;
for (; p && p->pNext[x] == q; p = p->fa) p->pNext[x] = nq;
last = nq; //成为接受后缀的节点。
return;
}
struct Node *np = create(p->mxCnt + , NULL);
for (int i = ; i < ; ++i) np->R[i] = false; //每次都要清空
np->R[id] = true;
np->flag = true; //前缀节点
np->pos = pos, last = np; //last是最尾那个可接收后缀字符的点。
for (; p != NULL && p->pNext[x] == NULL; p = p->fa) p->pNext[x] = np;
if (p == NULL) {
np->fa = root;
np->miCnt = ; // 从根节点引一条边过来
return;
}
struct Node *q = p->pNext[x];
if (q->mxCnt == p->mxCnt + ) { //中间没有任何字符,可以用来代替接受后缀、
np->fa = q;
np->miCnt = q->mxCnt + ; // q是状态8的"ab",np是状态7的"bab"长度是2+1
return;
}
struct Node *nq = create(-, q); // 新的q节点,用来代替q,帮助np接收后缀字符
for (int i = ; i < ; ++i) nq->R[i] = false;
nq->mxCnt = p->mxCnt + ; //就是需要这样,这样中间不包含任何字符
q->miCnt = nq->mxCnt + , np->miCnt = nq->mxCnt + ;
q->fa = nq, np->fa = nq; //现在nq是包含了本来q的所有指向信息
for (; p && p->pNext[x] == q; p = p->fa) {
p->pNext[x] = nq;
}
}
void init() {
t = ;
root = last = create(, NULL);
}
char str[maxn];
int dp[maxn * ][];
int d[maxn * ];
queue<int> que;
int in[maxn];
void work() {
init();
scanf("%s", str + );
for (int i = ; str[i]; ++i) addChar(str[i] - 'a', i, );
last = root;
scanf("%s", str + );
for (int i = ; str[i]; ++i) addChar(str[i] - 'a', i, );
for (int i = ; i < t; ++i) {
in[suffixAutomaton[i].fa->id]++;
if (suffixAutomaton[i].flag) {
dp[i][] = suffixAutomaton[i].R[];
dp[i][] = suffixAutomaton[i].R[];
d[i] = suffixAutomaton[i].R[] + suffixAutomaton[i].R[];
}
}
for (int i = ; i < t; ++i) {
if (in[i] == ) que.push(i);
}
while (!que.empty()) {
int cur = que.front();
que.pop();
if (!cur) break;
dp[suffixAutomaton[cur].fa->id][] += dp[cur][];
dp[suffixAutomaton[cur].fa->id][] += dp[cur][];
d[suffixAutomaton[cur].fa->id] += d[cur];
in[suffixAutomaton[cur].fa->id]--;
if (in[suffixAutomaton[cur].fa->id] == ) que.push(suffixAutomaton[cur].fa->id);
}
int ans = inf;
for (int i = ; i < t; ++i) {
assert(d[i] == dp[i][] + dp[i][]);
if (dp[i][] == && dp[i][] == ) {
ans = min(ans, suffixAutomaton[i].miCnt);
}
}
if (ans == inf) ans = -;
printf("%d\n", ans);
} int main() {
#ifdef local
freopen("data.txt", "r", stdin);
// freopen("data.txt", "w", stdout);
#endif
work();
return ;
}

D. Match & Catch 后缀自动机 || 广义后缀自动机的更多相关文章

  1. 字典树(trie树) 后缀树 广义后缀树

    转自:http://www.cnblogs.com/dong008259/archive/2011/11/11/2244900.html (1)字典树(Trie树) Trie是个简单但实用的数据结构, ...

  2. POJ3080 POJ3450Corporate Identity(广义后缀自动机||后缀数组||KMP)

    Beside other services, ACM helps companies to clearly state their “corporate identity”, which includ ...

  3. bzoj3926: [Zjoi2015]诸神眷顾的幻想乡 对[广义后缀自动机]的一些理解

    先说一下对后缀自动机的理解,主要是对构造过程的理解. 构造中,我们已经得到了前L个字符的后缀自动机,现在我们要得到L+1个字符的后缀自动机,什么需要改变呢? 首先,子串$[0,L+1)$对应的状态不存 ...

  4. BZOJ 3926 && ZJOI 2015 诸神眷顾的幻想乡 (广义后缀自动机)

    3926: [Zjoi2015]诸神眷顾的幻想乡 Time Limit: 10 Sec Memory Limit: 512 MB Description 幽香是全幻想乡里最受人欢迎的萌妹子,这天,是幽 ...

  5. BZOJ 3277 串 (广义后缀自动机)

    3277: 串 Time Limit: 10 Sec Memory Limit: 128 MB Submit: 309 Solved: 118 [Submit][Status][Discuss] De ...

  6. BZOJ 3473: 字符串 [广义后缀自动机]

    3473: 字符串 Time Limit: 20 Sec  Memory Limit: 256 MBSubmit: 354  Solved: 160[Submit][Status][Discuss] ...

  7. BZOJ.2780.[SPOJ8093]Sevenk Love Oimaster(广义后缀自动机)

    题目链接 \(Description\) 给定n个模式串,多次询问一个串在多少个模式串中出现过.(字符集为26个小写字母) \(Solution\) 对每个询问串进行匹配最终会达到一个节点,我们需要得 ...

  8. BZOJ.3926.[ZJOI2015]诸神眷顾的幻想乡(广义后缀自动机)

    题目链接 要对多个串同时建立SAM,有两种方法: 1.将所有串拼起来,中间用分隔符隔开,插入字符正常插入即可. 2.在这些串的Trie上建SAM.实际上并不需要建Trie,还是只需要正常插入(因为本来 ...

  9. 【CF666E】Forensic Examination 广义后缀自动机+倍增+线段树合并

    [CF666E]Forensic Examination 题意:给你一个字符串s和一个字符串集合$\{t_i\}$.有q个询问,每次给出$l,r,p_l,p_r$,问$s[p_l,p_r]$在$t_l ...

随机推荐

  1. WordPaster2-正式包布署说明

    1.1. 多平台布署说明 提供信息如下: 1.多平台控件包(根据购买版本提供) 2.控件信息 3.配置方法   WordPaster(x86)Clsid信息 ClsidParser F4B7C0FD- ...

  2. (转)第一次发博客-说说我的B/S开发框架(asp.net mvc + web api + easyui)

    原文地址:http://www.cnblogs.com/xqin/archive/2013/05/29/3105291.html 前言 这些年一直在.net下做企业web系统开发,前前后后经历的不同的 ...

  3. 编写高质量代码改善C#程序的157个建议——建议43:让接口中的泛型参数支持协变

    建议43:让接口中的泛型参数支持协变 除了上一建议中提到的使用泛型参数兼容接口不可变性外,还有一种办法是为接口中的泛型声明加上out关键字来支持协变,如下所示: interface ISalary&l ...

  4. delphi xe6 for android 自带控件LocationSensor优先使用GPS定位的方法

    delphi xe6 for android LocationSensor控件默认是优先使用网络定位,对定位精度要求高的应用我们可以修改原码直接指定GPS定位. 修改方法: 将C:\Program F ...

  5. 使用"*"通配符来选择文件

    Include 方法和IncludeDirectory 方法中的搜索模式中指定的虚拟路径可以接受一个"*"通配符字符作为前缀或后缀,以在最后一个路径段.搜索字符串是大小写不敏感的. ...

  6. FileTracker:error FTK1011编译错误的原因和解决办法

    原因: 今天创建好项目名字完成关了VS,后感觉文件夹名字不太对改了一下,后来程序就调试不了出现FileTracker:error FTK1011编译错误0.0,经过网络查询应该是路径问题 解决方法: ...

  7. WPF 控件库——可拖动选项卡的TabControl

    WPF 控件库系列博文地址: WPF 控件库——仿制Chrome的ColorPicker WPF 控件库——仿制Windows10的进度条 WPF 控件库——轮播控件 WPF 控件库——带有惯性的Sc ...

  8. 怎样检查fragmentation

    可以运行下面SQL语句: SELECT dbschemas.[name] AS 'Schema', dbtables.[name] AS 'Table', dbindexes.[name] AS 'I ...

  9. 转:Linux下同时启动两个Tomcat进行设置

    转: Linux下同时启动两个Tomcat进行设置 解压tar.gz:tar -zxvf apache-tomcat-6.0.41.tar.gz 至相应的路径下,可解压至两个不同的路径或者相同的路径下 ...

  10. Android 开源项目使用指南

    1.日历项目 https://blog.csdn.net/iamchan/article/details/81214498