【HT-086-Div.2】错乱的集合

比赛现场

更阅读体验的阅读体验

是个好题。但是我赛时怎么什么都不会。


首先简化一下题面:\(s\)\(t\) 被认为是相同的,当且仅当 \(s=t\)\(|t|=|s|-1\)\(t\)\(s\) 的后缀(或者反过来 \(s\) 是后缀)。

(以下都假设 \(s\) 是较长的那个)

那这样的话,\(s\) 就是 \(t\) 前面多一个字母。\(s\) 如果在集合里的话,\(t\) 就不能在集合里。反之亦然。

我们又考虑到,\(s\) 只会有一个对应的后缀 \(t\),但是 \(t\) 前面可以加任意字母构成任意的 \(s'\),也就是 \(t\) 会和多个较长的 \(s'\) 构成不合法关系。换句话说,这是个一对多的关系,约等于一个森林。

那这不就是《没有上司的舞会》吗?对的对的,如果我们把树建出来的话就能直接套用那个题的做法了。

那咋建树呢?或者说对于一个字符串 \(s\) 的话,怎么让它所有的前缀和它们不合法的那个后缀建上边呢?或者我们怎么找不合法后缀呢?

关键词:前缀。考虑用 Trie 树。我们把所有的串串扔进一个 Trie 树里。

原题解这里讲的不太清楚。我的理解是,对于第 \(i\) 个串 \(s_i\),我们从小到大枚举 \(j\) 表示当前这个前缀截止到 \(j\) 这个位置。

(由于我的坐标从 1 开始,所以我的 \(j \in [1,len]\)。)

我们对于每个 \(j\) 要看看是否有一个不合法后缀 \(s_{2,3,\cdots,j}\) 存在于 Trie 树上,有的话说明存在这样的前缀不能和当前前缀一个集合,我们就将当前前缀在树上的编号与这个前缀在树上的编号建边。

显然 \(j=1\) 时是没有这样的后缀的。当 \(j \in [2,len]\) 时,每当 \(j \to j+1\),那么当前要找的不合法后缀也会加一个对应字符。

比如我们考虑 abb 这个前缀的时候,它要找的不合法后缀是 bb。当我们考虑完这个位置,考虑 abba 这个前缀的时候,要找的不合法后缀也会多一个字母 a 变成 bba

这对应到 Trie 树上是什么?假设我们已经找完了 \(j\) 位置的不合法后缀,我们找 \(j+1\) 的时候,让 \(now \to tr_{now,s[i][j+1]}\),在树上往下跳即可。

如果找到了就像前面说的一样建边,如果找不到了就说明没有这样的后缀了,后面的前缀也不会再有了,直接跳出循环。

这样我们把树建好以后,跑一遍树上 dp 即可。

代码:

T2代码
#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=1e6+6;
int T,n,tr[N][30],awa,dp[N][2],h[N],fa[N],tot;
//tr:Trie 树
//awa:当前Trie树节点开到哪了 
//dp:树上dp数组
//fa[i]:Trie树上编号为 i 的点要向哪个点连边 
vector<int> pos[N];
//pos[i][j]:第 i 个串长度为 j 的前缀对应 Trie 树上的哪个点 
string s[N];
struct sw{
	int u,v,nxt;
}e[N];

inline void INIT(){
	for(int i=0;i<=awa;i++){
		dp[i][0]=dp[i][1]=0;fa[i]=0;h[i]=0;
		for(int j=1;j<=26;j++){
			tr[i][j]=0;
		}
	}
	awa=0;
	for(int i=1;i<=tot;i++){
		e[i]={0,0,0};
	}
	tot=0;
}

inline void INITT(){
	for(int i=1;i<=n;i++){
		pos[i].clear();
		s[i]=' ';
	}
}

inline void add(int u,int v){
	e[++tot]={u,v,h[u]};h[u]=tot;
}

inline void dfs(int u){
	//树上dp,不会的可看P1352,不过这里每个点的点权是1 
	dp[u][1]=1;
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].v;
		dfs(v);
		//考虑选u点的情况,此时子节点不能选 
		dp[u][1]+=dp[v][0];
		//考虑不选u点的情况,此时子节点任意 
		dp[u][0]+=max(dp[v][0],dp[v][1]);
	}
}

inline void ins(int id){
	//Trie树里的插入操作 
	int len=s[id].size()-1,now=0;
	pos[id].push_back(0);
	for(int i=1;i<=len;i++){
		int fu=s[id][i]-'a'+1;
		if(!tr[now][fu]){
			tr[now][fu]=++awa;
		}   
		now=tr[now][fu];
		pos[id].push_back(now);
	}
}

signed main(){
	freopen("b.in","r",stdin);
	freopen("b.out","w",stdout);
	T=read();
	while(T--){
		//多测记得初始化 
		INIT();
		n=read();
		INITT();
		for(int i=1;i<=n;i++){
			cin>>s[i];
			s[i]=' '+s[i];
			ins(i);
		}
		//处理每个点往哪里连边 
		for(int i=1;i<=n;i++){
			int now=0,len=s[i].size()-1;
			for(int j=2;j<=len;j++){
				int fu=s[i][j]-'a'+1;
				if(!tr[now][fu]){
					//没有找到不合法后缀,说明后面的点也找不到了 
					now=-1;
					break;
				} 
				now=tr[now][fu];
				//否则当前前缀记录往now上连边 
				fa[pos[i][j]]=now;
			}
		}
		//我们发现,对于没有限制的点,默认会往 0 号点连边,这样不仅是正确的(它们在dp里一定会被选),还不用再单独处理这种情况了 
		for(int i=1;i<=awa;i++){
			add(fa[i],i);
		}
		dfs(0);
		//由于 0 号点是虚拟的,所以选了没有意义,且有可能搞掉正确答案的选法 
		int ans=dp[0][0];
		printf("%lld\n",ans);
	}
	return 0;
}
posted @ 2025-11-14 20:13  qwqSW  阅读(9)  评论(0)    收藏  举报