Edit Step Ladders

2013-05-31 Robert Zhang 更多博文 » 博客 » GitHub »

原文链接 http://huiming.io/2013/05/31/edit-step-ladders.html
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


分析:按字典序排列的n个单词构成了一个隐式的有向无环图:每个单词单词都有一条出边指向在它字典序之后的每一个单词。要在这张图里找出符合条件的最长路径包含的顶点数。简单的想法就是“暴力”回溯了:对每个顶点进行一次回溯,找出以该点开始的最长的edit step ladder——这个时间复杂度高得能上了火星!思考一下就会发现,回溯包含了大量的重复计算:假设以单词w开始的最长ladder长度为l,那么对于字典中排在w之前的每一个单词(实际上是edit step为1的那些单词),在回溯经过w时都要做同样的重复计算,浪费了大量时间。如果我们计算好了以w开始的最长ladder的长度l,那么对于w的每一个one edit step前驱顶点,其最长ladder长度即为l+1。<!--more-->想到这里,我们可以从字典最后一个单词开始向前,计算每一个单词的最长ladder长度并保存,如下:

(需要注意的是:虽然算法的时间复杂度是O(n^2),但若不考虑优化系数,仍然会在UVa里超时。为此以下算法把已计算过的单词按长度分组以减少比较次数,降低时间复杂度系数)

#include <iostream>
#include <vector>
#include <map>
#include <string>

using namespace std;

typedef vector<string> Dict;
typedef map<string, int> LadderLen;

//p1 and p2 must have the same length.
inline bool equal(const char * p1, const char * p2) {
  for (; *p1 && *p1 == *p2; p1++, p2++);
  return !*p1;
}

//w1 and w2 must be different and not empty!
bool one_step(const string & w1, const string & w2) {
  bool r = false;
  int d = w1.size() - w2.size();
  const char * p1 = w1.c_str();
  const char * p2 = w2.c_str();
  if (!d) {
    while (*p1++ == *p2++);
    if (!*p1)
      r = true;
    else
      r = equal(p1, p2);
  }
  else {
    while (*p1++ == *p2++);
    if (d > 0)
      r = equal(p1, --p2);
    else
      r = equal(p2, --p1);
  }
  return r;
}

inline void search(const string & w, const Dict & d, LadderLen & len, int & max) {
  for (int j = 0; j < d.size(); j++) {
    const string & w2 = d[j];
    if (one_step(w, w2) && len[w2] > max) {
      max = len[w2];
    }
  }
}

int ladder(const Dict & d) {
  LadderLen len;
  vector<Dict> d2(17);
  int r = 0;
  for (int i = d.size() - 1; i > 0; i--) {
    const string & w = d[i];
    int max = -1;
    search(w, d2[w.size()], len, max);
    if (w.size() < 16)
      search(w, d2[w.size() + 1], len, max);
    if (w.size() > 1)
      search(w, d2[w.size() - 1], len, max);
    d2[w.size()].push_back(w);
    len[w] = ++max;
    if (max > r)
      r = max;
  }
  return r + 1;
}

int main() {
  Dict d(1); //The first word is just a place holder
  string w;
  while (cin >> w) {
    d.push_back(w);
  }
  cout << ladder(d) << endl;
  return 0;
}

结语:其实,所谓的动态规划,很像回溯的逆过程:前者从“前”往后递增计算,后者从“后”往前递减计算。只不过前者还记录了一些计算的中间结果以避免重复计算(其实后者也可以,但考虑到栈的深度,一些情况下回溯并不合适)。