题目
题意概要
有一个字符串 S S S,还有 m m m 个变换规则 a i → b i a_i\rightarrow b_i ai→bi,表示将字符串中所有字符 a i a_i ai 都变为 b i b_i bi 。
现在你可以任意进行这些变换(顺序无限制,次数无上限)。请问,在所有变换都进行了至少 1 1 1 次之后,得到的 S S S 中最多能有多少种不同的字符?
数据范围与提示
∣ S ∣ ⩽ 1 0 3 |S|\leqslant 10^3 ∣S∣⩽103 而字符集大小 ∣ Σ ∣ = 62 |\Sigma|=62 ∣Σ∣=62 。事实上这个数据范围限制极其宽松……
思路
建图 a i → b i a_i\rightarrow b_i ai→bi 。由于字符一旦合并,就无法分离,所以目标就是最小化合并字符的次数。
一个很显然的性质是,当 x → y x\rightarrow y x→y 进行后,立刻进行 x x x 的其他出边(所对应的操作)是不会引起字符合并的。所以 一个点只用选一条出边,即只有第一次选择的出边需要考虑字符合并。
现在考虑 x → y x\rightarrow y x→y,假如 x , y x,y x,y 都是存在的字符,显然我们希望先进行 y → z y\rightarrow z y→z,让 y y y 腾出来。而 z z z 是存在的字符时,我们又希望 z → ε z\rightarrow\varepsilon z→ε,这样一直递归下去……
所以先考虑一个简化的问题,假设 图是 D A G \rm DAG DAG 。那么,如果想要避免字符合并,递归链的结尾一定是某个字符串中本不存在的字符。同时,递归链上的点都已经满足了 “选一条出边” 的限制。我们可以联想到 链剖分。
问了便于叙述,将不存在的字符简记为 ∅ \varnothing ∅ 。注意 “不存在” 也可能是操作后变得不存在。
具体是怎样剖分呢?考察一下是否可以相交:一条递归链的操作,类似于让最底部的 ∅ \varnothing ∅ 上浮。容易看出链之间没有任何限制条件,除了结尾不能相同(因为一个 ∅ \varnothing ∅ 不能用多次)。
那些无法避免字符合并的呢?显然还是拓扑序大的点先操作。那么操作仍然构成若干条链,每条链都会导致 1 1 1 次字符合并。同理,链是可以相交的,因为操作等同于将链顶变为 ∅ \varnothing ∅ 。
上面这两种链的唯一区别就是结尾是否为 ∅ \varnothing ∅ 即原本不存在的字符。那么我们先把 D A G \rm DAG DAG 的结论放在这里:字符合并的次数 = = = 结尾非 ∅ \varnothing ∅ 的链数量。
一通讲解猛如虎,任何结论皆显然。结果被 S i s t e r \color{black}S\color{red}ister Sister 提醒了很重要的一点:为什么是链剖分?因为链需要 经过所有的点。为什么需要经过所有的点?因为只有被链经过的点(非链尾)才是选择过出边的。这里就出现了两个问题:
- 所有点都需要选择出边吗?没有出边的点 是不一定在链上的!
- 怎么保证非链尾覆盖所有点?由于本就是 “最小链覆盖”,所以链尾非 ∅ \varnothing ∅ 且出度 ≠ 0 \ne 0 =0 必然不优。而链尾是 ∅ \varnothing ∅ 时,可以在最初直接选择出边,不需要被覆盖。
所以第一点要尤其注意:如果一个非 ∅ \varnothing ∅ 没有出边,它只可能作为链尾;将其删去,顶多让这条链变短,答案不会变劣。故需要将其从图中移除。
D A G \rm DAG DAG 算是会做了。如果图中有环,还是要往链剖分上想。研究一下 ∅ \varnothing ∅ 向上浮动的过程,可以发现 ∅ \varnothing ∅ 能够在环中完整地旋转若干圈,然后跑出去。也就是说,链可以完整地经过环。另一方面,链有没有必要在环中间停下呢?显然不停在 ∅ \varnothing ∅ 则不优。那么强连通分量只有两种选择:停在某个 ∅ \varnothing ∅ 上,或者被某个链贯穿。缩点 成一个非 ∅ \varnothing ∅,向其中的若干个 ∅ \varnothing ∅ 连边,问题就变回了上面的 D A G \rm DAG DAG 了!这个缩点方法有点像圆方树啊。
顺便提一句, D A G \rm DAG DAG 中需要删掉的特殊点略有变化:只有大小为 1 1 1 的强连通分量可以删去。因为大小非 1 1 1 的强连通分量,就其事实性而言,其中的点并不是没有出度的;只是缩点后呈现出这样的表象罢了。
求出最优链剖分则是经典问题:当链不能经过相同的点时,就是二分图匹配,给每个点的入度匹配上另一个点的出度;当链可以经过相同的点时,在传递闭包上求解即可——相当于跳过了用过的点。每匹配一次,链的数量就减少 1 1 1 个。
但是我们不完全是最小链剖分;我们要去掉以 ∅ \varnothing ∅ 结尾的链。事实上,如果没有以某个 ∅ \varnothing ∅ 结尾的链,从这里断开不会更劣。于是所有 ∅ \varnothing ∅ 都被用上了,就可以知道需要减去多少。规定 ∅ \varnothing ∅ 为链尾(将代表其出度的点移除),然后求最小链剖分即可。
时间复杂度 O ( ∣ S ∣ + m ∣ Σ ∣ ) \mathcal O(|S|+m|\Sigma|) O(∣S∣+m∣Σ∣) 。当然二分图匹配可以用网络流,但是完全没必要……
代码
#include <cstdio> // XJX yyds!!!
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cctype>
#include <climits>
#include <cmath>
using namespace std;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
typedef long long llong;
inline int readint(){
int a = 0, c = getchar(), f = 1;
for(; !isdigit(c); c=getchar())
if(c == '-') f = -f;
for(; isdigit(c); c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
void writeint(int x){
if(x > 9) writeint(x/10);
putchar(char((x%10)^48));
}
inline void getMin(int &a,const int &b){
if(b < a) a = b;
}
int haxi[CHAR_MAX+1];
void prepare(){
rep(i,'0','9') haxi[i] = i-'0'+1;
rep(i,'a','z') haxi[i] = i-'a'+11;
rep(i,'A','Z') haxi[i] = i-'A'+37;
}
const int n = 62;
namespace Graph{
uint64_t g[n+1];
void addEdge(int a,int b){
g[a] |= (1ull<<b);
}
bool vis[n+1]; int mat[n+1];
bool _dfs(int x){
rep(i,1,n) if(g[x]>>i&1)
if(!mat[i] || (!vis[i] &&
(vis[i] = true) && _dfs(mat[i])))
return mat[i] = x, true;
return false; // fail
}
int hungary(){
int ans = 0;
rep(i,1,n) memset(vis+1,false,n), ans += _dfs(i);
return ans;
}
}
uint64_t g[n+1]; int cnt[n+1];
int dfn[n+1], low[n+1], dfsClock, bel[n+1];
bool insta[n+1]; int sta[n+1], top;
void tarjan(int x){
dfn[x] = low[x] = ++ dfsClock;
sta[++ top] = x, insta[x] = true;
rep(i,1,n) if(g[x]>>i&1){
if(!dfn[i]) tarjan(i), getMin(low[x],low[i]);
else if(insta[i]) getMin(low[x],dfn[i]);
}
if(low[x] == dfn[x]){
for(int lst=-1; sta[top+1]!=x; --top){
insta[sta[top]] = false;
if(!cnt[sta[top]]) bel[sta[top]] = sta[top];
else bel[sta[top]] = (lst == -1) ? (lst = sta[top]) : lst;
}
}
}
uint64_t xing[n+1];
int siz[n+1]; bool wxk[n+1];
char str[1005];
int main(){
prepare();
scanf("%s",str);
for(int i=0; str[i]; ++i)
++ cnt[haxi[int(str[i])]];
for(int m=readint(),a,b; m; --m){
scanf("%s",str);
a = haxi[int(*str)];
b = haxi[int(str[1])];
g[a] |= (1ull<<b);
}
rep(i,1,n) if(!dfn[i]) tarjan(i);
rep(i,1,n) ++ siz[bel[i]];
rep(i,1,n) rep(j,1,n) if(g[i]>>j&1)
xing[bel[i]] |= (1ull<<bel[j]);
rep(j,1,n) rep(i,1,n) if(xing[i]>>j&1)
xing[i] |= xing[j]; // floyed
uint64_t bad = 0; ///< need to be covered
int lost = 0; ///< how many are lost
rep(i,1,n) if(cnt[i] && i == bel[i])
wxk[i] = (siz[i] == 1 && !xing[i]);
rep(i,1,n) if(cnt[i] && i == bel[i]){
if(wxk[i]) continue; // no outer-edge!
bad |= (1ull<<i), ++ lost;
rep(j,1,n) if(j != i && !wxk[j])
if(xing[i]>>j&1) Graph::addEdge(i,j);
}
lost -= Graph::hungary();
int ans = 0; rep(i,1,n) ans += !!cnt[i];
printf("%d\n",ans-lost);
return 0;
}