[Google Code Jam] 2013-1A-C Good Luck 解法
问题的陈述在:https://code.google.com/codejam/contest/2418487/dashboard#s=p2&a=1,
官方的分析在:https://code.google.com/codejam/contest/2418487/dashboard#s=a&a=2。
这篇文章是结合官方的分析以及Dlougach的solution总结的解题思路。
1. 列举所有的可能的数字组合
由[2,M]区间内的数字组成长度为N的数组组合,本身有(M-1)N可能性,因为组合中的每一个数字都可以从[2,M]这M-1个数字中随机选取。
但是由于这些数字组合本身不存在顺序的问题,比如(5, 2, 3, 2)和(2, 2, 3, 5)是相同的,所以可以排除掉很多相同的情况。
可以编程列举出所有的数字组合,思路是从N个2的数字组合开始,顺序加1,直到N个M的组合。代码如下:
1 // all possible combinations 2 vector<vector<int>> digits; 3 vector<int> dig(N, 2); 4 while (dig != vector<int>(N, M)) 5 { 6 digits.push_back(dig); 7 int i = N-1; 8 while (dig[i] == M) i--; 9 dig[i]++; 10 for (int j = i + 1; j < N; j++) 11 dig[j] = dig[i]; 12 } 13 digits.push_back(dig); 14 int total = digits.size();
经过所有的列举,共有18564种可能的数字组合。
2. 计算每一种数字组合出现的概率。
当给出的所有的K个products都为1的时候应该选择哪一个数字组合呢?选择N个2还是其他?
要注意到,即便给出的所有K个products都为1,由于每一种数字组合出现的概率也有区别,也可以从中选取出可能出现概率最高的数字组合。
对于一个数字组合,计算这个数字组合可能出现的概率:
比如M = 8, N = 12的情况下,222333445558出现的可能性为:(C(12,3)*C(9,3)*C(6,2)*C(4,3)*C(1,1)) / (8-1)12,即在所有的12个位置中找3个位置摆放2,再在剩下的9个位置中找3个位置摆放3,再在剩下的6个位置中找2个位置摆放4.....最后除以总数(8-1)12。
其中C(n,k)就是我们在学概率时学到的Cnk。
所以要想求概率,首先需要统计出每个数字重复出现的次数,具体代码如下:
1 vector<double> prob(total, 0); 2 for (int i = 0; i < total; ++i) 3 { 4 vector<int> count(M+1, 0); 5 for (int j = 0; j < N; ++j) 6 count[digits[i][j]]++; 7 double p = 0; 8 int size = N; 9 for (int j = 2; j <= M; ++j) 10 { 11 p += log(C[size][count[j]]); 12 size -= count[j]; 13 } 14 prob[i] = p; 15 }
这段代码中有两点技巧:一是C(n,k)的计算方法,二是取对数(log)问题。
a) C(n,k)的计算
C(n,k)本身有一个计算公式是我们很熟悉的:C(n,k) = n! / (k!(n-k)!)。这样计算牵涉到阶乘,考虑到要计算多个C(n,k),为了重复利用计算好的结果,这里有一个比较简单的方法,就是杨辉三角型(Pascal's triangle)以及。
首先构建一个杨辉三角形存放于二维数组C中,如下图所示。这样的摆放有一个规律C[n][m] = C[n-1][m-1]+C[n-1][m],恰好是C(n,k)的另一种计算方法。
m = 0 m = 1 m = 2 m = 3 m = 4 m = 5...
n = 0 1 0 0 0 0 0...
n = 1 1 1 0 0 0 0...
n = 2 1 2 1 0 0 0...
n = 3 1 3 3 1 0 0...
n = 4 1 4 6 4 1 0...
这样一来只要提取数组元素C[n][k],就是C(n,k)的值,比如C(4,2) = C[4][2] = 6。数组搭建过程实现如下:
1 double C[13][13]; 2 memset(C, 0, sizeof(C)); 3 C[0][0] = 1; 4 for (int i = 1; i < 13; ++i) 5 { 6 C[i][0] = 1; 7 for (int j = 1; j <= i; ++j) 8 C[i][j] = C[i-1][j-1] + C[i-1][j]; 9 }
b) 取对数(log)问题
对数运算有一个性质:log(ABC...Z)=log(A)+log(B)+log(C)...log(Z)。
如果要比较几个元素的大小(所有元素都大于0),并且每个元素都由多个数的乘积构成,则可以简化为判断所有元素对数的大小(取对数不会影响增减性),这样一来对于每个元素来讲,并不需要计算多个数的乘法了,而是这些数取对数之后的加法。
这样计算有一个很大的好处,就是乘法运算本身很容易导致结果溢出,取对数变加法之后可以避免溢出,并且大大减小运算范围。
3. 对于每一种数字组合,列举所有可能出现的products,并对其进行计数
用一个mask,从1到1<<N遍历一遍,mask的二进制为1的相应位将参与product计算。比如mask为21,二进制表示为10101,那么该数字组合中,将从右数起的第1、3、5个数字相乘得到product。
1 vector<unordered_map<long long, int>> products(total); 2 for (int i = 0; i < total; ++i) 3 { 4 for (int mask = 1; mask < (1<<N); ++mask) 5 { 6 long long prod = 1; 7 for (int j = 0; j < N; ++j) 8 if (mask & (1<<j)) 9 prod *= digits[i][j]; 10 products[i][prod] += 1; 11 } 12 }
以上三步都是进行pre-computation,下面就是真正运行测试数据了。
4. 遍历每一种数字组合,计算该数字组合出现的概率,最后选取概率最高者输出。
给定product 1, 2, 3, ..., K的情况下,数字组合A出现的概率p(A) = A本身出现的概率prob[A] * product 1出现的概率products[A][product1] * .... * product K出现的概率products[A][product K]。
由于这些算好的概率都已经取号对数,所以应该用加法。
1 while (R--) 2 { 3 vector<int> test_products; 4 for (int i = 0; i < K; ++i) 5 test_products.push_back(rll()); 6 int res_i = 0; 7 double resProb = INT_MIN; 8 for (int i = 0; i < total; ++i) 9 { 10 double p = prob[i]; 11 for (int k = 0; k < K; ++k) 12 { 13 if (test_products[k] == 1) break; 14 unordered_map<long long, int>::iterator it = products[i].find(test_products[k]); 15 if (it != products[i].end()) 16 { 17 p += log((double)it->second); 18 } 19 else 20 { 21 p = INT_MIN; 22 break; 23 } 24 } 25 if (resProb < p) 26 { 27 res_i = i; 28 resProb = p; 29 } 30 } 31 for (int i = 0; i < digits[res_i].size(); ++i) 32 printf("%d", digits[res_i][i]); 33 printf("\n"); 34 }
这道题完整的代码可以在下面的链接获取:https://github.com/AnnieKim/ForMyBlog/blob/master/20130508/1A-C-Good%20Luck.cpp
原创文章,转载请注明出处:http://www.cnblogs.com/AnnieKim/archive/2013/05/08/3059614.html。