题目是这样:
试题 算法提高 着急的WYF(不同子串个数)
提交此题
资源限制
时间限制:476ms 内存限制:256.0MB
问题描述
由于战网的密码是一串乱码,WYF巧妙地忘记了他的密码。(他就是作死,如同自掘坟墓。说到掘坟墓,问题就来了——挖掘机技术究竟哪家强?)他现在非常着急,走投无路,都快飞起来了。他只记得他的密码是某个字符串S的子串。现在问题来了,你要告诉他有多少种可能的密码,以帮助他确定他能在多少时间内完成枚举并尝试密码的工作。
输入格式
输入仅包含一行,为字符串S,不含空格。
输出格式
输出一个整数,表示可能的密码数量。
样例输入
ToTal
样例输出
14
数据规模和约定
对于70%的数据,S的长度不超过1000;(暴力)
对于100%的数据,S的长度不超过15000。(Suffix Array)
翻译成人话就是:求一个字符串,它的不重复子串个数。
写在前面:
推荐拥有后缀数组和桶排法技能的人进行观看!!开个玩笑,本题解法使用了后缀数组的改装,所以需要对后缀数组有一定的了解。讲后缀数组的网上有很多,而且我觉得已经写得很好了,这里就不过多的进行阐述了。
这题目乍一看好难下手,暴力求解妥妥的超时。那我们把这个题目,大题化小,小题化了…
核心思想:
依次计算出,长度为1,2,3,…的子串不重复数。比如:计算长度为4的子串数,此时已知长度为3的子串rank排名,把长度3子串左边一个字符当‘十位’,长度3子串当‘个位’,桶排一下。最后比较一下相邻排名是不是重复的就行了。
ps.看懂了这段的就直接跳到代码吧(表达的不是很好,估计都看不懂吧)。
解题思路:
首先,把不同长度的子串拆开了算,最后加一起就行了。这样,我们考虑的就是一堆长度固定的字符串,求不重复的有几个。
嗯,还是挺麻烦的,开个挂先:假如我把第一步的字符串从大到小标记好了(按字典序标记,最小的标记为1,重复的标记数字相同),但是我又在每个子串的前面加了一个随机的字符,问:现在有几个不重复的字符串。
这就简单了,桶排一下就好了,新加的作为左边,原来的作为右边(已排序)。用左边的字符划分档次,原顺序不变的填入响应的档次里,排好了,重复的合并一下就OK了。
事实上,我们的假设:把第一步的字符串从大到小标记好了。是可以成立的,因为一开始是计算单个字符的rank排名。
举个例子:
字符串:aabab
长度为1子串有5个:a,a,b,a,b 给个默认排名:1,2,3,4,5 进行一波桶排,第一个字符为’a’的为第一档次,排名序号为:1-3 ‘b’:4-5 同一档次,根据默认排名的先后,区分先后。
初步排名更新为:1,2,4,3,5 (这个排名留着等下还有用)
然后进行精确排名:从第2名开始,比较自己与前一名子串的第一个字符和剩余字符是否都相同(这里只有1个字符,就只比较这1个字符),若都相同则并列名次(废话,大家都是100分,当然并列第一名喽)。2与1比较,大家都是’a’;3与2比较,大家都是’a’;5与4比较,大家都是’b’。所以精确排名更新为:1,1,2,1,2
叮!总子串数+2
然后是长度为2:a-a,a-b,b-a,a-b 很明显,取长度1子串后面4个,前面加上1个字符。它们通过第一步是有初始排名的(同理 第一步的默认排名),这里取第一步的初步排名:2,4,3,5 哪里断了,就后面的都前进1 -->1,3,2,4 进行一波桶排,'a’档次有三个,占掉前三:1-3 ‘b’:4 同一档次,根据初始排名的先后,区分先后。
初步排名更新为:1,2,4,3
精确排名:(比较相邻排名,左部和右部是否都相同)1,2,3,2
叮!总子串+3
长度为3:a-ab,a-ba,b-ab 初始排名:2,4,3->1,3,2
初步排名:1,2,3
精确排名:1,2,3
叮!总子串+3
叮!!未发现并列,执行清算(如果某长度不存在重复,那么在前面加字符还是不重复的)总子串+2+1.
再来一个例子:ababac
长度1:
默认排序:1,2,3,4,5,6
初步排序:1,4,2,5,3,6
精确排序:1,2,1,2,1,3 叮+3
长度2:
初始排序:4,2,5,3,6->3,1,4,2,5 取上一步初步排序的后n-1个
初步排序:1,4,2,5,3
精确排序:1,3,1,3,2 叮+3
长度3:
初始排序:4,2,5,3->3,1,4,2
初步排序:1,3,2,4
精确排序:1,2,1,3 叮+3
长度4:
初始排序:3,2,4->2,1,3
初步排序:1,3,2
精确排序:1,3,2 叮+3
叮叮+2+1(长度为5,6的不用算了,肯定没有重复的)
代码:
//求不重复子串数
#include<bits/stdc++.h>
using namespace std;
const int Max = 1e6 + 5;
char s[Max];
int m, n; //m是种类数量,n是字符串长度
long long Sum = 0; //不重复子串数
int rak[Max], tp[Max], sa[Max], tax[Max];
//rak[] i是位置 rak[i]表示第i个位置的排名是多少
//sa[] i是排名 sa[i]表示第i名的位置是多少
void pp(int tt[], int nn = n) //输出数组,调试的时候用的
{
for (int i = 0; i <= nn; i++)
{
cout << tt[i] << " ";
}
cout << endl;
}
void finallyAdd(int x) //当一种长度的子串不存在重复的,那么它后面的也肯定不会重复
{
//cout << "触发了finallyadd,当前加" << x << endl;
for (int i = x; i > 0; i--)
Sum += i;
}
void sort2(int a[], int b[]) //a[]未使用
{
for (int i = 0; i <= m; i++) //初始化桶
tax[i] = 0;
for (int i = 1; i <= n; i++) //计算每个桶的元素个数
tax[s[i]]++;
for (int i = 1; i <= m; i++) //累加出排名区间
tax[i] += tax[i - 1];
for (int i = n; i > 0; i--) //b[]是初始排名->计算出 sa[]初步排名
sa[tax[s[b[i]]]--] = b[i];
//b[] i是排名 存放位置
//s[] i是位置 存放值
//tax[] i是值 存放i所在档次的排名
//sa[]i是排名 存放位置
}
bool comp(int r[], int a, int b, int k = 1) //若左部和右部都相同,精确排名不增加
{
if (r[a + k] == r[b + k] && s[a] == s[b])
{
//pp(r, n + 1);
//cout <<"发生了同排名,a b分别为:"<< b << " " << a << endl;
}
return r[a + k] == r[b + k] && s[a] == s[b]; //r + s可以判断是否相同。
}
void get_sa(int a[], int b[])
{
//b[] 初始排名
//sa[] 初步排名
//a[] 精确排名
for (int i = 1; i <= n; i++)
m = max(m, a[i + 1] = s[i]), b[i] = i; //第一遍,默认排名为先后顺序
for (int p, j = 1; n > 0; j++, n--) //j为当前检索子串位数,p指示b[]的长度.交替更新b[]旧排名的小更新
{
p = 0;
for (int i = 1; i <= n + 1; i++)
if (j != 1 && sa[i] > 1)
b[++p] = sa[i] - 1; //只选取后n-1个的重组排名
sort2(a, b); //更新了b[] sa[]
//cout<<"初始排名:(排名1~6的位置)";pp(b, n);
//cout<<"初步排名:(排名1~6的位置)";pp(sa, n);
swap(a, b); //复制a[],因为a[]里面的上一轮数据还有用 别用int *t=a 或b=a。
//地址引用会一起被改变,最好自己for复制或swap
a[sa[1]] = p = 1; //开始更新a[],从这里开始b[]就没用了,让b[]存放a[]的复制
for (int i = 2; i <= n; i++)
a[sa[i]] = comp(b, sa[i], sa[i - 1]) ? p : ++p; //排名第2的和第1的子串是不是一样?
Sum += p; //叮!+p
//cout << "精确排名:(位置1~6的排名)"; pp(a); cout << endl << "---------" << endl<<endl;
if (p == n)
{
finallyAdd(n - 1);
break;
}
}
}
int main()
{
scanf("%s", s + 1);
n = strlen(s + 1);
get_sa(rak, tp);
printf("%lld", Sum);
return 0;
}
运行结果截图: