寒假“kmp,hash”题解

1、P2957 [USACO09OCT]Barn Echoes G

题目描述

The cows enjoy mooing at the barn because their moos echo back, although sometimes not completely. Bessie, ever the excellent

secretary, has been recording the exact wording of the moo as it goes out and returns. She is curious as to just how much overlap there is.

Given two lines of input (letters from the set a..z, total length in the range 1..80), each of which has the wording of a moo on it, determine the greatest number of characters of overlap between one string and the other. A string is an overlap between two other strings if it is a prefix of one string and a suffix of the other string.

By way of example, consider two moos:

moyooyoxyzooo

yzoooqyasdfljkamo

The last part of the first string overlaps 'yzooo' with the first part of the second string. The last part of the second string

overlaps 'mo' with the first part of the first string. The largest overlap is 'yzooo' whose length is 5.

POINTS: 50

奶牛们非常享受在牛栏中哞叫,因为她们可以听到她们哞声的回音。虽然有时候并不能完全听到完整的回音。Bessie曾经是一个出色的秘书,所以她精确地纪录了所有的哞叫声及其回声。她很好奇到底两个声音的重复部份有多长。

输入两个字符串(长度为1到80个字母),表示两个哞叫声。你要确定最长的重复部份的长度。两个字符串的重复部份指的是同时是一个字符串的前缀和另一个字符串的后缀的字符串。

我们通过一个例子来理解题目。考虑下面的两个哞声:

moyooyoxyzooo

yzoooqyasdfljkamo

第一个串的最后的部份"yzooo"跟第二个串的第一部份重复。第二个串的最后的部份"mo"跟第一个串的第一部份重复。所以"yzooo"跟"mo"都是这2个串的重复部份。其中,"yzooo"比较长,所以最长的重复部份的长度就是5。

输入格式

* Lines 1..2: Each line has the text of a moo or its echo

输出格式

* Line 1: A single line with a single integer that is the length of the longest overlap between the front of one string and end of the other.

输入输出样例

输入 #1复制

abcxxxxabcxabcd 
abcdxabcxxxxabcx 

输出 #1复制

11 

说明/提示

'abcxxxxabcx' is a prefix of the first string and a suffix of the second string.

 题解:定义一个函数,传入两个字符串,计算第二个字符串从何处开始能一直与第一个字符串的开头开始往后推一一对应相等,如果能一直相等到第二个字符串的末尾,则直接返回重合部分长度(因为第二个字符串是从左往右开始比较的,那么能完全相等到第二个字符串结束,那么第一个重合部分就是最长的)。在主函数里面调用函数时交换传的数组顺序,调用两边函数,分别可求出第一串字符前缀与第二串字符后缀,第二串字符前缀与第一串字符后缀长度,输出较大的即可。

#include <stdio.h>
int coni(char a[100],char b[100])
{int num,i,j;
 for(j=0;b[j];j++)//遍历第二个数组 
 {i=0,num=0;
  if(a[i]==b[j])
  {while(a[i]==b[j]&&a[i]&&b[j])
   {num++;
    i++;
	j++; 
   }
   if(b[j]==0)//从左往右,找到第一个能完全重合的部分,直接返回 ,因为从左到右所以重合部分最长 
    return num;
  } 
 }
 return 0;//整个序列遍历完都没返回,则无重合部分,返回0 
}
main()
{char s1[100],s2[100];
 int len1,len2;
 scanf("%s",s1);
 scanf("%s",s2);
 len1=coni(s1,s2);//第一串的头,第二串的尾 
 len2=coni(s2,s1);//第二串的头,第一串的尾
 printf("%d",len1>len2?len1:len2);
}

 2、P3375 【模板】KMP字符串匹配

题目描述

给出两个字符串 s1​ 和 s2​,若 s1​ 的区间 [l, r] 子串与 s2​ 完全相同,则称 s2​ 在 s1​ 中出现了,其出现位置为 l。
现在请你求出 s2​ 在 s1​ 中所有出现的位置。

定义一个字符串 s 的 border 为 s 的一个非 s 本身的子串 t,满足 t 既是 s 的前缀,又是 s 的后缀。
对于 s2​,你还需要求出对于其每个前缀 s′ 的最长 border t′ 的长度。

输入格式

第一行为一个字符串,即为 s1​。
第二行为一个字符串,即为 s2​。

输出格式

首先输出若干行,每行一个整数,按从小到大的顺序输出 s2​ 在 s1​ 中出现的位置。
最后一行输出 ∣s2​∣ 个整数,第 i 个整数表示 s2​ 的长度为 i 的前缀的最长 border 长度。

输入输出样例

输入 #1复制

ABABABC
ABA

输出 #1复制

1
3
0 0 1 

说明/提示

样例 1 解释

对于 s_2s2​ 长度为 33 的前缀 ABA,字符串 A 既是其后缀也是其前缀,且是最长的,因此最长 border 长度为 11。

数据规模与约定

本题采用多测试点捆绑测试,共有 3 个子任务

  • Subtask 1(30 points):∣s1​∣≤15,∣s2​∣≤5。
  • Subtask 2(40 points):∣s1​∣≤10^4,∣s2​∣≤10^2。
  • Subtask 3(30 points):无特殊约定。

对于全部的测试点,保证 1≤∣s1​∣,∣s2​∣≤10^6,s1​,s2​ 中均只含大写英文字母。

 题解:标准kmp,先将子串的前缀表计算出来,即依次计算出1~n位字符串,最大的前缀等于后缀的位数,但通常当字符串长度为n时的前缀数不要,而是将整个前缀表向后一位,将第一位填成负数,以便之后kmp查找。在计算前缀表时,可以通过数组前一位的前缀数计算后一位的,后一位的前缀数为多少,只需看前一个前缀数的后一个字符与当前位数的字符是否相等,如果相等则当前位数的前缀数加一,否则就是0或小于前一位前缀数的数。有了前缀表就能通过kmp轻松查找子串在主串中重合的开始下标。

#include <stdio.h>
#include <string.h>
#define N 1000010
char s1[N],s2[N];
int num[N],prenum[N],temp[N],k=0;//三个数组分别是:存完全匹配开始下标,存s2数组前缀表,存s2后移一位的前缀表以便KMP查找 
void getprenum()//获得前缀表 
{int len=0,i=1;
 prenum[0]=0;
 while(i<strlen(s2))
  {if(s2[i]==s2[len])
   {len++;
    prenum[i]=len;
    i++;
	}
    else
	{if(len>0)
	  len=prenum[len-1]; 
	 else
	  {prenum[i]=len;
	   i++;
	   } 
	}	 
   } 
}
void move()//后移前缀表 
{temp[0]=-1;
 for(int i=1;i<strlen(s2);i++)
  temp[i]=prenum[i-1];
}
void kmp_search()
{int i=0,j=0;
 while(i<strlen(s1))
 {if(j==strlen(s2)-1&&s1[i]==s2[j])//找到一组完全匹配 
  {num[k++]=i-j+1;
   j=temp[j];
  }
  if(s1[i]==s2[j])//相等则直接后移 
  {i++;j++;}
  else//不相等 
  {j=temp[j];
   if(j==-1)
   {i++;j++;}
   } 
 }
}
main()
{scanf("%s",s1);
 scanf("%s",s2);
 getprenum();
 move();
 kmp_search();
 for(int i=0;i<k;i++)
  printf("%d\n",num[i]);
 for(int i=0;i<strlen(s2);i++)
  printf("%d ",prenum[i]); 
} 

3、P1102 A-B 数对

题目描述

出题是一件痛苦的事情!

相同的题目看多了也会有审美疲劳,于是我舍弃了大家所熟悉的 A+B Problem,改用 A-B 了哈哈!

好吧,题目是这样的:给出一串数以及一个数字 C,要求计算出所有 A - B = C 的数对的个数(不同位置的数字一样的数对算不同的数对)。

输入格式

输入共两行。

第一行,两个整数 N,C。

第二行,N 个整数,作为要求处理的那串数。

输出格式

一行,表示该串数中包含的满足 A - B = C 的数对的个数。

输入输出样例

输入 #1复制

4 1
1 1 2 3

输出 #1复制

3

说明/提示

对于 75% 的数据,1≤N≤2000。

对于 100% 的数据,1≤N≤2×10^5。

保证所有输入数据绝对值小于 2^30,且 C≥1。

2017/4/29 新添数据两组

 题解:本题如果不是数值太大的话,可以直接用桶排的思路,用一个数组计算每一个数值的个数,但因为本题的数据范围比较大,数组开不了这么大,所以需要用map来映射他们的关系,其实原理还是跟桶排一样。之后就可以把求A-B=C转换为求B+C=A,首先将整个数组从小到大排序,从头开始将每个元素(看做是B)加上C,看A的个数有几个,最后再累加起来即可。

#include <bits/stdc++.h>
using namespace std;
#define N 200010
long long a[N],cnt=0;
map<int,int> b;//映射,以便节省空间 
main(){
	int n,c;
	scanf("%d%d",&n,&c);
	for(int i=0;i<n;i++){
		scanf("%lld",&a[i]);
		b[a[i]]++;//统计每个数的个数 
	}
	sort(a,a+n);
	for(int i=0;i<n;i++){
		cnt+=b[a[i]+c];//将A-B=C转换为B+C=A,然后统计B+C这个数有几个数 ,如果A-B!=C,那么B+C的个数即为0个 
	} 
	printf("%lld",cnt);
}

4、P3370 【模板】字符串哈希

题目描述

如题,给定 N 个字符串(第 i 个字符串长度为 Mi​,字符串内包含数字、大小写字母,大小写敏感),请求出 N 个字符串中共有多少个不同的字符串。

友情提醒:如果真的想好好练习哈希的话,请自觉,否则请右转PJ试炼场:)

输入格式

第一行包含一个整数 N,为字符串的个数。

接下来 N 行每行包含一个字符串,为所提供的字符串。

输出格式

输出包含一行,包含一个整数,为不同的字符串个数。

输入输出样例

输入 #1复制

5
abc
aaaa
abc
abcc
12345

输出 #1复制

4

说明/提示

对于 30% 的数据:N≤10,Mi​≈6,Mmax≤15。

对于 70% 的数据:N≤1000,Mi​≈100,Mmax≤150。

对于 100% 的数据:N≤10000,Mi​≈1000,Mmax≤1500。

样例说明:

样例中第一个字符串(abc)和第三个字符串(abc)是一样的,所以所提供字符串的集合为{aaaa,abc,abcc,12345},故共计4个不同的字符串。

Tip: 感兴趣的话,你们可以先看一看以下三题:

BZOJ3097:http://www.lydsy.com/JudgeOnline/problem.php?id=3097

BZOJ3098:http://www.lydsy.com/JudgeOnline/problem.php?id=3098

BZOJ3099:http://www.lydsy.com/JudgeOnline/problem.php?id=3099

如果你仔细研究过了(或者至少仔细看过AC人数的话),我想你一定会明白字符串哈希的正确姿势的^_^

 题解:对于哈希的题目,无非就是要找到最契合题目的映射方式,本题用的是将字符串转换成较大进制的方法,消耗多一点空间来减少哈希的冲突性。将所有单词都映射成数字存进数组,然后将数组排好序,计算不同的值的个数即可。(因为本题的数值比较大,数组开不了那么空间,所以用不了桶排,不然就直接能用桶排计算个数,无需排序)

#include <bits/stdc++.h>
using namespace std;
#define N 10010
unsigned long long a[N+1];
unsigned long long has(char *s){
	int ans=0;
	for(int i=0;s[i];i++){
		ans=(ans*131+(unsigned long long)s[i])%21237044013013795711+233317;//本题用的将字符串转换成进制 
	}
	return ans;
}
int main(){
	int n,cnt=0;
	char s[10100];
	scanf("%d",&n);
	for(int i=0;i<n;i++){
	 scanf("%s",s);
     a[i]=has(s);//将每个单词转换成数字 
	}
	sort(a,a+n);
	for(int i=0;i<n;i++)
	 if(a[i]!=a[i+1]) cnt++;//因为以排序,所以直接判断前后是否相等再累加即可 
	printf("%d",cnt); 
}

5、P2580 于是他错误的点名开始了

题目背景

XS中学化学竞赛组教练是一个酷爱炉石的人。

他会一边搓炉石一边点名以至于有一天他连续点到了某个同学两次,然后正好被路过的校长发现了然后就是一顿欧拉欧拉欧拉(详情请见已结束比赛 CON900)。

题目描述

这之后校长任命你为特派探员,每天记录他的点名。校长会提供化学竞赛学生的人数和名单,而你需要告诉校长他有没有点错名。(为什么不直接不让他玩炉石。)

输入格式

第一行一个整数 n,表示班上人数。

接下来 n 行,每行一个字符串表示其名字(互不相同,且只含小写字母,长度不超过 50)。

第 n+2 行一个整数 m,表示教练报的名字个数。

接下来 m 行,每行一个字符串表示教练报的名字(只含小写字母,且长度不超过 50)。

输出格式

对于每个教练报的名字,输出一行。

如果该名字正确且是第一次出现,输出 OK,如果该名字错误,输出 WRONG,如果该名字正确但不是第一次出现,输出 REPEAT

输入输出样例

输入 #1复制

5  
a
b
c
ad
acd
3
a
a
e

输出 #1复制

OK
REPEAT
WRONG

说明/提示

  • 对于 40% 的数据,n≤1000,m≤2000。
  • 对于 70% 的数据,n≤10^4,m≤2×10^4。
  • 对于 100% 的数据,n≤10^4,m≤10^5。

 题解:本题可以运用trie树来做,只不过需要对trie稍稍做一点升级即可,那就是在trie树中每个单词的末尾用另外一个数组来进行标记状态,表示已存在该单词或者该单词已被使用。对于每一次输入名字,我们就需要建立一下trie树,对于trie树的创建就是以一个空节点为树根(因为不可能所有名字都是以相同字母开头),然后向下一点一点创建节点存单词的每个字母,如果在trie树中单词的节点已存在则直接找下一个单词,如果不存在则创建一个新节点存下一个字母即可。而搜索跟创建几乎一样,只是在当trie树节点探索为空时不是创建节点而是表示该单词不存在,如果能在trie树中完完全全找到该单词那么就输出“OK”,并把它的状态改变,表示已经点过名。

#include <stdio.h>
#define N 10010
#define M 100010
int t[30][1000010],temp[1000010],id=0,flag1=1,flag2=0;
void insert(char s[100]){ //建造trie树 
	int p=0;
	for(int i=0;s[i];i++){
		int m=s[i]-'a';//将字母映射为数字 
		if(!t[m][p]){ //该节点为空则创建一个新节点 
			t[m][p]=++id;
		}
		p=t[m][p];
	}
	temp[p]=1;//末尾标记该单词已存在 
}
void search(char s[100]){ //查找 
	int p=0;
	for(int i=0;s[i];i++){
		int m=s[i]-'a';
		if(!t[m][p]){ //该节点为空,则表示不存在该单词 
			flag1=0;
		}
		p=t[m][p];
	}
	if(temp[p]==1){
		flag2=1;
		temp[p]=-1;//标记为已点名 
	}
}
int main(){
    int n,m;
    char a[100];
    scanf("%d",&n);
    while(n--){
	  scanf("%s",a);
      insert(a);
    }
    scanf("%d",&m);
    while(m--){
	  scanf("%s",a);
	  flag1=1;
	  flag2=0;
	  search(a);
	  if(!flag1)
	   printf("WRONG\n");
	  else if(flag2)
	   printf("OK\n");
	  else
	   printf("REPEAT\n");
    }
	return 0; 
}

6、CF1200E Compress Words

题目描述

Amugae has a sentence consisting of nn words. He want to compress this sentence into one word. Amugae doesn't like repetitions, so when he merges two words into one word, he removes the longest prefix of the second word that coincides with a suffix of the first word. For example, he merges "sample" and "please" into "samplease".

Amugae will merge his sentence left to right (i.e. first merge the first two words, then merge the result with the third word and so on). Write a program that prints the compressed word after the merging process ends.

输入格式

The first line contains an integer nn ( 1≤n≤10^5 ), the number of the words in Amugae's sentence.

The second line contains nn words separated by single space. Each words is non-empty and consists of uppercase and lowercase English letters and digits ('A', 'B', ..., 'Z', 'a', 'b', ..., 'z', '0', '1', ..., '9'). The total length of the words does not exceed 10^6106 .

输出格式

In the only line output the compressed word after the merging process ends as described in the problem.

题意翻译

Amugae有n个单词,他想把这个n个单词变成一个句子,具体来说就是从左到右依次把两个单词合并成一个单词.合并两个单词的时候,要找到最大的i(i≥0),满足第一个单词的长度为i的后缀和第二个单词长度为i的前缀相等,然后把第二个单词第ii位以后的部分接到第一个单词后面.输出最后那个单词

输入输出样例

输入 #1复制

5
I want to order pizza

输出 #1复制

Iwantorderpizza

输入 #2复制

5
sample please ease in out

输出 #2复制

sampleaseinout

 题解:本题就是要求出已合并字符串与待合并字符串的最长前后缀(注意并不是单纯的两两单词的最长的前后缀1),因此需要用到KMP,但传统的KMP都是只求一个字符串的前后缀,本题却求的是分别两个字符串的前缀和后缀的最长相等长度,那么我们可以进行一些转变。将已合并的字符串接到新的待合并字符串的后面,把两个字符串合并成了一个,而且也正好按顺序待合并前缀在前面已合并字符串后缀在后面,就正好符合KMP做法。但不只是单纯的直接把整个已合并字符串都放后面,因为它会越来越长,实现KMP的时间复杂度也会变长,其实只需截取已合并字符串和待合并字符串两者中较短的长度,毕竟前后缀都不能超过字符串本身。并且在待合并字符串和已合并字符串之间还要加一个永远不会输入的字符进行阻隔,因为我们是将两个字符串连成一个然后KMP,那么它们的前后缀也不应该跨界,在它们之间加一些其它字符隔开即可。最后在真正合并时跳过待合并字符串的前缀与已合并字符串后缀相等长度的字符就行了。

#include <stdio.h>
#include <string.h>
#define min(a,b) a<b?a:b
#define N 1000010 
char s[N],a[N];//s已合并字符串,a待合并字符串 
int nxt[N],top;
int main(){
	int n;
	scanf("%d",&n);
	scanf("%s",s+1);//第一句可直接存入合并字符串中 
	top=strlen(s+1);
	for(int i=2;i<=n;i++){
	   scanf("%s",a+1);
	   	 int lens=top,lena=strlen(a+1),cnt=lena;//lens已合并长度,lena,cnt新的待合并字符串长度 
	   	 int minlen=min(lens,lena);// 求已合并与待合并字符串中较短的长度 
	   	 a[++cnt]='@';//隔开新字符串和已合并字符串,防止两个字符串跨界匹配前后缀 
	   	 for(int j=1;j<=minlen;j++)
	   	   a[++cnt]=s[lens-minlen+j];//只需将待合并的字符串从后截取较短字符串到新字符串后面,以便组成一个字符串进行KMP 
	   	 nxt[0]=nxt[1]=0;            //截取待合并字符串长度为:待合并字符串和已合并字符串长度较短的长度,因为最大相等前缀与后缀不能超过字符串长度 
	   	 int j=0;
	   	 for(int k=2;k<=cnt;k++){   //查找最长相等前后缀 
	   	 	while(j&&a[k]!=a[j+1]) j=nxt[j];
			if(a[k]==a[j+1]) j++;
			nxt[k]=j;		
			}
		 for(int j=nxt[cnt]+1;j<=lena;j++)//跳过最长前后缀将两个字符串合并 
		   s[++top]=a[j];	 
	}
	puts(s+1);
	return 0;
}

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值