您的位置:首页 > 编程语言 > Go语言

[Google Code Jam] 2013-1A-C Good Luck 解法

2013-05-08 21:35 288 查看
问题的陈述在: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的组合。代码如下:

// all possible combinations
vector<vector<int>> digits;
vector<int> dig(N, 2);
while (dig != vector<int>(N, M))
{
digits.push_back(dig);
int i = N-1;
while (dig[i] == M) i--;
dig[i]++;
for (int j = i + 1; j < N; j++)
dig[j] = dig[i];
}
digits.push_back(dig);
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。

所以要想求概率,首先需要统计出每个数字重复出现的次数,具体代码如下:

vector<double> prob(total, 0);
for (int i = 0; i < total; ++i)
{
vector<int> count(M+1, 0);
for (int j = 0; j < N; ++j)
count[digits[i][j]]++;
double p = 0;
int size = N;
for (int j = 2; j <= M; ++j)
{
p += log(C[size][count[j]]);
size -= count[j];
}
prob[i] = p;
}


这段代码中有两点技巧:一是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
[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
[k],就是C(n,k)的值,比如C(4,2) = C[4][2] = 6。数组搭建过程实现如下:

double C[13][13];
memset(C, 0, sizeof(C));
C[0][0] = 1;
for (int i = 1; i < 13; ++i)
{
C[i][0] = 1;
for (int j = 1; j <= i; ++j)
C[i][j] = C[i-1][j-1] + C[i-1][j];
}


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。

vector<unordered_map<long long, int>> products(total);
for (int i = 0; i < total; ++i)
{
for (int mask = 1; mask < (1<<N); ++mask)
{
long long prod = 1;
for (int j = 0; j < N; ++j)
if (mask & (1<<j))
prod *= digits[i][j];
products[i][prod] += 1;
}
}


以上三步都是进行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]。

由于这些算好的概率都已经取号对数,所以应该用加法。

while (R--)
{
vector<int> test_products;
for (int i = 0; i < K; ++i)
test_products.push_back(rll());
int res_i = 0;
double resProb = INT_MIN;
for (int i = 0; i < total; ++i)
{
double p = prob[i];
for (int k = 0; k < K; ++k)
{
if (test_products[k] == 1) break;
unordered_map<long long, int>::iterator it = products[i].find(test_products[k]);
if (it != products[i].end())
{
p += log((double)it->second);
}
else
{
p = INT_MIN;
break;
}
}
if (resProb < p)
{
res_i = i;
resProb = p;
}
}
for (int i = 0; i < digits[res_i].size(); ++i)
printf("%d", digits[res_i][i]);
printf("\n");
}


这道题完整的代码可以在下面的链接获取:https://github.com/AnnieKim/ForMyBlog/blob/master/20130508/1A-C-Good%20Luck.cpp

原创文章,转载请注明出处:http://www.cnblogs.com/AnnieKim/archive/2013/05/08/3059614.html。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: