P10279 [USACO24OPEN] The 'Winning' Gene S

题目传送门

我的博客 - 欢迎光顾

本篇题解参照的是这位大佬的题解。(看不到的话点这个)。图片参考这篇题解

这个题面真的很绕。简化一下就是说,对于一个 \((K,L)\),要在原串里选出所有 \(K-mer\),然后对于每个 \(K-mer\),要找到字典序最小的长度为 \(L\) 的串串(有多个就选最靠左的)。然后我们把这个串串的起始位置丢到集合 \(P\) 里。

最后,这个集合 \(P\) 里会有所有 \(K-mer\) 扔进去的起始位置。当然,一部分起始位置会重复,所以 \(P\) 的大小不确定。

基本思路

我们发现这个 \(K\) 似乎没什么用……所以我们考虑一种常见的思路,叫做拆贡献。我们枚举 \(L\),接下来我们讨论的,都是基于特定的 \(L\) 之上的。

对于一个起始位置 \(pos\),它会被怎样的 \(K\) 丢进集合呢?显然,它要在某个 \(K-mer\) 里字典序最小才会有贡献。那也就是说,在某个区间里,不能有比它字典序更小的起始位置出现。

我们可以跑一遍单调栈,预处理出 \(l_{pos},r_{pos}\),分别表示左 / 右边第一个字典序小于 \(pos\) 的起始位置。

P10279_1_1

很显然,这样的话 \(K\) 的最小值取 \(L\)(即只有它一个串的情况),\(K\) 的最大值就是蓝色区间的长度,即 \(r_{pos}+L-2-l_{pos}\)

P10279_2_1

落在这个范围里的 \(K\),它总会有一个或多个 \(K-mer\),使得字典序最小的长度是 \(L\) 的串串开头是 \(pos\)。这样的话 \(pos\) 就会被扔进集合里了。

所以我们可以对 \([L,r_{pos}+L-l_{pos}-2]\) 进行区间加 1,这个用差分做就好。

(有些人就是数据结构学傻了,差分都用线段树写)

最后,我们用 \(ans_{K,L}\) 表示 \((K,L)\) 这个数对被贡献了多少次,也就是这个数对下集合里会有多少个数。我们的差分操作就是在这个数组上进行的。

我们另开一个桶 \(sum_{i}\) 表示集合大小为 \(i\) 的数对有几个。显然最后输出 \(sum_{1,2, \cdots ,n}\) 即可。

如何求字典序

大体思路有了,但是我们很快意识到一个严重的问题:我们枚举 \(L\),并且单调栈的时间复杂度已经是 \(O(n^2)\) 的了,而在做单调栈时要判断字典序大小,正常比较字典序是 \(O(n)\) 的,所以我们这样做的时间复杂度是 \(O(n^3)\) 的,正常情况下过不去。

那怎么办呢?我们考虑对求字典序的过程剪枝。

剪枝 1(记忆化)

字典序的比较规则是找第一个不相同的字符并比较大小。由于正常情况下我们是正序枚举 \(L\) 的,所以对于两个起始位置 \(p_1,p_2\),如果之前长度更短的情况下已经比较出来了大小,那我们现在就无须比较了。

具体实现上也很简单:单独开一个数组 \(f_{p1,p2}\) 记录当前是否已经比较出来了胜负,0 表示没有,1 表示字典序上 \(p_1>p_2\),-1 表示字典序上 \(p_1<p_2\)。每次判断字典序时提前判断即可。

但是这样会被一些特殊的构造卡掉:

aaaa...ab

aaaa...aa

显然我们直到发现 a<b 后才能比较大小。于是我们有了剪枝2。

剪枝 2(当前弧优化)

类似P7771里记录当前第一条没有被走过的边,我们记录 \(h_{p1,p2}\) 表示当前的 \(p1,p2\) 最后一个已被比较的位置是什么。这样我们比较字典序的时候,直接比较 \(h_{p1,p2}+1\) ~ \(L\) 的位置即可。

总之我们终于可以 AC 这个题了。

代码:

P10279
#include<bits/stdc++.h>
#define int long long
using namespace std;

inline int read(){
	int x=0,f=1;char c=getchar();
	while(c<48){
		if(c=='-') f=-1;
		c=getchar();
	}
	while(c>47) x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return x*f;
}

const int N=3e3+3;
int n,ans[N][N],l[N],r[N],f[N][N],h[N][N],sum[N];
char s[N],ch[N][N];
stack<int> st;
//ch[i][j]:在第i个位置扩展j位,最后一位是什么
//ans[k][l]:有多少初始位置贡献了(k,l)二元组 

inline bool cmp1(int p1,int p2,int len,char s1[],char s2[]){
	//记忆化 
	if(f[p1][p2]==1) return 1;
	if(f[p1][p2]==-1) return 0;
	//当前弧优化 
	for(int i=h[p1][p2]+1;i<=len;i++){
		if(s1[i]>s2[i]){
			f[p1][p2]=1;
			return 1;
		}
		else if(s1[i]<s2[i]){
			f[p1][p2]=-1;
			return 0;
		}
	}
	//记得更新f,h 
	h[p1][p2]=len;
	return 0;
}

//同cmp1 
inline bool cmp2(int p1,int p2,int len,char s1[],char s2[]){
	if(f[p1][p2]==1) return 1;
	if(f[p1][p2]==-1) return 0;
	for(int i=h[p1][p2]+1;i<=len;i++){
		if(s1[i]>s2[i]){
			f[p1][p2]=1;
			return 1;
		}
		else if(s1[i]<s2[i]){
			f[p1][p2]=-1;
			return 0;
		}
	}
	h[p1][p2]=len;
	return 1;
}

signed main(){
	n=read();
	scanf("%s",s+1);
	for(int len=1;len<=n;len++){
		//len同题解里的L 
		for(int i=1;i+len-1<=n;i++){
			//每次len+1时显然只会扩展1位 
			ch[i][len]=s[i+len-1];
			l[i]=0,r[i]=0;
		}
		//单调栈处理l,r数组 
		while(!st.empty()) st.pop();
		//解释一下两个cmp函数:
		//在字典序相同的情况下,优先取左边的串 
		//所以从左往右的单调栈是单调不升的,从右往左的单调栈是单调递减的 
		for(int i=1;i+len-1<=n;i++){
			while(!st.empty()&&cmp1(st.top(),i,len,ch[st.top()],ch[i])){
				st.pop();
			}
			if(!st.empty()) l[i]=st.top();
			else l[i]=0;
			st.push(i);
		}
		while(!st.empty()) st.pop();
		for(int i=n-len+1;i>=1;i--){
			while(!st.empty()&&cmp2(st.top(),i,len,ch[st.top()],ch[i])){
				st.pop();
			}
			if(!st.empty()) r[i]=st.top();
			else r[i]=n-len+2;
			st.push(i);
		}
		//区间加 
		for(int i=1;i+len-1<=n;i++){
			ans[len][len]++;ans[r[i]+len-l[i]-1][len]--;
		}
	}
	//统计答案 
	for(int j=1;j<=n;j++){
		for(int i=1;i<=n;i++){
			ans[i][j]+=ans[i-1][j];
			sum[ans[i][j]]++;
		}	
	}
	for(int i=1;i<=n;i++){
		printf("%lld\n",sum[i]);
	}
	return 0;
}
posted @ 2025-11-05 15:35  qwqSW  阅读(8)  评论(0)    收藏  举报