题目
思路
我佛了……搬题人翻译了个什么鬼话……
最初以为只能修改一段连续的相同字符,问题变为了一个矩形填充的问题。没有任何头绪,瞎想了很久。
终于又看题,发现题目中并没有说是连续的字符……可是翻译题面中说那是一支笔!难道一支笔不应该画过连续字符吗?是你没用过笔还是我没用过笔?
重新读了题。发现问题等价于,给出一个 D A G \rm DAG DAG,找到边数最小的 G ′ = ( V , E ′ ) G'=(V,E') G′=(V,E′) 使得 G ′ G' G′ 上传递闭包是原图传递闭包的超集。
然后随便胡了一个贪心上去:比如 a a a 要到达 b , c b,c b,c 两个点,那么显然 a → b → c a\rightarrow b\rightarrow c a→b→c 是最优选择。所有点都这样接出一条链,就行了!
也没多想这玩意儿是不是正确的。毕竟这场比赛是 3.5 h 3.5h 3.5h 四道题,这才 T 1 T_1 T1 就用了 1 h 1h 1h 了,八嘎!于是就得到了四十五分的好成绩。
上面那个贪心有啥问题呢?就是假如现在 a a a 已经有后继节点了,又应该选谁呢?真是漏洞百出;稍微想想,就不会觉得它是正确的。
而在这个想法的基础上,再进一步就是正解。上面其实已经说明了一点:没有一个点的出度超过一。同理可知,没有点的入度超过一。所以新图全是链!
那么,所有在原图上有边相连的点,都必须在新图的同一条链中。换句话说,原图的每个弱连通块都对应新图的一条链。所以答案就是 ∣ V ∣ − ∣ C ∣ |V|-|C| ∣V∣−∣C∣,其中 ∣ C ∣ |C| ∣C∣ 是连通块个数。
时间复杂度 O ( n + ∣ Σ ∣ ) \mathcal O(n+|\Sigma|) O(n+∣Σ∣) 。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cctype>
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;
}
const int MAXN = 100005;
char a[MAXN], b[MAXN];
unsigned int g[26];
int main(){
for(int T=readint(); T; --T){
int n = readint();
scanf("%s %s",a+1,b+1);
memset(g,0,26<<2);
bool bad = false;
rep(i,1,n){
if(a[i] > b[i]){
bad = true; break;
}
g[a[i]-'a'] |= (1u<<(b[i]-'a'));
g[b[i]-'a'] |= (1u<<(a[i]-'a'));
}
if(bad){
puts("-1"); continue; }
rep(k,0,25) rep(i,0,25) if(g[i]>>k&1)
g[i] |= g[k]; // floyed
int ans = 0, now = 0;
for(int i=0; i!=26; ++i)
if(!(now>>i&1) && g[i]){
ans += __builtin_popcount(g[i])-1;
now |= g[i]; // merge this
}
printf("%d\n",ans);
}
return 0;
}
后记
这件事给了我们一个很深刻的教训:不是所有人都脑子没病,知道怎么说人话;把题读清楚,否则慌乱之中错误频发!