2020寒假集训总结
在寒假不长又不短的7天(饭菜吃着我难受),我学到了很多东西。好记性不如烂笔头,现在我将其写下,以供后来参考。
day1 hash
第一天学了hash,这是一个非常好用的简单数据结构(但又不是数据结构),我现在总结以下几点:
1.hash的原理
hash本质上是将一个字符串(这本来要用一个数组开)转化为一个整数(8bits),可以极大地节省空间。并且由于hash可以将一个串变成整数,自然也有了整体打包的功能。而hash有两种写法(孔乙己???):
①对于时时刻刻要用各个地方的hash:
hash[i]=(hash[i-1]*p%mod+s[i]-'a'+1)%mod;
这里的p和mod是两个大质数,为的是防止重复(准确说是减少重复)。
②对于特殊的hash(这是我的独家秘笈哦)
让我们看这样的一道题
题目描述
给出一个只包含小写字母字符串,要求你将它划分成尽可能多的小块,使得这些小块构成回文串。
例如:对于字符串abcab,将他分成ab c ab或者abcab就是构成回文串的划分方法,abc ab则不是。
输入格式
第一行输入一个整数T表示数据组数。
接下来的T行,每行输入一个字符串,代表你需要处理的字符串,保证该字符串只包含小写字母。
输出格式
输出T行,对于每个输入的字符串,输出一行包含一个整数x,表示该字符串最多能分解成x个小块,使得这些小块构成回文串。
这里可以想到的是整体法,因为这道题是将一整个字符串当成一个整体来做回文。
但是我们可以发现一个性质:当外面的相同后就可以丢去,只关心里面的!!!
所以有两种做法(孔乙己???):
-
用KMP进行多次匹配(前后缀嘛),但是如果数据是 aaaaaaaaaaaaaaaaaaaaaaa……的话,n²的时间复杂度。
-
用hash。
①可以将hash的每个只求出来,然后用子串公式:
hashhh[i][j]=(hash[j]-hash[i-1]*power(p,j-i+1,mod)%mod+mod)%mod;
表示子串i到j的hash值(包含i,j);
②但我们发现了一个特殊的性质:这种题我们只需要将那一个连续串的hash求出来,并且不需要用到已经查询过的hash,于是我们便可以有不回头hash算法(下面贴我的代码,这种方法只是原创,并且特别利用了hash的原理)
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const long long MM=1e6+20;
const long long p=786433;
const long long mod1=1e9+9;
int t;
char txt[MM];
long long power(long long a,long long b,long long mm){
long long ans=1%mm;
for(;b;b>>=1){
if(b&1) ans=ans*a%mm;
a=a*a%mm;
}
return ans;
}
inline int read(){
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
return s*w;
}
void judge(){
int start1=0,start2=strlen(txt)-1,end1=0,end2=strlen(txt)-1;
int one=0,two=0,ans=0;
while(end1<start2){
one=(one*p)%mod1+txt[end1]-'a'+1;
two=(two+((txt[start2]-'a'+1)*power(p,end2-start2,mod1))%mod1+mod1)%mod1;
if(one==two){
end1++;start1=end1;
one=two=0;
start2--;end2=start2;
ans+=2;
}
else end1++,start2--;
}
long long l=strlen(txt);
if(end1!=start1) if(l%2==0)ans++;
if(l%2!=0) ans++;
cout<<ans<<endl;
}
int main(){
t=read();
while(t--){
scanf("%s",txt);
judge();
}
return 0;
}
2.hash的注意事项
记得开long long!!!!!!!(血的教训)
时时刻刻记得mod模一下,不要怕时间复杂度过高
day2 KMP
KMP不难,主要是next后缀数组的运用与理解,现在我贴出代码并进行解释
void pre_next(){
int s1=0,s2=-1;
next[0]=-1;
while(s1<lenth){
if(s2==-1||b[s1]==b[s2]) next[++s1]=++s2;
else s2=next[s2];
}
}
解释的话,请看这篇博客吧
循环节相关的知识的话……
看这篇博客吧(自己网上搜)
然后附上几道题
理解kmp算法:poj2752+ poj2406+ poj1961+
常规kmp算法练习:poj3461+ poj2185(很妙,值得再看)
day3 manacher
没啥说的,注意一下代码细节就对了
#include<cstdio>
#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
int manacher(string s){
string pre="$#";
for(int i=0;i<s.size();i++){
pre+=s[i];pre+="#";
}
vector<int> p(pre.size(),0);
int mid=0,rightmax=0,lenmax=0;
for(int i=1;i<pre.size();i++){
if(rightmax>i) p[i]=min(p[2*mid-i],rightmax-i);
else p[i]=1;
while(pre[i+p[i]]==pre[i-p[i]]) p[i]++;
if(i+p[i]>rightmax) rightmax=i+p[i],mid=i;
if(p[i]>lenmax) lenmax=p[i];
}
return lenmax-1;
}
int main(){
string a;
cin>>a;
cout<<manacher(a)<<endl;
return 0;
}
//luogu3805
day4 trie
原理
trie的原理实际上就是建一棵树,将枝干化为字母,点化为另一些代号。
代码
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int MM=1000000+20;
int n,m,map[MM][29],gg=1,tim[MM];
bool judge[MM],wrong,repeat;
char a[55];
void insert(char *s){
int len=strlen(s);
int fa=1;
for(int i=0;i<len;i++){
int son=s[i]-'a';
if(!map[fa][son]) map[fa][son]=++gg;
fa=map[fa][son];
}
judge[fa]=true;
}
void search(char *s){
int len=strlen(s);
int fa=1;
for(int i=0;i<len;i++){
int son=s[i]-'a';
if(!map[fa][son]){
wrong=true;
return ;
}
fa=map[fa][son];
}
if(!judge[fa]){
wrong=true;
return ;
}
else
if(!tim[fa]) tim[fa]++;
else{
repeat=true;
return ;
}
}
int main(){
cin>>n;
while(n--){
cin>>a;
insert(a);
}
cin>>m;
while(m--){
cin>>a;
wrong=repeat=false;
search(a);
if(wrong){
cout<<"WRONG"<<endl;
}
else
if(repeat){
cout<<"REPEAT"<<endl;
}
else cout<<"OK"<<endl;
}
}
//luogu2580
//主要看insert部分(建树)和search(查询)部分
野鸡
trie可以来将重边合并并且统计,看这道题:
题目描述
给定一颗n个节点的无根树,每条边上附有一个小写英文字母。
于是一条路径对应一个字符串。
一共有q次询问,每次询问以节点u为起点的非空字符串中有多少字典序严格小于字符串
u—>v。
输入格式
第一行,两个个整数n,q。
接下来n-1行,每行两个整数,一个小写字母u,v,c。 表示存在字母为c的树边(u,v)。保证u≠v。
接下来q行,每行两个整数u,v。
输出格式
q行,每行一个答案。
附上代码(每次写到后面我就变懒了):
#include <bits/stdc++.h>
using namespace std;
int n, q;
int link[4010][26], cnt[4010], pos[4010], now;
int tmp[4010], key[4010][4010], num;
char s[2];
int head[4010];
struct node {
int to, next, val;
} l[8010];
void dfs(int s, int from) {//from==fa
for (int i = head[s]; i != -1; i = l[i].next) {
if (l[i].to == from)
continue;//防止往回走
if (!link[pos[s]][l[i].val])
link[pos[s]][l[i].val] = pos[l[i].to] = ++now;
else
pos[l[i].to] = link[pos[s]][l[i].val];//直到这里,仍是都只是普通的trie建树
cnt[pos[l[i].to]]++;//求的是去到
dfs(l[i].to, s);
}
}//dfs本质上是通过trie建树来将重边打包合并
void solve(int s) {
for (int i = 0; i < 26; i++)
if (link[s][i])
tmp[link[s][i]] = num, num += cnt[link[s][i]], solve(link[s][i]);
}
int main() {
scanf("%d%d", &n, &q);//输入
memset(head, -1, sizeof head);//用前向星存的边
for (int i = 0, one, two; i < n - 1; i++) {
scanf("%d%d%s", &one, &two, s);
one--, two--;//说明将编号改为0~n-1;
l[2 * i].to = two, l[2 * i].next = head[one], l[2 * i].val = s[0] - 'a', head[one] = 2 * i;
l[2 * i + 1].to = one, l[2 * i + 1].next = head[two], l[2 * i + 1].val = s[0] - 'a',
head[two] = 2 * i + 1;//普通的前向星 (双向)
}
for (int i = 0; i < n; i++) {
memset(link, 0, sizeof link);
memset(cnt, 0, sizeof cnt);
memset(pos, 0, sizeof pos);
now = 0;
dfs(i, -1);
memset(tmp, 0, sizeof tmp);
num = 0;
solve(0);
for (int j = 0; j < n; j++) key[i][j] = tmp[pos[j]];
}
int u, v;
while (q--) {
scanf("%d%d", &u, &v);
u--, v--;
printf("%d\n", key[u][v]);
}
return 0;
}
day5 AC自动机
说实在话,我现在都忘了它是什么了。
(大脑回顾中)
(大脑回顾中)
(大脑回顾中)
(大脑回顾中)
(大脑回顾中)
想到了:
给定n个模式串和1个文本串,求有多少个模式串在文本串里出现过。
这个,还是直接看代码吧,只要是之前trie的建树&fail数组的查询
#include<cstdio>
#include<iostream>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
const int MM=2*1e6+20;
int n,pre[MM],road[MM][29],gg;
char a[MM];
int fail[MM];
void trie(char *s){
int lena=strlen(a),fa=0;
for(int i=0;i<lena;i++){
int son=s[i]-'a'+1;
if(!road[fa][son]) road[fa][son]=++gg;
fa=road[fa][son];
}
pre[fa]++;
}
queue<int> q;
void make_fail(){
for(int i=1;i<=26;i++) if(road[0][i]) fail[road[0][i]]=0,q.push(road[0][i]);
while(!q.empty()){
int fa=q.front();
q.pop();
for(int i=1;i<=26;i++){
if(road[fa][i]) {fail[road[fa][i]]=road[fail[fa]][i];q.push(road[fa][i]);}
else road[fa][i]=road[fail[fa]][i];
}
}
}
int quary(char *s){
int now=0,ans=0;
int l=strlen(s);
for(int i=0;i<l;i++){
int son=s[i]-'a'+1;
now=road[now][son];
for(int j=now;j&&pre[j]!=-1;j=fail[j]){
ans+=pre[j];
pre[j]=-1;
}
}
return ans;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a;
trie(a);
}
make_fail();
cin>>a;
cout<<quary(a)<<endl;
}
然后luogu有三个模板,最后一个可以看看,优化好像是那个(啥)
还是直接附代码吧,topo排序我刚刚也没转过神来。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int MM=2*1e5+20;
int n,trie[MM][30],gg;
char a[2000000+20];
int kind[MM],kind2[MM],in[MM],ans[MM],vis[MM],fail[MM];
queue<int> q;
void insert(char *a,int kid){
int lena=strlen(a),fa=0;
for(int i=0;i<lena;i++){
int son=a[i]-'a'+1;
if(!trie[fa][son]) trie[fa][son]=++gg;
fa=trie[fa][son];
}
if(!kind[fa]) kind[fa]=kid;
kind2[kid]=kind[fa];
}
void make_fail(){
for(int i=1;i<=26;i++) if(trie[0][i]) q.push(trie[0][i]);
while(!q.empty()){
int fa=q.front();q.pop();
for(int i=1;i<=26;i++){
if(trie[fa][i]){fail[trie[fa][i]]=trie[fail[fa]][i];q.push(trie[fa][i]);in[fail[trie[fa][i]]]++;}
else trie[fa][i]=trie[fail[fa]][i];//并未满足条件,∴不需要++in[]
}
}
}
void quary(char *s){
int lens=strlen(s),now=0;
for(int i=0;i<lens;i++){
now=trie[now][s[i]-'a'+1];
ans[now]++;
}
}
void topsort(){
for(int i=1;i<=gg;i++) if(!in[i]) q.push(i);
while(!q.empty()){
int fa=q.front();
q.pop();
vis[kind[fa]]=ans[fa];
int grandfa=fail[fa];
ans[grandfa]+=ans[fa];
in[grandfa]--;
if(!in[grandfa]) q.push(grandfa);
}
for(int i=1;i<=n;i++){
cout<<vis[kind2[i]]<<endl;
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a;
insert(a,i);
}
make_fail();
cin>>a;
quary(a);
topsort();
}
day6 考试
day7 我写下这篇博客
柒星完成博客啦
附
我没有写扩展KMP,BM,sunday的一系列算法和尺取法的一些运用,一部分是它们并不难,可以说是简单,二是在今后的算法竞赛中(至少csp中)我还不大可能用到它们,但也算是掌握了一些。之后再看吧
made by starseven