第一题 纸牌游戏 (20分)
John喜欢与同伴们玩一种纸牌游戏。游戏的规则如下:总共有N张牌,每个人手里拿着K张牌。每张牌上有个数字。每个人手里那副牌的强度取决于其中最大的那张牌的数字。手中牌强度最大的那个人获胜。在揭示所有玩家的牌之前,每个人都可以打赌自己可以获胜。
John需要你的帮助来帮助他赢得赌局。他决定当他手中牌的强度高于平均牌的强度时,他就打赌自己能获胜。因此,Joh需要计算所有人手中牌的平均强度(就是说这N张牌的集合中所有大小为K的子集的平均强度)。John自己能做除法运算,所以他需要你帮他计算其他人手中牌的强度总和。
你的任务是:
给你一个数组N,里面最多有10,000个不同的整数。给你一个整数K,K在1到N之间。我们知道,从N个数的全集里可以找到若干个大小为K的子集,把每个子集中最大的那个数求总和。最后,把总和整除1,000,000,007取余数。
官方答案:
这是本轮比赛中最简单的题目,60%的参赛者都成功解决了此题。给出一个有N个不同整数的数组A,我们需要打印出 所有大小为K的子集中最大值的求和。最后的结果需要被1000000007整除取余数,你应该知道,1000000007是一个素数。
首先,我们给整个数组排序,这样 A[1] < A[2] < ... < A[n]。
现在,我们需要知道,对于其中任意的整数 A[i] ,它能够在大小为K的子集中充当最大数的情况的总数,当然前提条件是 i >=k (因为如果 i<k,那么 A[i] 不可能是任何子集中最大的数)。好了,假设 A[i] 是一个子集中最大的数,这就意味着我们 要在 A[i] 之前的数中选取 k-1 个数。我们可以使用二项式系数公式求出 在 i-1 个数中选取 k-1 个数的方法的总数。我们把这个二项式系数记作 bin[i-1][k-1] 。
不难看出,题目要求的最终总和就是 sum ( A[i] * bin[i-1][k-1] ) sum里面i的下标范围是 k <= i <= n。
所以,现在我们需要计算所有二项式因数 bin [k - 1][k - 1], ..., bin [n - 1][k - 1] 计算的方法有很多种,最简单的办法就是使用递推公式。
bin [0][0] = 1; for (n = 1; n < MAXN; n++) { bin [n][0] = 1; bin [n][n] = 1; for (k = 1; k < n; k++) { bin [n][k] = bin [n - 1][k] + bin [n - 1][k - 1]; if (bin [n][k] >= MOD) { bin [n][k] -= MOD; } } } qsort (a, n, sizeof(long), compare); sol = 0; for (int i = k - 1; i < n; i++) { sol += ((long long) (a [i] % MOD)) * bin [i][k - 1]; sol = sol % MOD; }
注意,我们没有在计算二项式因子的时候使用%取余数运算符,因为直接作减法运算要快很多。 程序总的复杂度是 排序需要花费 O (n log n), 计算 二项式因子需要花费 O (n^2)。
另外一种计算二项式因子的算法是使用 递推式 bin [n + 1][k] = ((n + 1) / (n + 1 - k)) * bin [n][k] 同时使用 Big Integer 类型做除法。 尽管这样做也许会运行慢一点,但是这些二项式因子取余数后的结果可以事先计算一次并保存在外部文件的大表中。
还有,之前我们说过,1000000007是一个素数,你可以考虑使用 扩展欧几里得算法 (百科链接),将除法运算换成对倒数的乘法,这样可以将算法最终优化到 O(n log n)
参赛者最常见的错误出在边界数据上,即当k=1或者k=n的时候。另外的错误包括,忘记定义二项式递推的基础条件 bin[0][0] = 1,以及乘以两个大数的时候忘记使用 64位整数类型来保存。
第二题 信息安全 (35分)
现在有一个新的消息加密系统,它的工作方式如下:
对于服务器端和客户端之间的通信依赖于一段字符串K,K由M段组成,每段的长度是L,K中字符的取值范围只可能是小写字母{a, b, c, d, e, f}。另外,服务器端有个钥匙K1,客户端有个钥匙K2:
k1 = f(k) 其中,f是一个函数,它在k中随机地选择字符并把原始字符替换成?字符。 ?字符意味着那个位置可能是取值范围里的任意字母。
k2 = f(g(k)) 其中,g是一个函数,它把K中的M段做随机全排列。而f的定义跟上一段中定义相同。
你的任务是,知道K1和K2,要求解出K。 如果K有若干种可能,则字母序最小的解。 如果K无解,则输出“IMPOSSIBLE”。
官方答案:
为了解决本题,我们需要分两步走:
1. 第一个问题是: 找出是否存在一个可行解。我们需要找出 k1中的某段 和 k2中的某段 之间的关系。函数F 把随机字符替换成问号字符, 函数G 把M段做随机全排列。因此,k1中的某段在k2中也出现了,但是可能是按照不同的排列顺序。另外,由于函数F 的影响,k1的那段与k2的那段之间 可能有区别。 所以,为了找出是否存在一个可行解,我们需要检查k1的某段是否在k2中出现了,或者相反的情况。 但是对于 k1中的一段,在k2中可能有若干段都能与之对应(要考虑问号字符)。例如:
m = 2
k1 = "aaab"
k2 = "a???"
k1中的"aa"这段可能对应与k2中的“a?”或者"??"。这样k1中的这一段在k2中就存在两个候选匹配段。
要解决这个问题,我们可以使用 最大二分图匹配 算法(英文维基链接)。该算法归纳如下:所有的顶点被分在两个集合,一个集合U1代表k1中的所有段,另一个集合U2代表k2中的所有段。每个定点代表一段。定点间的关系可描述为:k2中的第j段是否是 k1中的第i段 候选匹配段。
最终,如果我们找到了一种图最大匹配,那么表示我们找到了一个解。现在没我们需要按照字典序输出最小解。
2. 第二个问题是:如果题目存在解,则需要找到字母序最小的解。这是对于参赛者来说更难解的一部分。一个简单的办法是:从左往右遍历k1中所有的问号字符,把第一个问号字符换成’a‘,验证此时是否存在一个解,如果存在解,就前往下一个问号字符,以此类推;如果不存在解,则换成'b',再接着试。
该算法最坏情况下的时间复杂度是 |候选字符的集合| * |k1| * O(最大二分图匹配), 因为候选字符的取值只可能是{a, b, c, d, e, f},所以 |候选字符的集合| = 6
本题的代码可以参见Dmytro的解题代码(她排名第三) https://fbcdn-dragon-a.akamaihd.net/cfs-ak-ash3/676632/98/332530593518613_-/tmp-/QMb6Q7
第三题 显示器上的坏点 (45分)
一块显示器,宽W像素,高H像素。上面有N个坏点。第i个坏点的位置是 (x[i], Y[i])。注意,(0, 0)是左上角,(W-1, H-1)是右下角。每个坏点的位置的计算公式如下。另外,坏点有可能会重叠(落在相同的像素点上),所以总共最多有N个不同位置的坏点。
x[0] = X
y[0] = Y
x[i] = (x[i - 1] * a + y[i - 1] * b + 1) % W (for 0 < i < N)
y[i] = (x[i - 1] * c + y[i - 1] * d + 1) % H (for 0 < i < N)
现在我们要在这个显示器上显示一张宽度为P像素,高度为Q像素的图像。图像显示的范围里不能有坏点。这叫做“完美显示”该图像。
你的任务是,已知W,H,P,Q,和坏点位置公式里的参数。要求在这块显示器上能“完美显示”该图像的位置的个数。注意,图像不能旋转(P只能对应X,Q只能对应Y)。
通俗地说一遍,一个W*H大小的白纸,上面有一些黑点。现在要在这白纸上圈出一个P*Q的矩形,矩形里不能有黑点。问有多少种圈法?
官方答案:
为了解决此题,我们创建一种新的数据结构来支持两种运算:更新(插入/删除)一个坏点 和 查询在长度为P的范围内有多少个连续的子区间。我们可以在线段树的基础上加以改进。
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶子结点。
对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。
因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。
参考阅读:
《浅谈线段树在信息学竞赛中的应用》 http://bbs.whu.edu.cn/wForum/boardcon.php?bid=160&id=1105529224&ftype=3&ap=269
这个改进的线段树(区间从 0 到 W-1)可以支持两种运算,复杂度分别为 O(logN) 和 O(1)。 代码如下:
struct node { int left, right; // left and right boundary of the interval int leftmost_dead_pixel, rightmost_dead_pixel, count; } nd[N]; int F(int left, int right) { // given the position of the left and the right dead pixel // count the different position of a continuous interval with length P return max(right - left - P, 0); } // O(logN) void update(int k, int dead_pixel_x) { update(leftchild, dead_pixel_x); update(rightchild, dead_pixel_x); nd[k].count = leftchild.count + rightchild.count; nd[k].count += F(leftchild.rightmost_dead_pixel, rightchild.leftmost_dead_pixel); nd[k].count -= F(leftchild.rightmost_dead_pixel, leftchild.right); nd[k].count -= F(rightchild.leftmost_dead_pixel, rightchild.left); } // O(1) void query() { return root.count; }
先把坏点按照Y轴上的位置排序,然后从左往右插入坏点,并不断线段区间里面count数。 程序的复杂度是 O(N log N),其中N是坏点的个数。