zz: http://www.strongczq.com/2012/03/srm534-div1-3-ellysstring.html
题目原文:http://community.topcoder.com/stat?c=problem_statement&pm=10700&rd=14727
题目原文:http://community.topcoder.com/stat?c=problem_statement&pm=10700&rd=14727
题目大意:
给定两个长度相等的英文小写字母字符串S和T,用最少的次数将S转化为T,每次可以对S进行以下两种操作之一:
- 将S中的任意一个字母替换成另一个字母
- 将S中相邻的两个字母对换
数据规模:字符串
最大长度可达2500.
思路:
看到题目的第一感觉的是,1000分的题目居然这么简短,真是不思议。后来看官方题解,说到这道题好像有多种解法,效率相差的也比较大。官方给出的解答不是最优的(O(n*n*logn),至少比我下面给出的解法效率低),我没太看懂。据说Petr的解法是最简单的,我也同样没看懂...
先分析一下问题,其实不难发现调换两个字母的位置不会节省太多的操作次数。假设存在某个
最优解法,我们可以把S字符串按照该解法分成一个个不相交的区间,满足以下条件:
- 在最优解中,两个区间之间没有发生字母对换(即相邻区间的头字母和尾字母没有发生对换)
- 一个区间之内,只有存在两种情况:a. 只发生了字母替换,没有发生字母对换;b. 没有发生字母替换,相邻的字母两两之间均发生了一次对换(未必是按顺序进行对换)
然后我们分析一下字母对换带来的好处,对于b类型的区间,假设区间长度为n,则操作次数为n-1,而最坏情况下如果S和T在该区间内对应位置的字母都不同,用字母替换也只需要n次操作。所以每个b类型的区间最多只能节省1次操作。
可以确定b类型的区间在结构上具有以下特点:
- b区间内S和T对应位置的字母均不同
- b区间内S和T的同一个字母出现的次数完全相同(因为如果不同,仅靠字母对换是搞不定的,至少还需要一次字母替换)
- 定义左子区间为从区间最左端开始的一个子区间,类似的也定义右子区间。那么b区间中任意长度的左子区间或右子区间内,S与T正好有两个字母出现的次数相差1。
以下给出我自己的两种解法。
解法一:复杂一点的DP,但是不需要对问题特点有深入的了解,并且效率比较高O(n), dp部分的空间复杂度可达到O(1)
从左到右扫描两个字符串,显然如果对应位置的字母是相同的就不需要什么操作了,如果是不同的话,那么有两种可能操作了(如果是最后一个字母就只能直接替换了)。直接替换比较简单,不需要讨论。字母对换的话就需要多考虑一点。假设当前的位置为pos,则字母对换的对象是pos+1,那么现在的问题是,在实际最优解法中,S[pos+1]很可能也会和S[pos+2]对换,那么这两次对换的实际顺序就需要考虑了。
我们可以设计两个DP函数来解决这个问题:
- f1(pos,c): 只考虑[pos, n) 区间,当S[pos]值已转化成c时,最少还需要多少次操作可以将该区间内的S转换成与T相同;
- f2(pos,c): 只考虑[pos, n) 区间,最少需要多少次操作可以将S[pos]转化成c,把[pos+1,n)区间的S转换成与T相同;
考虑f1(pos,c)。当c==T[pos]时,不需要任何操作,所以f1(pos,c)=f1( pos + 1,S[pos + 1])。不等的时候取以下几种情况f1(pos,c)的最小值:
- 直接替换S[pos]为T[pos]:则f1(pos,c) = f1(pos + 1,S[pos + 1]) + 1;
- 先对换S[pos](值为c)与S[pos+1]:则f1(pos,c) = f1(pos + 1,c) + 1;如果换来的S[pos+1]与T[pos]不等还要再加1(实际上如果不等就可以不考虑这种情况了)。
- S[pos+1]已经发生变化,然后才与S[pos]对换:假设S[pos+1]先被转化为x后,再对换S[pos]与S[pos+1],x可以取值'a'-'z'。那么可以首先考虑将S[pos+1]转化为x,并且[pos+2, n)区间S转化为T的最小操作数f2(pos+1,x)。对于每一个x,我们还要考虑x与T[pos],c与T[pos + 1]是否相等,不等的话均要加1。遍历x从'a'到'z',取最小值。(实际上这里只需要考虑x等于T[pos]的情况即可)
考虑f2(pos,c)。当S[pos]==c时,不需要任何操作,所以f2(pos,c)=f2(pos + 1,T[pos + 1])。不等的时候取以下几种情况f2(pos,c)的最小值:
- 直接替换S[pos]为c,则f2(pos,c) = f2(pos + 1,T[pos + 1]) + 1;
-
先对换S[pos](值为c)与S[pos+1]:则f1(pos,c) = f1(pos + 1,c) + 1;如果换来的S[pos+1]与T[pos]不等还要再加1(实际上如果不等就可以不考虑这种情况了)。
- S[pos+1]已经发生变化,然后才与S[pos]对换:假设S[pos+1]先被转化为x后,再对换S[pos]与S[pos+1],x可以取值'a'-'z'。那么可以首先考虑将S[pos+1]转化为x,并且[pos+2, n)区间S转化为T的最小操作数f2(pos+1,x)。对于每一个x,我们还要考虑x与c,S[pos]与T[pos + 1]是否相等,不等的话均要加1。遍历x从'a'到'z',取最小值。(实际上这里只需要考虑x等于c的情况即可)
最后,f1(0, S[0])即为将S转换成T的最少操作数。可以使用迭代的方法实现以上介绍的DP算法。假设S和T的长度为N,则该算法的时间复杂度为O(N*26*26),其中26为c的取值个数,以下代码中的空间复杂度为O(N),实际上可以进一步优化DP实现,使得dp部分的空间复杂度达到O(26)(即O(1))。
Java代码:
public class EllysString{
public int theMin(String[] ss, String[] tt)
{
String s = "";
String t = "";
for(String str : ss){
s += str;
}
for(String str : tt){
t += str;
}
int n = s.length();
int[] si = new int[n];
int[] ti = new int[n];
for(int i = 0; i < n; ++i){
si[i] = s.charAt(i) - 'a';
ti[i] = t.charAt(i) - 'a';
}
int[][] mem1 = new int[n][26];
int[][] mem2 = new int[n][26];
for(int i = 0; i < 26; ++i){
if(ti[n - 1] == i){
mem1[n - 1][i]= 0;
}else{
mem1[n - 1][i] = 1;
}
if(si[n - 1] == i){
mem2[n - 1][i] = 0;
}else{
mem2[n - 1][i] = 1;
}
}
for(int pos = n - 2; pos >= 0; --pos){
for(int c = 0; c < 26; ++c){
if(ti[pos] == c){
mem1[pos][c] = mem1[pos + 1][si[pos + 1]];
}else{
mem1[pos][c] = mem1[pos + 1][c] + (si[pos + 1] == ti[pos] ? 1 : 2);
mem1[pos][c] = Math.min(mem1[pos][c], mem1[pos + 1][si[pos + 1]] + 1);
for(int j = 0; j < 26; ++j){//这个循环其实可以不用,只考虑j==ti[pos]的情况
int tmp = mem2[pos + 1][j] + 1;
tmp += (c == ti[pos + 1] ? 0 : 1);
tmp += (j == ti[pos] ? 0 : 1);
mem1[pos][c] = Math.min(mem1[pos][c], tmp);
}
}
if(si[pos] == c){
mem2[pos][c] = mem2[pos + 1][ti[pos + 1]];
}else{
mem2[pos][c] = mem1[pos + 1][si[pos]] + (si[pos + 1] == c ? 1 : 2);
mem2[pos][c] = Math.min(mem2[pos][c], mem2[pos + 1][ti[pos + 1]] + 1);
for(int j = 0; j < 26; ++j){//这个循环其实可以不用,只考虑j==i的情况
int tmp = mem2[pos + 1][j] + 1;
tmp += (c == j ? 0 : 1);
tmp += (si[pos] == ti[pos + 1] ? 0 : 1);
mem2[pos][c] = Math.min(mem2[pos][c], tmp);
}
}
}
}
return mem1[0][si[0]];
}
}
解法二:比较简单的DP,但是需要对问题有一定的剖析,时间复杂度为O(n*n),空间复杂度为O(n)
解法二的代码简洁程度可以和Petr的一拼,时间空间复杂度,DP的思路上也基本相同,但是具体的状态转移方程不同,没能完全理解Petr的思路。
这个解法是基于寻找b类型区间的。dp函数为f(pos),表示最少需要多少次操作可以将[0, pos]区间内的S转成T。状态转移方程为,考虑第pos+1个位置:
- 如果S[pos+1]与T[pos+1]相等,则f(pos+1)=min(f(pos+1), f(pos));
- 如果不相等,那么我们可以有两种操作
-
- 替换S[pos+1]为T[pos+1],则f(pos+1)=min(f(pos+1), f(pos)+1);
- 寻找一个以pos+1为起点的b区间,如果能找到,且区间长度为len,则f(pos+len) = min(f(pos+len), f(pos) + len - 1)。
需要注意的是,对于pos+1位置,即便能找到以该位置为起点的b区间,最优解法中未必会采用这个b区间,所以贪心法是不可取的,必须dp。该算法的状态总数为n,状态转移过程的复杂度为O(n),所以总时间复杂度为O(n*n),空间复杂度为O(n)。
上面的算法描述中还遗留了一个问题,就是如何判断是否存在“以pos+1为起点的b区间”。在上面的分析中我们提到了b区间的特点,也就是成为b区间的必要条件。我们猜想这些条件也是成为b区间的充分条件。即满足以下条件的区间必然是b区间(相邻字母两两对换一次可以将S转成T):
- S和T对应位置的字母均不同
- 同一个字母出现的次数完全相同
- 任意长度的左子区间或右子区间内,S与T正好有两个字母出现的次数相差1。
假设该区间长度为m, 用[0,m-1]表示整个区间。我们首先证明该区间内存在两个相邻的字母(位置分别为x和x+1),对换这两个字母后,[0,x]和[x+1, m-1]各自满足以上三个条件。
证明:
显然,只要证明这两个区间都满足第二个条件,那么三个条件就都满足了。我们用二元组{c(i),d(i)}表示区间[0,i]内S比T多一个字母c(i),少一个字母d(i),显然i取值范围为[0,m-2]。考虑c(0),c(1),...,c(m-2)这个序列,找到第一个x,使得c(x)==c(x+1),如果不存在这样的x,则取x=m-2。根据x的选取规则可以判断S[x]=c(x), S[x+1]=d(x),对换S[x]与S[x+1]的位置,则[0,x]区间内S和T的各字母出现次数相同,[x+1, m-1]亦如此。得证。
根据以上的结论,我们在把划分后的子区间做同样的分割处理,每一次分割相当于对相邻两个字母进行了一次对换。最后整个区间被切割为每一个字母一个区间我们就完成对换了。所以上面的猜想得证。
Java代码:
public int theMin(String[] ss, String[] tt) {
String s = "";
String t = "";
for (String str : ss) {
s += str;
}
for (String str : tt) {
t += str;
}
int n = s.length();
int[] si = new int[n];
int[] ti = new int[n];
for (int i = 0; i < n; ++i) {
si[i] = s.charAt(i) - 'a';
ti[i] = t.charAt(i) - 'a';
}
int[] dp = new int[n + 1];
Arrays.fill(dp, n + 1);
dp[0] = 0;
int[] cnter = new int[26];
for (int i = 0; i < n; ++i) {
if(si[i] == ti[i]){
dp[i + 1] = Math.min(dp[i + 1], dp[i]);
}else{
dp[i + 1] = Math.min(dp[i + 1], dp[i] + 1);
Arrays.fill(cnter, 0);
boolean ok = false;
cnter[si[i]]++;
cnter[ti[i]]--;
int j = i + 1;
for (; j < n; ++j) {
if (si[j] == ti[j]) {
break;
} else {
cnter[si[j]]++;
cnter[ti[j]]--;
if (cnter[si[j]] != 0 && cnter[ti[j]] != 0) {
break;
} else if (cnter[si[j]] == 0 && cnter[ti[j]] == 0) {
ok = true;
break;
}
}
}
if (ok) {
dp[j + 1] = Math.min(dp[j + 1], dp[i] + j - i);
}
}
}
return dp[n];
}