比赛现场
更阅读体验的阅读体验
是个好题。但是我赛时怎么什么都不会。
首先简化一下题面:\(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;
}