2020牛客暑期多校训练营(第四场) C - Count New String (字符串,广义后缀自动机,序列自动机)
Count New String
题意:
- 定义字符串函数 \(f(S,x,y)(1\le x\le y\le n)\),返回一个长度为y-x+1的字符串,第 i 位是 \(max_{i=x...x+k-1}S_i\)
- 设集合\(A = {f(f(S, x_1,y_1),x_2-x_1+1,y_2-x_1+1)|1\le x_1 \le x_2 \le y_2 \le y_2 \le n}\)
- 求集合A 的大小
- \(N\le 1e5\) 字符集大小 <=10
分析:
先放出官方题解

方法一
核心点1比较容易想到,进一步可以观察到他们之间有很大一部分后面是重复的,感性的想到如果倒着插入Trie树,在Trie树上面的节点可能不会很多。具体证明来讲,当前字符 \(i\), 最近的大于它的字符的位置是 \(j (j > i)\), 那么在将位置 \(i + 1\) 的字符插入到Trie树之后,还要把长度 \((j - i)\) 的字符串插入到Trie树中。考虑一个 \(j_2\),再找一个最大的 \(j_1\) 有 \(S_{j_1} \ge S_{j_2}\),那么\([j_1,j_2]\)这个区间最多利用10次,所以Trie树节点不超过10N。然后就是常规的在Trie树上面建立广义后缀自动机,扫一遍所有节点即可得到本质不同的子串个数
#include<bits/stdc++.h>
//#define ONLINE_JUDGE
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
#define dbg(x...) do { cout << "\033[32;1m" << #x <<" -> "; err(x); } while (0)
void err() { cout << "\033[39;0m" << endl; }
template<class T, class... Ts> void err(const T& arg,const Ts&... args) { cout << arg << " "; err(args...); }
const int N = 2000000 + 5; //N为字符串长度两倍
const int P = 10;
char s[N];
struct node{
int link, len, trans[P];
void clear(){
memset(trans,0, sizeof trans);
link = len = 0;
}
};
struct SAM{
node S[N];
int p, np, size;
int b[N], c[N];
SAM():p(1),np(1),size(1){}
void clear(){
for(int i=0;i<=size;i++)S[i].clear();
np = size = p = 1;
}
void insert(char ch){
int x = ch - 'a';
np = ++size;
S[np].len = S[p].len + 1;
while(p != 0 && !S[p].trans[x]) S[p].trans[x] = np, p = S[p].link;
if(p == 0)S[np].link = 1;
else{
int q, nq;
q = S[p].trans[x];
if(S[q].len == S[p].len + 1) S[np].link = q;
else{
nq = ++size;
S[nq] = S[q];
S[nq].len = S[p].len + 1;
S[np].link = S[q].link = nq;
while(p != 0 && S[p].trans[x] == q) S[p].trans[x] = nq, p = S[p].link;
}
}
p = np;
}
}sam;
int pos[N];
int n;
int main(){
#ifndef ONLINE_JUDGE
freopen("i.in","r",stdin);
// freopen("o.out,"w",stdout);
#endif
scanf("%s", s+1);
n = strlen(s+1);
stack<int> st;
pos[n+1] = 1;
st.push(n+1);
for(int i=n;i>=1;i--){
while(st.size() != 1 && s[st.top()] < s[i]) st.pop();
int k = st.top();
sam.p = pos[k];
for(int j=i;j<k;j++) sam.insert(s[i]);
pos[i] = sam.p;
st.push(i);
}
ll res = 0;
for(int i = 2;i<=sam.size;i++){
res += sam.S[i].len - sam.S[sam.S[i].link].len;
}
printf("%lld\n", res);
return 0;
}
方法二
然后讲解一下场上自己想出来的一个做法:
观察到\(f(S, x, y)\),最多只会有 10 段连续的一样的字符。采取这样的键值对表示法:从序列\(abcdefghij\)中抽连续的某一个部分,比如aabbc可以用\(\{"abc", [2, 2, 1]\}\) 来表示, bccd 可以用\(\{"bcd", [1, 2, 1]\}\)来表示。注意到这样的表示中,缩减的字符串最多只会有1024种。每个字符串对应的序列的长度最大是10。
对于一个后缀\(f(S, i, n)\),从中可以提取出来的键值对最多有100个。所以整个字符串,可以提取出来的键值对极限只有1e7,实际上会小很多。
对于键值相同的情况,比如 bccdde 与 bcccde, bccddee,将它们一起处理,"bcde" 对应的值是一个二维数组\([[1, 2, 2, 1], [1, 3, 1, 1], [1, 2, 2, 2]]\)。然后重点思考如何处理这个二位数组,因为我们利用"bcde" 去产生子串,具体的选择上面3个串中的某个时,c和d字符的个数是确定的,b和e的个数可以控制。
然后进一步观察,bccdde与bccddee是同一类,因为其中c和d的数量是一样的,而bcccde是另外一类。我们要对他们进行归类,也就是对二维数组\([[1, 2, 2, 1], [1, 3, 1, 1], [1, 2, 2, 2]]\)中的一位数组元素归类。
如何归类?通过他们的第2到len-1个元素的值进行归类。这个操作可以用set实现,具体来说,起初他们都属于一个集合,然后遍历第2到len-1个数字,遍历所有集合,然后根据当前这个数字,对集合进行拆分即可。
最后考虑如何计算对答案的贡献即可。比如上面的例子,bccdde与bccddee归为一类之后,只考虑b和e的个数:\(\{[1, 1], [1, 2]\}\), 对答案的贡献就是2。(这里处理起来还有一些细节需要注意)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
#define dbg(x...) do { cout << "\033[32;1m" << #x <<" -> "; err(x); } while (0)
void err() { cout << "\033[39;0m" << endl; }
template<class T, class... Ts> void err(const T& arg,const Ts&... args) { cout << arg << " "; err(args...); }
const int N = 100000 + 5;
int n;
char s[N];
int next_big[N]; // 下个比它大的字符
int next_not_equ[N]; // 下个不等于的位置
map<string, int> mp;
vector<vector<int>> vv[2000];
set<int> st[2000010];
int vis[N];
int totn;
int get(string str){
if(mp.count(str)) return mp[str];
mp[str] = ++totn;
return totn;
}
ll solve(){
ll res = 0;
for(int ii = 1; ii <= totn; ii ++){
vector<vector<int>> &tt = vv[ii];
int len = tt[0].size();
int n = tt.size();
// 一开始所有的都属于一个集合,这个集合里面应该有一个最大值
int set_count = 1;
ll Max = 0;
for(int i=0;i<n;i++) st[i].clear();
for(int i = 0; i < n ;i++){
st[0].insert(i);
Max = max(Max, 1ll * tt[i][0]);
}
if(len == 1) {
res += Max;
continue;
}
for(int r = 1; r < len-1; r ++){
// 拆集合了
int new_set_count = set_count;
for(int j = 0; j < set_count; j++){
int num = 0;
vector<int> tmp;
set<int> temp = st[j];
st[j].clear();
for(auto x : temp){
tmp.push_back(tt[x][r]);
if(vis[tt[x][r]] == -1) {
if(num == 0) vis[tt[x][r]] = j;
else {
vis[tt[x][r]] = new_set_count ++;
}
num ++;
}
int index = vis[tt[x][r]];
st[index].insert(x);
}
for(auto x : tmp) {
vis[x] = -1;
}
}
set_count = new_set_count;
}
for(int j = 0; j < set_count; j++){
vector<pair<int,int>> t;
for(auto x : st[j]){
t.push_back({tt[x][0], tt[x][len-1]});
}
sort(t.begin(), t.end());
int rmax = t.back().second;
for(int i=t.size()-1;i>=0;i--){
t[i].second = max(t[i].second, rmax);
rmax = t[i].second;
}
for(int i=0;i<t.size();i++){
ll l = t[i].first, r = t[i].second;
if(i != 0 && l == t[i-1].first) continue;
if(i == 0) {
res += l * r;
} else
res += (l - t[i-1].first) * r;
}
}
}
return res;
}
int main(){
#ifndef ONLINE_JUDGE
freopen("i.in","r",stdin);
// freopen("o.out,"w",stdout);
#endif
memset(vis, -1, sizeof vis);
scanf("%s", s+1);
n = strlen(s+1);
int aft[10];
for(int i=0;i<10;i++) aft[i] = n + 1;
for(int i = n; i >= 1; i--){
s[i] = s[i] - 'a';
}
aft[s[n]] = n;
next_big[n] = next_not_equ[n] = n + 1;
for(int i=n-1;i>=1;i--){
next_big[i] = n + 1;
for(int j=s[i]+1;j<10;j++){
if(aft[j] != n + 1) {
next_big[i] = min(next_big[i], aft[j]); // 要找最近的
}
}
aft[s[i]] = i;
if(s[i] == s[i+1]) next_not_equ[i] = next_not_equ[i+1];
else next_not_equ[i] = i + 1;
}
int l = 1;
while(l <= n){
string str = "";
int r = l, pre = 0;
vector<int> v;
do {
pre = r;
r = next_big[r];
str += char('0' + s[pre]);
v.push_back(r - pre);
int id = get(str);
vv[id].push_back(v);
}while(r <= n);
l = next_not_equ[l];
}
printf("%lld\n", solve());
return 0;
}
2020牛客暑期多校训练营(第四场) C - Count New String (字符串,广义后缀自动机,序列自动机)的更多相关文章
- 2020牛客暑期多校训练营(第一场)H Minimum-cost Flow
Minimum-cost Flow 题目:给n个点,m条边.接下来m行包含(a,b,c),即a,b之间有单位流量代价为c的边.接下来有q个问题,每个问题给定(x,y),即假设每条边的容量为x/y时,从 ...
- 2020牛客暑期多校训练营(第二场) Boundary
传送门:Boundary 题意:给你n个点的坐标,问最多有多少个点可以在同一个圆上,(0,0)必须在这个圆上. 题解:三个点确定一个圆,所以暴力枚举两个点和(0,0)组成的圆,如果三个点不共线的话, ...
- 2020牛客暑期多校训练营(第一场)Easy Integration
传送门:J. Easy Integration 题意:给你n,求这个积分,最后的结果分子是记为p,分母记为q. 求(p*q-1)mod 998244353. 题解:比赛完看到巨巨说这是贝塔函数,我一搜 ...
- 2020牛客暑期多校训练营 (第二场) All with Pairs
传送门:All with Pairs 题意:给你n个字符串,求出,f(si,sj)的意思是字符串 si 的前缀和字符串 sj 后缀最长相等部分. 题解:先对所有的字符串后缀hash,用map记录每个h ...
- 2020牛客暑期多校训练营(第二场)Fake Maxpooling
传送门:Fake Maxpooling 题意:给出矩阵的行数n和列数m,矩阵 Aij = lcm( i , j ) ,求每个大小为k*k的子矩阵的最大值的和. 题解:如果暴力求解肯定会t,所以要智取 ...
- 2020牛客暑期多校训练营(第二场) F.Fake Maxpooling (单调队列)
题意:有一个\(n\)x\(m\)的矩阵,\(A_{i,j}=lcm(i,j)\),对于每个\(k\)x\(k\)的子矩阵,其最大元素贡献给答案,求答案的最大值. 题解:矩阵构成我们直接\(i*j/g ...
- 2020牛客暑期多校训练营(第四场)BCFH
BCFH B. Basic God Problem 题意 给出c和n,求fc(n). 题解 递归到最后 fc 函数肯定等于1,那么就变成了求c被乘了几次,只要找到 x 最多能被分解成多少个数相乘就好了 ...
- 2019牛客暑期多校训练营(第九场)A:Power of Fibonacci(斐波拉契幂次和)
题意:求Σfi^m%p. zoj上p是1e9+7,牛客是1e9: 对于这两个,分别有不同的做法. 前者利用公式,公式里面有sqrt(5),我们只需要二次剩余求即可. 后者mod=1e9,5才 ...
- 2019牛客暑期多校训练营(第一场)A题【单调栈】(补题)
链接:https://ac.nowcoder.com/acm/contest/881/A来源:牛客网 题目描述 Two arrays u and v each with m distinct elem ...
- 2019牛客暑期多校训练营(第一场) B Integration (数学)
链接:https://ac.nowcoder.com/acm/contest/881/B 来源:牛客网 Integration 时间限制:C/C++ 2秒,其他语言4秒 空间限制:C/C++ 5242 ...
随机推荐
- Mirai qq机器人 c++版sdk(即用c++写mirai)
Mirai机器人c++版 前言 类似教程 本文git,gitee地址 c++开发mirai 原理 大概流程 实现 如何使用 注意事项 常见错误 前言 改分支版本以及过时,暂时不再维护 请看最新版kot ...
- CSS卡片旋转
html{ perspective: 800px; } body{ display:flex; flex-wrap: wrap; } .card{ transform-style: preserve- ...
- python学习笔记 | wordcloud安装指南
问题: 直接在命令行输入: pip install wordcloud 不出意外,直接报错,显示缺失vc*****.bat,意思是缺失vc版本,这个安装方式基本可以扔掉. 解决: http://t.c ...
- dig的安装和使用
-bash: dig: command not found 解决办法: yum -y install bind-utils dig www.baid bu.com 查看a记录 dig www.ba ...
- PHP反序列化 - Pikachu
概述 序列化serialize()序列化说通俗点就是把一个对象变成可以传输的字符串,比如下面是一个对象: class S{ public $test="pikachu"; } $s ...
- oracle常用hint添加
1.视图添加索引 /* Formatted on 2020/1/6 下午 04:46:37 (QP5 v5.163.1008.3004) */ SELECT /*+index(VIEW_NAME.TA ...
- merge join pg伪代码
Join { get initial outer and inner tuples INITIALIZE do forever { while (outer != inner) { SKIP_TEST ...
- 入门OJ:photo
题目描述 有N个人,来自K个家族.他们排成一行准备照相,但是由于天生的排外性,每个人都希望和本家族的人站在一起,中间不要加入别的家族的人.问最少从队列中去掉多少个就可以达到这个目的. 输入格式 第一行 ...
- 基于scrapy框架的分布式爬虫
分布式 概念:可以使用多台电脑组件一个分布式机群,让其执行同一组程序,对同一组网络资源进行联合爬取. 原生的scrapy是无法实现分布式 调度器无法被共享 管道无法被共享 基于 scrapy+redi ...
- tail -f 在对文件进行动态追踪时失效的问题
在我是用 tail -f file.txt 对这个文件进行动态追踪时: 我重新打开一个新的终端进行vim编辑这个文件并且保存 这是我们发现,tail -f file.txt'动态追踪的这个文件没有任何 ...