求不重复子串数

题目是这样:

试题 算法提高 着急的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;
}
运行结果截图:

在这里插入图片描述
在这里插入图片描述

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值