[CodeJam2020]Replace All

题目

传送门 to CodeJam

题意概要
有一个字符串 S S S,还有 m m m 个变换规则 a i → b i a_i\rightarrow b_i aibi,表示将字符串中所有字符 a i a_i ai 都变为 b i b_i bi

现在你可以任意进行这些变换(顺序无限制,次数无上限)。请问,在所有变换都进行了至少 1 1 1 次之后,得到的 S S S 中最多能有多少种不同的字符?

数据范围与提示
∣ S ∣ ⩽ 1 0 3 |S|\leqslant 10^3 S103 而字符集大小 ∣ Σ ∣ = 62 |\Sigma|=62 Σ=62 。事实上这个数据范围限制极其宽松……

思路

建图 a i → b i a_i\rightarrow b_i aibi 。由于字符一旦合并,就无法分离,所以目标就是最小化合并字符的次数。

一个很显然的性质是,当 x → y x\rightarrow y xy 进行后,立刻进行 x x x 的其他出边(所对应的操作)是不会引起字符合并的。所以 一个点只用选一条出边,即只有第一次选择的出边需要考虑字符合并。

现在考虑 x → y x\rightarrow y xy,假如 x , y x,y x,y 都是存在的字符,显然我们希望先进行 y → z y\rightarrow z yz,让 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;
}

猜你喜欢

转载自blog.csdn.net/qq_42101694/article/details/121365474
ALL