一道Google top coder的850分例题及解答

一道Google top coder的850分例题及解答
 
原题:
 
假设有这样一种字符串,它们的长度不大于 26 ,而且若一个这样的字符串其长度为 m ,则这个字符串必定由 a, b, c ... z 中的前 m 个字母构成,同时我们保证每个字母出现且仅出现一次。比方说某个字符串长度为 5 ,那么它一定是由 a, b, c, d, e 这 5 个字母构成,不会多一个也不会少一个。嗯嗯,这样一来,一旦长度确定,这个字符串中有哪些字母也就确定了,唯一的区别就是这些字母的前后顺序而已。
 
现在我们用一个由大写字母 A 和 B 构成的序列来描述这类字符串里各个字母的前后顺序:
 
l         如果字母 b 在字母 a 的后面,那么序列的第一个字母就是 A (After),否则序列的第一个字母就是 B (Before);
l         如果字母 c 在字母 b 的后面,那么序列的第二个字母就是 A ,否则就是 B;
l         如果字母 d 在字母 c 的后面,那么 …… 不用多说了吧?直到这个字符串的结束。
 
这规则甚是简单,不过有个问题就是同一个 AB 序列,可能有多个字符串都与之相符,比方说序列"ABA",就有"acdb"、"cadb"等等好几种可能性。说的专业一点,这一个序列实际上对应了一个字符串集合。那么现在问题来了:给你一个这样的AB 序列,问你究竟有多少个不同的字符串能够与之相符?或者说这个序列对应的字符串集合有多大?注意,只要求个数,不要求枚举所有的字符串。
 
注:如果结果大于10亿就返回-1。
 
 
我的最终解答(没有考虑溢出的情况):
 
// CODE 1
// the best way
// O(N^2)
int countABbest(const string& AB)
{
    assert(AB.find_first_not_of("AB") == string::npos);
 
    vector<int> current, next; // should we reserve these vectors?
    current.push_back(1);
 
    for (string::const_iterator letter = AB.begin();
        letter != AB.end(); ++letter) {
        next.resize(current.size()+1); // or next.insert(next.end(), 2, 0);
        next[0] = 0; // in fact, we could set the entire vector to zero
 
        if (*letter == 'A') {
            partial_sum(current.begin(), current.end(), next.begin()+1);
        } else {
            partial_sum(current.rbegin(), current.rend(), next.begin()+1);
            reverse(next.begin(), next.end());
        }
        swap(current, next);
    }
    return accumulate(current.begin(), current.end(), 0);
}
 
int main()
{
    constchar* AB = "ABBAAB";
    printf("'%s' : %d\n", AB, countABbest(AB));
}
 
下面谈一谈我在解决这个问题时的思路。
 
第一步 初步分析
 
以下“字符串”特指题目中提到的由小写字母a、b、c等等组成的字符串,每个字母出现且仅出现一次。显然题目要求我们写一个函数f,f的输入是一个长度为v 的AB序列w,w代表了一个字符串集合s(集合中的元素都是长度为m(m=v+1)的字符串),f的返回值是这个集合的元素个数|s|,即|s|=f(w)。用高中学过的一点排列组合知识,可分析知:
1.       长度为m的字符串有m! 个(’!’ 表示阶乘)因为这相当于m个不同字母的全排列;
2.       长度为v的AB序列有2^v个(’^’ 表示指数)因为每个位置有2种可能,一共有v个位置;
3.       由于2^v <= m! (m=v+1),所以AB序列的数目不大于字符串的数目。
4.       每个字符串刚好有一个AB序列与之对应。比如对于字符串”abdec”,我们很容易得知b在a后,c在b后,d在c前,e在d后,因此它对应的AB序列为”AABA”。可见拿到一个字符串,立刻就能求出它对应的那一个AB序列。
5.       每个AB序列至少对应一个字符串(当然也对应多个,因为字符串数目远远大于AB序列数目)。比如任取一个AB序列”ABA”,很容易构造出与它对应的字符串:
                                  i.    b在a后,得”ab”;
                                ii.    c在b前,得”acb”或”cab”;
                              iii.    d在c后,拿”acb”来说,可得”acdb”和”acbd”;拿”cab”来说,可得 ”cdab”、”cadb”和”cabd”;这样一共构造了5个与”ABA”对应的字符串,而且不会再有别的字符串了(why?)。
其实我们已经找到了蛮力解决问题的办法。
6.       根据4、5,得知如果穷举出长度为v的AB序列(共2^v个),并计算每个序列对应的字符串数目,那么把所有这些数目加起来,应该等于(v+1)!,这可以用作我们算法的一个检验。
7.       其实这可以看作集合的划分,把一个有 m! 个元素的集合U划分为2^v个不相交的子集s_0, s_1, s_{2^v–1},每个子集s_i是一个类别,每个字符串都属于一个类别,问题转变为求给定类别中有多少个元素。
 
第二步 蛮力解决
 
在想到前面的分析之前,我先用一种蛮力办法部分地解决了这个问题,思路是拿到一个长度为v的AB序列,穷举所有长度为v+1的字符串,遇到匹配的就记录下来。这样得到第一个程序,这个程序虽然效率极低,但可以用来检验后面程序的正确性,是个标竿。
 
// CODE 2
bool match(const string& AB, const string& str)
{
    // many ways to improve this function, but we won’t bother it.
    for (size_t i = 0; i < AB.length(); ++i) {
        size_t first = str.find('a'+i);
        size_t second = str.find('a'+i+1);
        assert(first != string::npos && second != string::npos);
 
        if (AB[i] == 'A' && first > second) {
            returnfalse;
        } elseif (AB[i] == 'B' && first < second) {
            returnfalse;
        }
    }
    returntrue;
}
 
// the stupid way
// O(N! * N^2)
int countAB(const string& AB)
{
    assert(AB.find_first_not_of("AB") == string::npos);
 
    string str;
    int count = 0;
    int m = (int)AB.length() + 1;
   
    // construct the initial string
    for (int i = 0; i < m; ++i) {
        str.push_back('a'+i);
    }
   
    do {
        if (match(AB, str)) {
            printf("%s, ", str.c_str());
            count++;
        }
    } while (next_permutation(str.begin(), str.end()));
   
    return count;
}
 
上面这个程序是以AB序列为中心,想办法找到与它匹配的字符串。为了看它能否通过第6点分析的检验,我写了一个enumAB(int v)函数,用来穷举长度为v的所有AB序列,并做检验(检验基本靠眼)。
 
// CODE 3
void enumAB(int v)
{
    assert(0 <= v && v < 26);
    int nAB = 1 << v;
    int total = 0;
 
    for (int i = 0; i < nAB; ++i) {
        string AB;
        for (int bit = v-1; bit >= 0; --bit) {
            if (i & (1 << bit)) {
                AB.push_back('B');
            } else {
                AB.push_back('A');
            }
        }
        int count = countAB(AB);
        total += count;
        printf("%s : %d\n", AB.c_str(), count);
    }
 
    printf("\nTotal strings: %d\n", total);
}
 
以下是enumAB(4)的运行结果(5!=120,初步检验通过):
 
AAAA : 1
AAAB : 4
AABA : 9
AABB : 6
ABAA : 9
ABAB : 16
ABBA : 11
ABBB : 4
BAAA : 4
BAAB : 11
BABA : 16
BABB : 9
BBAA : 6
BBAB : 9
BBBA : 4
BBBB : 1
 
Total strings: 120
 
如果想穷举所有AB序列和它们对应的字符串,还可以用一种效率稍高的蛮力算法,以字符串为中心,穷举所有长度为m的字符串,把它归入相应的AB序列名下。代码如下。
 
// CODE 4
string getAB(const string& str)
{
    constchar* alphabet = "abcdefghijklmnopqrstuvwxyz";
    assert(str.find_first_not_of(alphabet, 0, str.length()) == string::npos);
 
    int pos[26] = {0};
    char AB[26] = {0};
 
    int m = (int)str.length();
   
    for (int i = 0; i < m; ++i) {
        pos[str[i]-'a'] = i;
    }
 
    for (int i = 0; i < m-1; ++i) {
        AB[i] = pos[i] < pos[i+1] ? 'A' : 'B';
    }
 
    return AB; // we are not return the local char array, but a string object.
}
 
void enumStr(int m)
{
    string str;
    int nAB = 0;
   
    for (int i = 0; i < m; ++i) {
        str.push_back(char('a'+i));
    }
   
    map<string, vector<string> > AB2strs;
   
    do {
        string AB = getAB(str);
        //printf("%s is of %s\n", str.c_str(), AB.c_str());
        AB2strs[AB].push_back(str);
    } while (next_permutation(str.begin(), str.end()));
   
 
    for (map<string, vector<string> >::iterator it = AB2strs.begin();
        it != AB2strs.end(); ++it) {
            ++nAB;
            printf("%s (%d): ", it->first.c_str(), it->second.size());
            for (vector<string>::iterator str = it->second.begin();
                str != it->second.end(); ++str) {
                    printf("%s, ", str->c_str());
            }
            printf("\n");
    }
    printf("\nTotal ABs : %d\n", nAB);
}
 
以下是enumStr(4)的运行结果(2^3=8,初步检验通过):
 
AAA (1): abcd,
AAB (3): abdc, adbc, dabc,
ABA (5): acbd, acdb, cabd, cadb, cdab,
ABB (3): adcb, dacb, dcab,
BAA (3): bacd, bcad, bcda,
BAB (5): badc, bdac, bdca, dbac, dbca,
BBA (3): cbad, cbda, cdba,
BBB (1): dcba,
 
Total ABs : 8
 
第三步 进阶分析
 
我们也可以根据前面第5点分析,做出一个更高效的蛮力算法,不过蛮力毕竟是蛮力,还是让我们动动脑筋,做个真正高效的算法吧。
我第一次拿到这个问题时,先用蛮力算法打印出前面的结果,试图分析其规律,没成功。便又在纸上演算了了一阵,发现其实可以递推解决(当然也可以递归解决),以下内容最好在纸上演算。比如对于序列”AAA”,字母d只可能在第3号位置出现一次(abcd);递推一下,对于序列”AAAB”,e在d前,那么e可以在第0、1、2、3号位置各出现一次(eabcd、aebcd、abecd、abced)。
又比如根据以前面第5点分析,如果我们知道对于序列”AB”,字母c可能在第0号位置出现一次(cab)、在第1号位置出现一次(acb);那么对于序列”ABA”,字母d会在第1、2、3号位置分别出现1、2、2次,因此”ABA”对应的字符串共有5个;同理对于序列”ABB”,字母d会在第0、1号位置分别出现2、1次,因此”ABB”对应的字符串共有3个。
继续递推,对于序列”ABBA”,e在d后,那么e可以在第1、2、3、4号位置分别出现2、3、3、3次(具体说来,对于d在第0号位置出现2次,那么e可以在第1、2、3、4号位置各出现2次;d在第1号位置出现1次,那么e可以在第2、3、4号位置各出现1次,对位加起来就得到前面“2、3、3、3”的结果),因此”ABBA”对应的字符串共有11个。
到这里,我们已经发现递推的规律了:对于AB序列w,用二维数组occurs[][]表示第letter个字母在位置pos出现的次数occurs[letter][pos](这个说法不太严格,应该说是w的前面长度为letter的子序列对应的字符串中,最大那个字母出现的位置和次数,呵呵,还是比较绕口)。如果字母p在位置q1出现n1次,而AB序列的当前元素为’A’,那么字母p+1会在位置q1+1, q1+2, . . . , p各出现n1次;如果AB序列的当前元素为’B’,那么字母p+1会在位置0, 1, . . . , q1各出现n1次;如果字母p还在q2位置出现了n2次,那么对于’A’ 情况,字母p+1还会在位置q2+1, q2+2, . . . , p各出现n2次;那么对于’B’ 情况,字母p+1还会在位置0, 1, . . . , q2各出现n2次。需要把这些情况都累加起来。
对于序列”ABBAA”,递推表如下:
1, 0, 0, 0, 0, 0       字母a在位置0出现1次
0, 1, 0, 0, 0, 0       字母b在位置1出现1次
1, 1, 0, 0, 0, 0       字母c在位置0、1分别出现1次
2, 1, 0, 0, 0, 0       字母d在位置0、1分别出现2、1次
0, 2, 3, 3, 3, 0       字母e在位置1、2、3、4分别出现2、3、3、3次
0, 0, 2, 5, 8, 11     字母f 在位置2、3、4、5分别出现2、5、8、11次
可知对应的字符串有26个。如果细心,已经能发现递推中的部分和(partial sum)关系。
 
第四步 解决
 
既然递推关系有了,很容易就能写出代码。这个算法的复杂度是O(N^3)。
 
// CODE 5
// the better way
// O(N^3)
int countABbetter(const string& AB)
{
    assert(AB.find_first_not_of("AB") == string::npos);
 
    int v = (int)AB.length();
    int m = v + 1;
 
    // 'letter' at 'pos' occurs 'occurs[letter][pos]' times.
    vector<vector<int> > occurs(m, vector<int>(m, 0));
 
    // letter 'a' at pos 0, 1 time
    occurs[0][0] = 1;
 
    for (int letter = 1; letter < m; ++letter) {
        for (int pos = 0; pos < letter; ++pos) {
            int first_pos = 0;
            int last_pos = 0;
 
            if (AB[letter-1] == 'A') {
                // after current pos
                first_pos = pos + 1;
                last_pos = letter;
            } else {
                assert(AB[letter-1] == 'B');
                // before (and at) current pos
                first_pos = 0;
                last_pos = pos;
            }
 
            int occur = occurs[letter-1][pos];
            for (int t = first_pos; t <= last_pos; ++t) {
                occurs[letter][t] += occur;
                assert(occurs[letter][t] >= 0);
            }
        }
    }
    return accumulate(occurs[m-1].begin(), occurs[m-1].end(), 0);
}
 
第五步 优化
 
前面提过一句,在递推的过程中其实隐藏了一个“部分和”的关系,利用这一性质,可以很容易地将复杂度降为O(N^2),而且递推只是根据当前字母的出现位置退出下一字母的出现位置,因此可以省去2维数组,改用两个vector就行了。最后的代码就是前面一开始列出的 CODE 1。
 
第六步 展望
 
我猜测算法的复杂度能进一步降到 O(N log N),不过自己已经没有能力实现了。另外,为了附庸风雅一把,我发现整个递推算法的过程如果用矩阵来描述,会变得相当清楚。比如对于序列”ABAAB”,很容易构造矩阵A1、B2、A3、A4、B5(每个矩阵都是6阶方阵),初始向量x=[1 0 0 0 0 0]T,生成向量y=B5*A4*A3*B2*A1*x,那么对应的字符串有sum(y)个(sum表示y的各分量之和)。
注:也可以定义初始向量x=[1],矩阵A1是2x1、矩阵B2是3x2、矩阵A3是4x3、……、矩阵B5是6x5,一样可以计算出向量y。
例如:(这些矩阵中的元素都是0或1,排列起来像三角形(因为是求部分和),很有规律的。)
A1 = [0; 1]
B2 = [1 1; 0 1; 0 0]
A3 = [0 0 0; 1 0 0; 1 1 0; 1 1 1]
A4 = [0 0 0 0; 1 0 0 0; 1 1 0 0; 1 1 1 0; 1 1 1 1]
B5 = [1 1 1 1 1; 0 1 1 1 1; 0 0 1 1 1; 0 0 0 1 1; 0 0 0 0 1; 0 0 0 0 0]
算出y = B5*A4*A3*B2*A1 = [9 9 9 8 5 0] T
sum(y) = 40,与前面程序的结果相同。
 
. 完 .


Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=653418

[点击此处收藏本文]   陈硕发表于 2006年04月06日 22:04:00
 
dynamic programming 发表于2006-04-07 08:51:00  IP: 10.61.98.*
就是以dynamic programming算法。

Python实现如下:

def genPositions(a, position):
doSum = { 'A':(lambda x: sum(position[:x])),'B':(lambda x: sum(position[x:])) }
return map( doSum[a[-1]], range(len(a)+1) )

def pn(a):
if( len(a) == 0 ): return [1]
return genPositions(a, pn(a[0:-1]))

def count(a):
return sum(pn(a))


 
Solstice 发表于2006-04-07 15:09:00  IP: 207.46.89.*
拿 Scheme 写了一个递归版的,初学乍练,写得不地道,请大家拍砖。

(define (countAB AB)
(if (string? AB)
(sum (listAB AB))
(display "The argument must be a AB-sequence!\n")))

(define (sum L)
(apply + L))

(define (listAB AB)
(if (= (string-length AB) 0)
(list 1)
(let ((last (- (string-length AB) 1)))
(next-listAB (string-ref AB last) (listAB (substring AB 0 last))))))

(define (next-listAB lastABchar sub-listAB)
(if (char=? lastABchar #\A)
(cons 0 (partial-sum sub-listAB))
(append (reversed-partial-sum sub-listAB) (list 0))))

(define (partial-sum L)
(reverse (reversed-partial-sum (reverse L))))

(define (reversed-partial-sum L)
(if (null? L)
(list)
(cons (sum L) (reversed-partial-sum (cdr L)))))

(countAB "ABABABAB")

 
曾登高 发表于2006-04-17 16:50:00  IP: 211.100.21.*
TrackBack来自《上周技术关注:《断背山2》之Google爱情故事》

今天周末,给大家讲一个发生在Google里面的爱情故事。说到爱情故事,真是无奇不有。如果你对传统男女之间的生离死别感到厌倦,那么李安导演的两个牛仔之间的断背之情可能会在一定程度上满足你的要求。当然,如果你不喜欢牛仔,那么王家卫的《春光乍泄》是个不错的后备选择——至少张国荣与梁朝伟都是国产帅哥。什么?!你很喜欢这种类型的爱情故事?!那好,下面是一个发生在Google里面的《断背山2》。

 
S 发表于2006-04-24 09:47:00  IP: 218.94.21.*
谷歌,在深谷里面唱歌,只有自己才能听到!

现在搜索的数量和质量和baidu比差远了!

长期这样下去,我看连alltheweb都不如了

 
请教 发表于2006-04-28 09:18:00  IP: 61.141.169.*
// CODE 1
// the best way
// O(N^2)
???
为什么会是O(n^2)
我觉得你的算法复杂度是:O(nlogn)

 
Solstice 发表于2006-04-28 13:07:00  IP: 207.46.89.*
外层循环执行N次,内层partial_sum和reverse的复杂度都是O(N),所以一共是O(N^2)。

 
璇锋暀 发表于2006-04-28 13:51:00  IP: 59.40.26.*
明白了,谢谢

 
再请教一个问题 发表于2006-04-28 14:49:00  IP: 59.40.26.*
下面是一个对"活动安排"进行重新排列的问题的贪心算法:它的时间复杂度是O(nlogn),这个logn应该如何解释???是否能够给出一些直观的解释,谢谢

template<class Type>
void GreedySelector(int n, Type s[], Type f[], bool A[])
{
A[1]=true;
int j=1;
for(int i=2; i<=n; i++)
{
if(s[i]>=f[j])
{
A[i]=true;
j=i;
}
else
{
A[i]=false;
}
}
}

 
Solstice 发表于2006-04-28 18:28:00  IP: 207.46.89.*
建议你去问写出这个算法的人。

 
xinshijie 发表于2006-05-09 13:10:00  IP: 60.10.134.*
楼主,题目没看完也知道这种题目肯定是找递归关系啦。这种类型的题目N年前我中学竞赛训练时递归数列那块就出现过很多了。用了10分钟写了一下代码。。。感觉应该可以推出统一公式直接计算,等会推推试试,当年常干这活。呵呵

最近在做Google那道e的小数点后十万位的题目,头都大了。

 
西玉林 发表于2006-05-30 22:12:00  IP: 162.105.199.*
算法在f[]已排序情况下是O(n),如果未排序,则是排序复杂度O(nlogn)

 
malin 发表于2006-06-29 20:26:00  IP: 221.219.48.*
先写了一个,明天再加入DP试试

#include<iostream>

using namespace std;

char input[32];
int len;

int recurise(char left, char right, char posi)
{
//cout << "recurise call: " << (int)left << " " << (int)right << " " << (int)posi << endl;

if( posi >= len)
return 1;

int all = left + right + 1; //当前共有
int sum = 0;
int temp, i;

if( 'A' == input[posi] ) //在之后
{
for( i = left + 1; i <= all; i++ )
{
temp = recurise( i, all - i, posi + 1 );

if( -1 == temp || temp > 1000000000 )
return -1;
sum += temp;
}
}
else //'B',在之前
{
for( i = left; i >= 0; i-- )
{
temp = recurise( i, all - i, posi + 1);

if( -1 == temp || temp > 1000000000 )
return -1;
sum += temp;
}
}

return sum;
}

int main(void)
{
cin >> input;
cout << "input: " << input << endl;

len = strlen( input );
int sum = recurise( 0, 0, 0 );

cout << "result: " << sum << endl;
}


 
malin 发表于2006-06-29 20:48:00  IP: 221.219.48.*
只有上一个字符的位置,和它左边右边的已排个数对现在的字符位置有影响,所以就用数字就可以了,不用字符串。加上DP可能更快。
对不起我没仔细看楼主兄的内容,周末有时间再看看吧。感觉您考虑复杂了,也许是我肤浅了,见谅。

 
malin 发表于2006-06-30 18:41:00  IP: 221.217.51.*
今天一看,原来是4月的帖子,哎
这是加上DP的最终,基本上是最快的了

#include <iostream>
#include <map>

using namespace std;

#pragma pack(push,1)
struct key
{
char left;
char right;
char posi;
char no;

key( char _left, char _right, char _posi )
{
left = _left;
right = _right;
posi = _posi;
no = 0;
}

bool operator<( const key &one ) const
{
return *(int*)this < *(int*)&one;
}
};
#pragma pack(pop)

char input[32];
int len;
map<key, int> dp_map;

int recurise( char left, char right, char posi );

inline int from_dp( char left, char right, char posi )
{
key k( left, right, posi ); //make a key
map<key, int>::iterator i = dp_map.find( k ); //find
int temp;

if( dp_map.end() == i ) //no found
{
temp = recurise( left, right, posi );
dp_map.insert( i, make_pair(k, temp) );

return temp;
}
else //found
{
return i->second;
}
}

int recurise( char left, char right, char posi )
{
if( posi >= len )
return 1;

int all = left + right + 1; //in all at present
int sum = 0;
int temp, i;

if( 'A' == input[posi++] ) //after
{
for( i = left + 1; i <= all; i++ )
{
temp = from_dp( i, all - i, posi );

if( -1 == temp || temp > 1000000000 )
return -1;
sum += temp;
}
}
else //'B', before
{
for( i = left; i >= 0; i-- )
{
temp = from_dp( i, all - i, posi );

if( -1 == temp || temp > 1000000000 )
return -1;
sum += temp;
}
}

return sum;
}

int main( void )
{
cin >> input;

len = strlen( input );
cout << "m = " << len + 1 << endl;

if(len > 25)
return -1;

int sum = recurise( 0, 0, 0 );
cout << "result: " << sum << endl;
}


 
Solstice 发表于2006-06-30 21:40:00  IP: 221.216.152.*
请问你这个算法的复杂度是O(?) ?

 
aRies 发表于2006-07-16 23:09:00  IP: 211.100.21.*
TrackBack来自《一道Google top coder的850分例题及解答》

一道Google top coder的850分例题及解答

 
aRies 发表于2006-07-16 23:09:00  IP: 211.100.21.*
TrackBack来自《一道Google top coder的850分例题及解答》

一道Google top coder的850分例题及解答

 
phoenixsh 发表于2006-08-10 21:29:00  IP: 58.33.28.*
不知道DP跟这道题有什么关系。malin的程序没看懂,不过他有一个地方说对了,就是“只有上一个字符的位置,和它左边右边的已排个数对现在的字符位置有影响”。知道这一点就可以递推下去了。只需要一个数组来保存上次最后一个字符在各个位置上的出现频率,当然计算中还需要另外一个数组保存当前结果。这个题是有O(n)的解法的。

to xinshijie: 我怀疑简单的公式解的存在性。


 
phoenixsh 发表于2006-08-13 19:37:00  IP: 218.82.51.*
想了一个周末,要想突破O(n^2)看来还是比较困难的。
posted on 2006-08-25 12:22 hunter's space 阅读(397) 评论(0)  编辑 收藏 引用 所属分类: 程序/技术
只有注册用户登录后才能发表评论。