NOIP2024模拟赛13:拆开未来

NOIP2024模拟赛13:拆开未来

写在前面:进制哈希的P不要用998244353会被卡!用131。注意取模为负数的情况!

C-重复

  • 一句话题意:给定字符串 S, 问 S 的所有子串共有多少种“好的拆分方案”。对于一个字符串 S, 一个划分是好的当且仅当能把 S 划分成 6 个非空子串 a,b,c,d,e,f, 满足 a=b=e, c=f (一个字符串可能有多种划分方式)

  • 标签:前缀和,思维,散列表(哈希)

  • 简化一下就是要满足 AABCAB.

  • 60分的暴力是对每一个区间枚举开头的 A 和结尾的 B. (P.S.模数设 998244353 会被卡!!!)

    const int N=5005;
    const ll P=131;
    ull h[N],p[N];
    char s[N];
    int n; ll ans=0;
    ull get(int l,int r){ return h[r]-h[l-1]*p[r-l+1]; }
    void solve(int l,int r){
    	int len=r-l+1;
    	F(i,1,len/3) for(int j=1;j<=len/2 && 3*i+2*j<len;++j){
    			ull A=get(l,l+i-1),B=get(l+i,l+2*i-1),C=get(l+2*i,l+2*i+j-1),E=get(r-i-j+1,r-j),F=get(r-j+1,r); 
    			if(A == B && B == E && C == F) ++ans;
    		}
    }
    signed main(){
    	freopen("c.in","r",stdin); freopen("c.out","w",stdout);
    	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    	cin>>(s+1); n=strlen(s+1); p[0]=1;
    	F(i,1,n) h[i]=h[i-1]*P+s[i]-'0',p[i]=p[i-1]*P;
    	F(l,1,n) F(r,l+5,n) solve(l,r);
    	cout<<ans;
    	return 0;
    }
    
  • 但正如邓老师所说,这种拆分方式是“不平衡的”,虽然我们可以用 O(1) 的时间去检查剩下的位置,但我们却需要花 O(N2) 的时间枚举所有的首尾情况。

  • 所以正解我们选择枚举 AB. 枚举 A 的左端点 iB 的右端点 j.

  • 对于 AA, 在固定 i 的情况下可以用前缀和处理所有贡献。

  • 对于 AB 和后面那个 AB 的匹配, 把所有已经扫到过的 AB 存进一个散列表并统计其个数(unorderedmap 会 T掉)

  • 两者的贡献相乘即是单次的总贡献。

  • 注意循环的初末条件,C 不为空(详见代码)

#include<bits/stdc++.h>
#define F(i,l,r) for(int i(l);i<=r;++i)
#define G(i,r,l) for(int i(r);i>=l;--i)
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int N=5005,mod=1e7+9;
const ll P=131;
ll h[N],p[N];
char s[N];
int n,f[N][N]; 
ll ans=0;
ull get(int l,int r){ return h[r]-h[l-1]*p[r-l+1]; }
struct node{//AB的散列表 
	int first[mod],ne[N*N/2],cnt=0;
	ll val[N*N/2],dui[N*N/2];
	//注意:first的下标肯定小于mod,但由于顺延的原因,散列表的总大小完全可能大于mod , 所以空间开 N*N/2 而不是mod 
	inline void update(ll x){
		ll o = (x%mod+mod)%mod;//把要加入的数用取模得到一个映射 
		for(int i=first[o];i;i=ne[i]){
			if(dui[i]==x){
				val[i]++;
				return ;
			}//对于已存在的数,直接加个数(val) 
		}
		//对于不存在的数,顺次延后新开一个位置(完全同邻接表写法) 
		ne[++cnt]=first[o];
		first[o]=cnt;
		dui[cnt]=x;
		val[cnt]=1;
		return ;
	}
	inline ll fd(ll x){
		ll o = (x%mod+mod)%mod;
		for(int i=first[o];i;i=ne[i]){
			if(dui[i]==x) return val[i];
		}
		return 0;
	}
}mp;//散列表(充当unordered_map,但更快) 
signed main(){
	freopen("c.in","r",stdin); 
	freopen("c.out","w",stdout);
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	cin>>(s+1); n=strlen(s+1); p[0]=1;
	F(i,1,n) h[i]=h[i-1]*P+s[i]-'0'+37,p[i]=p[i-1]*P;//哈希 
	
	F(len,1,n){
		for(int i=len+1;i+len-1<=n;++i){
			int j=i+len-1;
			f[i][j]=(get(i-len,i-1)==get(i,j));
		}
	}
	F(i,1,n) F(j,i,n) f[i][j]+=f[i][j-1];//处理 A=A 的前缀和 
	
	//枚举AB 
	for(int j = n-2; j >= 1; j--) { //B的右端点,由于是用AB更新散列表,长度至少要留2,所以 j 从 n-2 开始 
		//首先是统计答案 
		for(int i = 2; i < j; i++) {//A的左端点,因为要满足 AAB,所以i从2开始 
			int rp = min(2 * i - 2, j - 1);
			//因为A对称,所以A的长度最大为i-1,所以AA的右端点最大为  2*(i-1)
			//同时不能超过 B 的右端点 
			ans += 1ll * f[i][rp] * mp.fd(get(i, j));//AA的方案数 * AB 的方案数 = AAB的方案数
		}
		//接着是更新散列表 
		for(int i = j + 2; i <= n; i++) {
			mp.update(get(j + 1, i));//因为下一次的j统计答案时,至少要给C留1的位置,所以起点为 j+1. 
		}
	}
	cout<<ans<<"\n";
	return 0; 
}
posted @   superl61  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 全程使用 AI 从 0 到 1 写了个小工具
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
点击右上角即可分享
微信分享提示