SRM534-div1-3-EllysString


题目大意:
      给定两个长度相等的英文小写字母字符串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];
    }


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值