【暑假集训模拟DAY6】字符串

前言

之前没写,现在补上

 字符串部分。。。一直感觉好像会一点,但是又非常不熟练

现在我会的字符串算法:

  • Trie
  • KMP
  • maracher
  • Hash

本次做题感觉还是挺好的,除了T3有点慌张没拿到分,其他题都有接近正解的思路

期望得分:100+50+30+70=250

实际得分:20+100+0+30=150

出乎意料的得分

题解

T1 wordlist单词表

思路挺简单,就是查询Trie上出现两次以上的字符串,怎么瞎搞都可以

但是在询问2不能简单的把字符串反着读入,还需要反着比较字典序,反着输出,没考虑到就是100->20

不过实际上我写的复杂度高了,因为是在插入的时候每次都dfs,实际上全插入之后dfs一次就可以了

另一个问题是strcmp运用错了(>0,0,<0) 

看来还是要自己多造几组数据测才保险’

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int INF = 0x3f3f3f3f,N = 5e5+10;
int n,trie[2][N][30],tot[2]={1,1};
char ans[2][N],s[N],tmp[N],tmp2[N];
int ansl[2],anscnt[2],cnt[2];
void solve(int len,int op)
{
	if(!len) return;
	if(ansl[op]<len)//更新长度,重新计算 
	{
		if(op==0) for(int i=1;i<=len;i++) ans[op][i]=s[i];
		else for(int i=1;i<=len;i++) ans[op][i]=s[len-i+1];
		ansl[op]=len,cnt[op]=2;
		anscnt[op]=1;
		return;
	}
	else if(ansl[op]==len)
	{
		if(op==0) for(int i=1;i<=len;i++) tmp[i]=s[i];
		else for(int i=1;i<=len;i++) tmp[i]=s[len-i+1];
		//printf("op=%d,tmp=%s\n",op,tmp+1);
		if(strcmp(tmp+1,ans[op]+1)<0) //字典序变化,重新计算 
		{
			for(int i=1;i<=len;i++) ans[op][i]=tmp[i];
			cnt[op]=2;
			anscnt[op]=1;
		}
		else if(strcmp(tmp+1,ans[op]+1)==0)//更新对数 
		{ 
			cnt[op]++,anscnt[op]=(cnt[op]-1)*cnt[op]/2;
			//printf("anscnt[%d]=%d\n",anscnt[op],op);
		}
		memset(tmp,0,sizeof(tmp));
	}
	//printf("ans[%d]=%s,ansl[%d]=%d\n",op,ans[op]+1,op,ansl[op]);
}
void insert(int op)
{
	int p=1,len=strlen(s+1);
	bool flag=0;
	for(int i=1;i<=len;i++)
	{
		int ch=s[i]-'a';
		if(!trie[op][p][ch]) 
		{
			if(!flag) {solve(i-1,op);flag=1;}
			trie[op][p][ch]=++tot[op];
		}
		p=trie[op][p][ch];
	}
	if(!flag) solve(len,op);
}
int main()
{
	//freopen("wordlist.in","r",stdin);
	//freopen("wordlist.out","w",stdout);
	scanf("%d",&n);
	while(n--)
	{
		scanf("%s",s+1);
		insert(0);
		int tmpl=strlen(s+1);
		for(int i=1;i<=tmpl;i++)
			tmp2[i]=s[i];
		for(int i=1;i<=tmpl;i++)
			s[i]=tmp2[tmpl-i+1];
		//printf("s'=%s\n",s+1);
		memset(tmp2,0,sizeof(tmp2));
		insert(1);
	}
	printf("%s %d\n%s %d",ans[0]+1,anscnt[0],ans[1]+1,anscnt[1]);
	return 0;
}
/*
5
bbbaa
aacbb
bbdaa
aaaaa
bbcaa
*/

T2 password密码

本题一开始写的是 n^2 的暴力,没想到居然切了(感谢评测姬)

暴力思路就是用hash判断前后缀是否相同,枚举最长的长度(可能因为这里找到最长的长度就直接跳出,过了大数据)

正解是KMP,借此机会重新复习了一下板子

KMP也不止一种做法,正解是求出nxt之后一直跳到合法的nxt为止就可以,具体还是看代码吧,复杂度O(n)

还有一种复杂度O(nlogn)的KMP做法,也是先求出nxt,之后二分最长的前缀长度,check函数里面跑kmp,如果跑出当前前缀次数>=3则说明合法

代码:

暴力代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int INF = 0x3f3f3f3f,N = 1e6+10,base = 10,mod = 1e9+7;
int n,T;
ll mi[N],sum[N];
char s[N];
void pre()
{
	mi[0]=1;
	for(int i=1;i<=n;i++)	
	{
		sum[i]=sum[i-1]*base+s[i];sum[i]%=mod;
		mi[i]=mi[i-1]*base;mi[i]%=mod;
	}
}
ll Hash(int l,int r)
{
	return (sum[r]-(sum[l-1]*mi[r-l+1]%mod)+mod)%mod;
}
int main()
{
	//freopen("password.in","r",stdin);
	//freopen("password.out","w",stdout);
	//scanf("%d",&T);
//while(T--)
	{
		bool flag=0;
		scanf("%s",s+1);
		n=strlen(s+1); 
		pre();
		//printf("hash=%lld,%lld\n",Hash(1,3),Hash(13,15));
		for(int len=n;len>=1;len--)//枚举长度 
		{
			if(flag) break;
			if(Hash(1,len)!=Hash(n-len+1,n)) continue;
			for(int i=2;i+len-1<n;i++)//中间部分 
				if(Hash(1,len)==Hash(i,i+len-1))
				{
					for(int i=1;i<=len;i++)
						printf("%c",s[i]);
					printf("\n");
					flag=1;	
					break;
				}	
		}
		if(!flag) printf("Just a legend\n");
	}
	return 0;
}
/*
3
fixprefixsuffix
abcdabc
abcabcabcabc
*/
O(n)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int INF = 0x3f3f3f3f,N = 1e6+10;
int nxt[N],vis[N],len;
char s[N];
void pre()
{
	nxt[1]=0;
	int j=0; 
	for(int i=1;i<len;i++)
	{
		while(j&&s[j+1]!=s[i+1]) j=nxt[j];
		if(s[j+1]==s[i+1]) j++;
		nxt[i+1]=j; 
	}
}
int main()
{
	scanf("%s",s+1);
	len=strlen(s+1);
	pre();
	for(int i=1;i<len;i++) 
		if(nxt[i]) vis[nxt[i]]=1;
	int l=len;
	while(l)
	{
		if(vis[nxt[l]]) 
		{
			for(int i=1;i<=nxt[l];i++)
				printf("%c",s[i]);
			return 0;
		}
		l=nxt[l];
	}
	printf("Just a legend\n");
	return 0;
}
O(nlogn)
#include <bits/stdc++.h>
#define fo(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout)
using namespace std;
const int INF = 0x3f3f3f3f , N = 1e6+5 , base = 131;
typedef long long ll;
typedef unsigned long long ull;
inline ll read(){
	ll ret = 0 ; char ch = ' ' , c = getchar();
	while(!(c >= '0' && c <= '9')) ch = c , c = getchar();
	while(c >= '0' && c <= '9') ret = (ret << 1) + (ret << 3) + c - '0' , c = getchar();
	return ch == '-' ? -ret : ret;
}
int n;
char a[N];
int p[N];
ull ba[N],ha[N];
void gethash(char a[]){
	ba[0] = 1;
	for(int i = 1 ; i <= n ; i ++)
		ba[i] = ba[i-1] * base,
		ha[i] = ha[i-1] * base + a[i] - 'a' + 1;
}
inline ull Hash(int l,int r){
//	printf("gethash(%d,%d) : %llu\n",l,r,ha[r] - ha[l-1] * ba[r-l+1]);
	return ha[r] - ha[l-1] * ba[r-l+1];
}
void pre(char b[]){
	p[1] = 0;
	for(int i = 2 , j = 0 ; i <= n; i ++){
		while(j && b[j+1] != b[i]) j = p[j];
		if(b[j+1] == b[i]) j ++;
		p[i] = j;
	}
}
int kmp(char a[],char b[],int m){
	int ans = 0;
	for(int i = 1 , j = 0 ; i <= n ; i ++){
		while(j && b[j+1] != a[i]) j = p[j];
		if(b[j+1] == a[i]) j ++;
		if(j == m) ans ++ , j = p[j];
	}
	return ans;
}
int ans[N],tot;
inline bool check(int len){return kmp(a,a,len) > 2;}
void work(){
	scanf("%s",a+1);
	n = strlen(a+1);
	gethash(a);
	pre(a);
	tot = 0;
	for(int i = 1 ; i < n ; i ++)
		if(Hash(1,i) == Hash(n-i+1,n))
			ans[++tot] = i;
	int l = 0 , r = tot;
	while(l < r){
		int mid = (l + r + 1) >> 1;
		if(check(ans[mid])) l = mid;
		else r = mid - 1;
	}
	if(!l)puts("Just a legend");
	else {for(int i = 1 ; i <= ans[l] ; i ++)putchar(a[i]);puts("");}
}
signed main(){
//	fo("password");
//	int T = read();
//	while(T--)
		work();
	return 0;
} 
/*
3
fixprefixsuffix
abcdabc
abcabcabcabc
*/

T3 readtree树读

由于思路一直受限于Trie树,居然没有想到显而易见的暴力:直接爆搜,用map/set去重,查询答案为mp.size()/s.size()

正解:

使用字符串 HASH 的话,注意到一颗子树 subtree[i]内读到的字符串可以同
时在前面加上从点 1 到点 i 父节点的部分,因此思路就是 DFS 整棵树,对于每个
节点,从根节点走到该节点都对应一个字符串,取 hash 值,一棵子树的结果就
是子树对应 DFS 区间内不同 hash 值的个数。
这个问题可以按照询问区间左端点从右到左排序并从右到左处理序列中的
hash 值——每当读到一个 hash 值的时候,这个位置往右具有相同 hash 值的项
就可以看作无效了,写一个树状数组单点修改以及求和即可。
(或者考虑自底到上维护逆序字符串的 hash 值集合进行启发式合并,不过
感觉略微繁琐一些)
如果使用 Trie 的话,就是做自底到上合并——对于当前节点 i,建立 Trie
树的根节点 root[i],假设存在一个有字符 c 的子节点 son,那么就令 root[i]
读入 c 的转移为 son,如果 root[i]读入 c 原本已有转移,那么就对这两棵 Trie
树进行类似于线段树合并的技巧并统计 Trie 树上当前有多少个不同的字符串,
时间复杂度为可重复线段树合并的 O(nlogn)*字符集的大小,复杂度分析与启发
式合并类似。

然而,我不知道启发式合并,同时也不了解Trie的合并,不过查一下就明了了,附上解释的传送门:启发式合并基础概念

写启发式合并的时候一直对id数组的意义搞不清楚,总想用hson直接代替,但是是不行的,因为那样理论上还需要把重儿子的map传给祖先(否则祖先每次就只统计了向下一层的mp.size(),显然是不对的)

启发式合并代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
const int INF = 0x3f3f3f3f,N = 3e5+10,base = 233;
int ecnt=-1,head[N<<1],n;
ll res[N];
int hson[N],id[N];
char s[N];
ull hash[N];
map<int,map<ull,bool> >mp;
map<ull,bool>::iterator it;
struct edge
{
	int nxt,to;
}a[N<<1];
void add(int x,int y)
{
	a[++ecnt].nxt=head[x];
	a[ecnt].to=y;
	head[x]=ecnt;
}
void dfs(int u,int fa)
{
	hash[u]=hash[fa]*base+s[u]-'a'+1;
	res[u]=1;//初始化(这里不初始化也可以,但是初始化符合逻辑) 
	for(int i=head[u];~i;i=a[i].nxt)
	{
		int v=a[i].to;
		if(v==fa) continue;
		dfs(v,u);
		if(!hson[u]||res[hson[u]]<res[v]) hson[u]=v; //找重儿子 
	}
	/*
	if(!hson[u]) hson[u]=u;
	mp[hson[u]][hash[u]]=1;//当前字符串hash值放入map 
	for(int i=head[u];~i;i=a[i].nxt)
	{
		int v=a[i].to;
		if(v==fa||v==hson[u]) continue;
		for(it=mp[hson[v]].begin();it!=mp[hson[v]].end();it++)
			mp[hson[u]][(*it).first]=1;//统计所有轻儿子 
	}
	res[u]=mp[hson[u]].size();
	*/
	if(!hson[u]) id[u]=u;
	else id[u]=id[hson[u]];
	mp[id[u]][hash[u]]=1;//当前字符串hash值放入map 
	for(int i=head[u];~i;i=a[i].nxt)
	{
		int v=a[i].to;
		if(v==fa||v==hson[u]) continue;
		for(it=mp[id[v]].begin();it!=mp[id[v]].end();it++)
			mp[id[u]][(*it).first]=1;//统计所有轻儿子 
	}
	res[u]=mp[id[u]].size();
}
int main()
{
	memset(head,-1,sizeof(head));
	scanf("%d",&n);
	scanf("%s",s+1);
	for(int i=1;i<n;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y),add(y,x);
	}
	dfs(1,-1);
	for(int i=1;i<=n;i++) printf("%lld ",res[i]);
	return 0;
}

然而Trie的合并仍然不太了解,简单看了下代码感觉不太复杂,这里也贴上

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=300005;
struct node
{
	int s[30],size,w;
}trie[maxn];
int n,ecnt,head[maxn<<1],ans[maxn],root[maxn],tot;
char ch[maxn];
struct edge
{
	int nxt,to;
}e[maxn<<1];
void add(int x,int y)
{
	e[++ecnt]=(edge){head[x],y};
	head[x]=ecnt;
}
void merge(int &r1,int &r2)
{
	if(r1==0) {r1=r2;return;}
	if(r2==0) return;
	trie[r1].size=1;
	for(int i=1;i<=26;i++)
	{
		merge(trie[r1].s[i],trie[r2].s[i]);
		trie[r1].size+=trie[trie[r1].s[i]].size;
	}
}
void dfs(int u,int fa)
{
	root[u]=++tot;
	trie[root[u]].size=1;
	trie[root[u]].w=ch[u]-'a'+1;
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
		merge(trie[root[u]].s[ch[v]-'a'+1],root[v]);
	}
	trie[root[u]].size=1;
	for(int i=1;i<=26;i++) trie[root[u]].size+=trie[trie[root[u]].s[i]].size;
	ans[u]=trie[root[u]].size;
}
int main()
{
	cin>>n;
	scanf("\n%s\n",ch+1);
	int u,v;
	for(int i=1;i<n;i++) cin>>u>>v,add(u,v),add(v,u);
	dfs(1,0);
	for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
	return 0;
}
/*
10
cacabbcddd
1 2 
6 8 
7 2 
6 2 
5 4 
5 9 
3 10
2 5 
2 3
*/

T4 bracket括号序列

考场上有接近正解思路的一个题,但也只是接近

想到了加括号最多只需要加abs(左括号-右括号)个,调整字典序后在最左边加左括号,在最右边加右括号

没想到的?那就多了:

  • 记录当前状态是否合法,进一步地,用前缀和记录(左括号-右括号)的值,可以O(1)查询当前是否合法
  • 把括号序列长度×2,因为把末尾接到开头本质上相当于成环
  • 用pos记录答案序列的开始位置
  • 用ST表或者单调队列维护区间最小值(也可以偷懒用multiset
  • 对于每一个枚举的区间 [ i , i+n-1 ] ,二分找到和当前答案区间相同的前缀,比较第一个不相同的位置。若答案区间该位置是右括号且当前区间该位置是左括号,就更新答案区间

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int INF = 0x3f3f3f3f,N = 5e5+10,base = 233;
const ll mod = 1e9+8;
char s[N*2];
int sum[N*2],n,a[N*2],L,R,pos;
ll sumh[N*2],mi[N*2];
void pre()
{
	mi[0]=1;
	for(int i=1;i<=n*2;i++)
	{
		sumh[i]=(sumh[i-1]*base+s[i]-'(')%mod;
		mi[i]=mi[i-1]*base%mod;
	}
}
inline ll Hash(int l,int r)
{
	return (sumh[r]-sumh[l-1]*mi[r-l+1]%mod+mod)%mod; 
}
multiset<int> st;
int main()
{
	scanf("%s",s+1);
	n=strlen(s+1);
	for(int i=1;i<=n;i++)
		if(s[i]=='(') a[i]=a[i+n]=1,s[i+n]=s[i],L++;
		else a[i]=a[i+n]=-1,s[i+n]=s[i],R++;
	for(int i=1;i<=n*2;i++) sum[i]=sum[i-1]+a[i];
	pre();
	for(int i=1;i<=n;i++) st.insert(sum[i]);
	int num=R-L;
	//printf("num=%d\n",num);
	for(int i=1;i<=n;i++)
	{
		if(i!=1)
		{
			st.erase(st.find(sum[i-1]));
			st.insert(sum[i+n-1]);
		}
		
		int minn=*st.begin();
		if(minn-sum[i-1]+max(num,0)<0) continue;
		if(!pos) {pos=i;continue;}
		//printf("i=%d\n",i);
		int l=1,r=n;
		while(l<r)
		{
			int mid=(l+r)>>1;
			if(Hash(i,i+mid-1)==Hash(pos,pos+mid-1)) l=mid+1;
			else r=mid; 
		}
		if(s[i+l-1]=='('&&s[pos+l-1]==')') pos=i;
	}
	//printf("pos=%d\n",pos);
	if(num>=0) 
	{
		for(int i=1;i<=num;i++) printf("(");
		for(int i=pos;i<=pos+n-1;i++) printf("%c",s[i]);
	}
	else 
	{
		for(int i=pos;i<=pos+n-1;i++) printf("%c",s[i]);
		for(int i=1;i<=-num;i++) printf(")");
	}
	return 0;
}
posted @ 2021-08-21 23:29  conprour  阅读(29)  评论(0编辑  收藏  举报