习题课4-2
回文串(二分len数组)
-
比较正反hash值
-
线性时间计算出hash值?
-
hash值是可以利用后缀和计算的,因此我们可以在经过O(n)时间的预处理后,O(1)的时间求一个串的hash值。
-
h a s h ( { a x . . y } = ( ∑ i = x n a i B i − x − B y − x + 1 ∑ i = y + 1 n a i B i − y − 1 ) m o d m o hash(\{a_{x..y}\} = (\sum_{i=x}^na_iB^{i-x}-B^{y-x+1}\sum_{i=y+1}^na_iB^{i-y-1})\space mod\space mo hash({ ax..y}=(i=x∑naiBi−x−By−x+1i=y+1∑naiBi−y−1) mod mo
-
因此可以在每个位置i上,二分出len(i)数组的值
-
时间复杂度O(nlogn)
-
解法:先用mnanacher算法O(n)处理出 s1串的最长回文子串长度 L,那么最后的答案肯定是L,L-2,L-4 ~ 0,并且我们已经求出了 p[i] 数组(以i为中心的最长回文半径),用于后面二分答案的判断。
这里有个坑点:二分时奇偶回文要分开二分。因为如何有2k+2长度的公共回文。肯定也会有2k长度的公共回文,但是不保证也有2k+1的长度的公共回文。。. 如果不想奇偶分开讨论的话,可以用马拉车的技巧,转化为全部都是奇回文。
-
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> #include <queue> #include <vector> #include <map> #include <set> #include <stack> #include <string> #include <bits/stdc++.h> using namespace std; typedef unsigned long long ull; #define lson l,mid,rt<<1 #define rson mid+1,r,rt<<1|1 #define mem(a,b) memset(a,b,sizeof(a)) #define lowbit(x) x&-x const int maxn = 2e6 + 5; const int mod = 1e9 + 7; const double eps = 1e-6; const double pi = acos(-1.0); char s[maxn<<1],ss[maxn<<1],s1[maxn],s2[maxn],str[maxn<<1]; int p[maxn<<1]; ull hash1[maxn<<1],hash2[maxn<<1],po[maxn<<1]; int len1,len2; ull geth1(int l,int r){ if(l == 0) return hash1[r]; return (ull)(hash1[r]-hash1[l-1]*po[r-l+1]); } ull geth2(int l,int r){ if(l == 0) return hash2[r]; return (ull)(hash2[r]-hash2[l-1]*po[r-l+1]); } int manacher(int len){ int id = 0,mx = 0; int res = 0; for(int i = 2; i < len; i++){ p[i] = mx>i?min(p[2*id-i],mx-i):1; while(str[i+p[i]] == str[i-p[i]]) p[i]++; if(i+p[i] > mx){ mx = i+p[i]; id = i; } res = max(res,p[i]); //printf("%d ",p[i]); } //cout<<res-1<<endl; //cout<<endl; return res-1; } map<ull,int>mp; bool judge(int len,int sz){ mp.clear(); for(int i = 2; i+len-1 < sz; i+=2){ //枚举回文中心 if(p[i]-1>=len){ //以i为中心的回文子串长度大于二分的长度 int ra = len/2; int id = i/2-1; //对应回原串回文中心 mp[geth1(id-ra,id+ra)] = 1; //cout<<1; } } for(int i = 0; i+len-1 < len2; i++){ if(mp.count(geth2(i,i+len-1))) return true; } return false; } int main(){ //freopen("in.txt","r",stdin); //freopen("out.txt","w",stdout); po[0] = 1; for(int i = 1; i < maxn*2; i++){ po[i] = po[i-1]*31; } while(~scanf("%s%s",s1,s2)){ /*预处理将字符串中的回文串全部变成奇回文*/ len1 = len2 = 0; s[len1++] = 'A'; for(int i = 0; s1[i]!='\0'; i++){ s[len1++] = s1[i]; s[len1++] = 'A'; } ss[len2++] = 'A'; for(int i = 0; s2[i]!='\0'; i++){ ss[len2++] = s2[i]; ss[len2++] = 'A'; } /*...........................*/ /*manacher的预处理*/ int tot = 0; str[tot++] = '$'; str[tot++] = '#'; for(int i = 0; i < len1; i++){ str[tot++] = s[i]; str[tot++] = '#'; } str[tot] = '\0'; /*...................*/ /*hash处理*/ hash1[0] = s[0]; for(int i = 1; i < len1; i++) hash1[i] = hash1[i-1]*31+s[i]; hash2[0] = ss[0]; for(int i = 1; i < len2; i++) hash2[i] = hash2[i-1]*31+ss[i]; /*..........*/ int l = 0, r = manacher(tot)/2; /*由于之前的奇回文处理,现在的r是s串的(最长回文串长度-1)/2,其实就是原串的最长回文串长度,因为s串加倍处理过*/ int ans = 0; while(l <= r){ int mid = (l+r)>>1; //二分答案长度 if(judge(mid<<1|1,tot)){ l = mid+1; ans = mid*2+1; }else r = mid-1; } printf("%d\n",ans/2); } return 0; }
-
#include<cstdio> #include<iostream> #include<string> #include<vector> #include<algorithm> using namespace std; typedef long long ll; const ll mod=1000000007; const ll p=10000019; const ll maxn=200010; ll powp[maxn],h1[maxn],h2[maxn]; void init(){ powp[0]=1; for(int i=1;i<maxn;i++){ powp[i]=(powp[i-1]*p)%mod; } } void calh(ll h[],string &str){ h[0]=str[0]; for(int i=1;i<str.length();i++){ h[i]=(h[i-1]*p+str[i])%mod; } } int calsinglesubh(ll h[],int i,int j){ if(i==0) return h[j]; return ((h[j]-h[i-1]*powp[j-i+1])%mod+mod)%mod; } int binarysearch(int l,int r,int len,int i,int iseven){ while(l<r){ int mid=(l+r)/2; int h1l=i-mid+iseven,h1r=i; int h2l=len-1-(i+mid),h2r=len-1-(i+iseven);//难! int hashl=calsinglesubh(h1,h1l,h1r); int hashr=calsinglesubh(h2,h2l,h2r); if(hashl!=hashr) r=mid; //说明回文半径≤mid else l=mid+1; } return l-1; } int main(){ init(); string str; getline(cin,str); calh(h1,str); reverse(str.begin(),str.end()); calh(h2,str); int ans=0; //奇回文 for(int i=0;i<str.size();i++){ int maxlen=min(i,(int)str.length()-1-i)+1; int k=binarysearch(0,maxlen,str.length(),i,0); ans=max(ans,k*2+1); } //偶回文 for(int i=0;i<str.length();i++){ int maxlen=min(i+1,(int)str.length()-1-i)+1; int k=binarysearch(0,maxlen,str.length(),i,1); ans=max(ans,k*2); } printf("%d\n",ans); return 0; }
邓老师数
-
求出所有不超过n的质数
-
求出所有不超过n的邓老师数
-
质数是除了1与自身不再有其他因数的正整数
-
邓老师数:合数,正因数除了1与自身外,全都是质数
-
质数用筛法求解
代码解析
-
i从2到n枚举
-
把它的2倍到若干倍但是小于等于n的数标记为合数
-
筛掉质数倍数的数(质数倍数的数不是质数,但是要判断一下)
-
static class Task { // 本函数求解质数或邓老师数(将这两个功能合并在了一起) // n, k:意义均与题目描述相符 // 返回值:如果 k=0,则将所求的质数按从小到大的顺序放入返回值中;如果 k=1,则将所求的邓老师数按从小到大的顺序放入返回值中。 List<Integer> getAnswer(int n, int k) { boolean[] isPrime = new boolean[n+1]; boolean[] isDeng = new boolean[n+1]; Arrays.fill(isPrime,true); Arrays.fill(isDeng,true); List<Integer> ans = new ArrayList<>(); for (int i = 2; i <= n; i++) { // 筛质数 if (isPrime[i]) { isDeng[i] = false; } // 添加质数 if (k==0 && isPrime[i]){ ans.add(i); } // 添加邓老师数 if (k==1 && isDeng[i]){ ans.add(i); } // 每个数的倍数 for (int j = i+i; j <= n; j += i) { isPrime[j] = false; // 筛掉合数的倍数 if (!isPrime[i]) { isDeng[j] = false; } } } return ans; } }
子序列
- aaa的子串是a,aa,aaa,不能重复计算
解法1
- O(2^n)的时间吧所有的子序列求出来,然后去重
解法2(动态规划)
-
递推,在竞赛中也称为动态规划
-
f(i)表示s(i)所拥有的不同子序列个数
-
pre(i)表示字符s(i)在s(i-1)最后出现的位置
-
f ( i ) = { f ( i − 1 ) + f ( i − 1 ) + 1 , p r e ( i ) = 0 f ( i − 1 ) + f ( i − 1 ) − f ( p r e ( i ) − 1 ) , p r e ( i ) ≠ 0 f(i)=\begin{cases} f(i-1)+f(i-1)+1 , pre(i)=0\\ f(i-1)+f(i-1)-f(pre(i)-1), pre(i)\neq0\end{cases} f(i)={ f(i−1)+f(i−1)+1,pre(i)=0f(i−1)+f(i−1)−f(pre(i)−1),pre(i)=0
-
pre(i)=0
-
此时s(i)在前面s(i-1)没有出现过
-
第一个f(i-1):f(i-1)的子序列也是f(i)的子序列
-
第二个f(i-1):将前i-1的子序列后面都加新字符也可以构成f(i-1)个子序列
-
单独的字符
-
-
pre(i)不等于0
-
s(i)在前面s(i-1)中出现过
-
第一个f(i-1)同上
-
第二个f(i-1)同上
-
由于字符已经出现过了,所以第二次加了重复的,减去在pre(i)这个位置以前的字符串子串,加上字符s(i)结尾的子序列
-
-
for (int i = 1; i <= n; i++) { if (p[i]==0){ f[i] = f[i-1] + f[i-1] +1; }else { f[i] = f[i-1] + f[i-1] - f[p[i] - 1] + mo; } } return f[n]%mo;
-
为什么要加一个mo,因为对mo取余后,做减法的时候可能为负
-
static class Task { final int N = 500005, mo = 23333; /* 全局变量 */ // 表示前i个字符形成的本质不同的子序列个数 int[] f = new int[N]; // 表示字符s[i]最后出现的位置 int[] p = new int[N]; // 表示字符i最后出现的位置 int[] last = new int[26]; // 为了减少复制开销,我们直接读入信息到全局变量中 // s:题目所给字符串,下标从1开始 // n:字符串长度 int n; char[] s; // 求出字符串s有多少不同的子序列 // 返回值:s不同子序列的数量,返回值对mo取模 int getAnswer() { for (int i = 1; i <= n; i++) { // 获取0-25的编号 int a = s[i] - 'a'; p[i] = last[a]; // 最后出现的位置 last[a] = i; } for (int i = 1; i <= n; i++) { if (p[i]==0){ f[i] = (f[i-1] + f[i-1] +1)%mo; }else { f[i] = (f[i-1] + f[i-1] - f[p[i] - 1])%mo; } } return f[n]%mo; } }
前缀
- 给定n个字符串,询问m次,每个询问给出一个字符串,求出这个字符串是n个字符串里,多少个串的前缀
- 前缀:从头开始的一段连续子串。比如字符串ab是字符串abcd的前缀,也是字符串ab的前缀,但是不是bab的前缀
解法1
-
将每个字符串的前缀全部拉出来,然后丢进hash表
-
时间复杂度是O(L^2+mk)
-
const int base1 = 233; const int mo1 = 998244353; const int base2 = 31; const int mo2 = 19260817; // set是可重复的 // multiset是不可重复的 multiset<pair<int,int>> Hash; void add(char *s){ int h1 = 0; int h2 = 0; for(;*s!=0;++s){ // 需要从1开始 int a = *s - 'a' +1; h1 = ((long long)h1*base1 +a)%mo1; h2 = ((long long)h2*base2 +a)%mo2; Hash.insert(pair<int,int>(h1,h2); } }
解法2
- trie树
代码解析
-
trie树上的边,c(x)(y)表示从x节点出发(x从1开始),字符为y的边(y从0到25),
-
trie树的根节点是0,是起点,但没有字符的点
-
遍历一个字符时,如果最终停止在终止节点,说明这个字符出现过
-
在trie中添加新字符串
-
int cnt = 0; void add(String s) { int x = 0; for (int i = 0; i < s.length(); i++) { int y = s.charAt(i) - 'a'; if (c[x][y] == 0) { c[x][y] = ++cnt; } x = c[x][y];//将该字符标记为某节点(从1开始) } ++sz[x];//最终终止的节点 }
-
最终是终止节点,所以终止节点的数目+1
-
对于非终止节点,如何计算size
-
void dfs(int x) { for (int y = 0; y < 26; y++) { int z = c[x][y]; if (z!=0){ dfs(z);// 没有终止,继续向下递归 sz[x] += sz[z]; } } }
-
把x的子节点的所有sz都算出来,然后都加到x身上
-
int walk(String s) { int x = 0; for (int i = 0; i < s.length(); i++) { int y = s.charAt(i) - 'a'; if (c[x][y] == 0) { return 0;//不存在该条边,直接返回0 } x = c[x][y]; } return x;//返回最终停止在哪个节点上 }
标程
-
static class Task { final int M = 505, L = 1000005; // c:trie树上的边,c[x][y]表示从节点x出发(x从1开始),字符为y的边(y范围是0到25) // sz:sz[x]表示x节点的子树中终止节点的数量(子树包括x自身) // cnt:trie树上节点的数目 // 通过统计终止节点个数可以知道字符串数目 int[][] c = new int[L][26]; int[] sz = new int[L]; int cnt; // 将字符串s加入到trie树中 // s:所要插入的字符串 void add(String s) { int x = 0; for (int i = 0; i < s.length(); i++) { // 形成0-25 int y = s.charAt(i) - 'a'; // 边不存在,新建一个节点 if (c[x][y] == 0) { c[x][y] = ++cnt; } // 沿着边继续往下走 x = c[x][y]; } // 此时x是终止节点 ++sz[x]; } // 用于计算sz数组 // x:当前节点 // 可以把这一堆去掉,把{ ++sz[x];}这一句上移到for循环里面就可以了 void dfs(int x) { for (int y = 0; y < 26; y++) { int z = c[x][y]; if (z!=0){ dfs(z); sz[x] += sz[z]; } } } // 用字符串s沿着trie树上走,找到相应的节点 // s:所给字符串 // 返回值:走到的节点 int walk(String s) { int x = 0; for (int i = 0; i < s.length(); i++) { int y = s.charAt(i) - 'a'; if (c[x][y] == 0) { return 0; } x = c[x][y]; } return x; } void solve(InputReader in, PrintWriter out) { int n = in.nextInt(), m = in.nextInt(); for (; n-- != 0; ) add(in.next()); dfs(0); sz[0] = 0; for (; m-- != 0; ) out.println(sz[walk(in.next())]); } }