[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。

posted @ 2013-05-08 21:35  AnnieKim  阅读(1677)  评论(0编辑  收藏  举报