CSDN周赛64期题解(含部分代码)

《计算之魂》主题周赛如期回归。因为差不多每次都是新题,让人多了点期待。

相信非编程题无需多言,答案都在书里。——翻书翻得快,满分无障碍。当然,如果提前读过此书就更好了。比如原书中把金块切了 2 刀,问题中扩展了一下,变成切 9 刀,如果提前理解过原书相应章节,套用这个思路模板就可以更快地找出正确答案。(注:答案是 2^{n+1}-1

废话不多说,着重讲讲编程题。本期貌似只有我全部 AC 了(-_-||)。


第一题 - 小球游戏

某台有10个小球的游戏机,其设定的规则如下:每一轮游戏在开始之前会把编号为0到9的小球依次放入从左到右编号也为0到9的10个位置;游戏开始后会快速对调任意两个球的位置若干次,并在结束时要求观众写出从左到右的小球编号顺序,写对就得奖。由于速度很快,所以直接靠观看写对很难。但有个程序员发现这台游戏机其实有一个固定的长度为n的操作序列数据库,每一轮游戏都是随机取一个起始操作序列编号和一个结束操作序列编号(操作序列编号从1到n)并从起始到结束依次执行每个操作序列编号对应的操作,而每个操作序列编号对应的操作就是对该次操作指定的两个编号的位置上的小球进行对调。
现在给出操作序列数据库和每一轮游戏的起始操作序列编号和结束操作序列编号,求每轮游戏结束时从左到右的小球编号顺序。

输入描述:第一行两个正整数n和m,表示操作序列的长度和游戏轮数。接下来n行,每行两个非负整数a和b,表示每个操作序列指定对调的两个小球所在位置的编号。接下来m行每行两个正整数c和d表示该轮游戏中的起始操作序列编号和结束操作序列编号(c和d都是从1到n之间的正整数且c<d)

输出描述:m行,代表每轮游戏结束时的小球编号顺序

输入样例

5 3
0 1
1 2
2 3
0 1
9 0
3 3
1 5
3 4

输出样例

0 1 3 2 4 5 6 7 8 9
9 1 3 0 4 5 6 7 8 2
1 0 3 2 4 5 6 7 8 9

解析

其实没啥可解析的,大水题。纯模拟,题目怎么说,你就怎么做,和算法没什么关系。考察的点可能在于基础的编程能力,也就是用代码语言描述现实的能力。只要看懂了题目,AC应该不是问题。

我能想到的唯一可能会疏忽的点在于,m 表示游戏轮数,每轮开始小球都会回复原位,并不是基于上一轮的结果进行操作。当然,如果读题比较仔细的话,这个坑应该也不会跳。

直接放代码了,本题不存在什么特殊技巧,代码雷同率应该极高。下次如果再遇见,简单复制过去的话可能会有被判抄袭的风险。

代码
n, m = map(int, input().split())
q = list()
for _ in range(n):
    a, b = map(int, input().split())
    q.append((a, b))
for _ in range(m):
    balls = list(range(10))
    c, d = map(int, input().split())
    for i in range(c-1, d):
        a, b = q[i]
        balls[a], balls[b] = balls[b], balls[a]
    print(*balls)

第二题 - 编号分组

现欲对各有正整数编号的n个人进行分组,规定同组中不得出现两人编号乘积之开立方为正整数的情况,求构成最大分组的人数。

输入描述:单行n个数字表示各人的编号(0<n<100000,每个数字的范围在1到2000000000之间)。

输出描述:构成最大分组的人数d(d>=1)

输入样例

27 4 2 16

输出样例

3

槽点

题目描述有点蹩脚,更通俗点的描述是:给定 n 个正整数,从中取出 d 个,要求这 d 个整数中任意两个整数乘积都不是某个整数的立方数。问:满足条件的取法中,d 最大是多少。

C 站的一贯特色:阅读理解的能力水平直接决定了最终能否 AC。

另一个特色就是残缺错误的考试用例,因为我有九成九的把握,本题第一个考试用例是错的。

下面是我把第一个考试用例提供的 1971 个数字进行降序排列,列出的最大的前十个数字:

19997571080115729828351060605
1811932342261311014930513
79428554199541910392356
149305334390088916727
29825365379515640719
18834122460230394981
15074763325549425778
8718860087271581977
8070332308957594232
2116262505797211521

这一刻,题目描述里的“每个数字的范围在1到2000000000之间”,赫然在打脸。而且,在这 1971 个数字里,大于 2000000000 的数字共有 131 个。

基于本题考察的数论特性,需要大量计算,面对如此大的天文数字(1e29),任何一种算法都会超时,这还没有考虑到大数本身的存储和计算问题。(顺便说一句,即使对这些超出 2000000000 的数字取余,或者模拟溢出,也得不到正确答案。)

所以我猜大部分选手都卡在了这一步。面对超时的反馈,不禁开始怀疑自己,不断地推到重来。。。最终放弃。

然鹅,巴特!如果你绕过了第一个用例,或者像我一样狗屎运地试出了它的正确答案,继续往下走,你就会发现,后面九个用例的输入数据都是正常的!(0<n<100000,每个数字的范围在1到2000000000之间)

所以我深度怀疑,第一个用例的输入数据中,遗失了一些空格,导致一些数字粘连在一起,变成巨大数。(不过由于本题没有事先给定数字个数,所以无从佐证。)

下面在给出我的解法之前,先给出本题使用的后面几个比较短的用例,大家可以先自己试试看:

- 第 8 例
input: 1 1 1 8 8 6 48 162 36
ouput: 4
- 第 10 例
input: 2 32 3 72 5184 26873856 13436928 6718464 3 9
ouput: 6
- 第 7 例
input: 6181217 140813 14402309 1842561 404594 22853 2723749 4874462 142353 2592 372407 163426 184732 515087 113172 19341559 315320965 35234304 1417385 113345611 182919 3148651 21043121 577150 14731162 197971 1901323 1252246 91001 54169057 14155 8521727 7720021 102837581 22918894 2507140 12489 107130829 40591 6572626 2379539 1555322 18178705 591641 3383853 1783360 43793318 5513356 22221665 263398 514807 1381393 826580 2608088 338741 191828911 74086351 145747762 31583665 869925 175742 18938851 185231 3176607 869849 2182073 1919778 1432897 7954322 2506076 6373205 16178715 126855 791745 8226554 29422975 439867 4054699 692325 3208824 774283 416989 3367256 1504792 86947 2059245 28227580 1739820 71849473 16577676 750282 12572333 2588865 1781554 137800 1352421 130592185 88873 5641203 30205801 5193933 3111945 4626080 26572 142132321 923 41668 17380901 75192 319344 40045921 227699 204155535 46768 4106 426909 26444111 2199986 550385 349083 45784063 26304993 9620 42727897 612451 204602961 9505565
ouput: 127

其中第 7 例的答案就等于数字的个数,说明本身给出的这些数字里任意两个数的乘积都不是某个整数的立方数。

解析

本题偏数论。鉴于本人学识有限,在此仅分享我能想到的解法,也及其欢迎有朋友分享其他解法可供学习。

在排除掉前面的数据问题后,我们来看看这道题有什么思路。为了方便描述,下面的文字里我自作主张,定义“两个数的乘积为某个整数的立方数”的这种情况为——这两个数互相“配对

- 思路一

第一步,双循环穷举这 n 个数,找出那些能够配对的数对,剩下的整数与任何其他数字都不配对,自然可以放在一起。此过程的算法复杂度为 O(n^{2})。(实际为 n*(n-1)/2 次计算)

第二步,还要在那些能够配对的整数里,找出“最少去除多少数字,使得剩下的数字都不能配对”。

很显然,将这两部分的结果加在一起就是答案。

但是稍作分析,如果我们把 n 个数字类比成节点,能够配对的两个数相当于两个节点之间有一条边。那么通过第一步,我们就可以构建一张无向图,而第二步就相当于在这张无向图中找到最大的独立集——这是图论中一个比较经典的 NP 难问题。简单地说,就是用目前已知的任何多项式算法都无法找到准确答案,只能通过贪心这种近似算法找到局部最优答案。

额,我不太相信 C 站会出这种难度的问题,所以直接放弃了这种看似荒唐的想法。而且构建无向图本身也无太多意义,没有利用到数字本身的数学特性。(不过赛后自己复盘的时候,使用这种思路,加上贪心算法也是可以得到正确答案的。)

其实该思路最大的问题还在于复杂度太高。0<n<100000,复杂度达到 1e10 级别,这还没有考虑在判断两数是否配对时所花费的时间,以及第二步中贪心算法耗费的时间(贪心的过程可能耗费时间更多)。

- 思路二

我们不妨先观察一下当两个数能够配对时,数字本身的结构是什么样子的,以及题目使用立方根作为配对条件的意义。

我在之前题解的文章里曾不止一次提到过,所有的整数都可以进行质因数分解,写成诸如 a^{x}b^{y}c^{z} 的连乘形式,其中 a,b,c 是不同的质数。如果这个数是某个整数的立方数——或者按题目的说法,对它开立方得到的是正整数。那么就说明上述形式中的 x,y,z 分别都能够被 3 整除。

那么,如果两个数能够配对(乘积是某个整数的立方数),用上述形式进行表示即

a^{x_{1}}b^{y_{1}}c^{z_{1}}*a^{x_{2}}b^{y_{2}}c^{z_{2}} = a^{x_{1}+x_{2}}b^{y_{1}+y_{2}}c^{z_{1}+z_{2}}

其中,x_{1}+x_{2},y_{1}+y_{2},z_{1}+z_{2} 分别都能够被 3 整除。

注意到上面的表示形式还可以继续进行分解,假如我们把某个整数的立方数分离出来,变成下面这种形式(n 可以为 0):

a^{x_{1}+x_{2}}b^{y_{1}+y_{2}}c^{z_{1}+z_{2}} = (a^{3n}b^{3n}c^{3n})*(a^{(x_{1}+x_{2})mod3}b^{(y_{1}+y_{2})mod3}c^{(z_{1}+z_{2})mod3}) = (a^{3n}b^{3n}c^{3n})*(a^{x_{1}mod3}b^{y_{1}mod3}c^{z_{1}mod3})*(a^{x_{2}mod3}b^{y_{2}mod3}c^{z_{2}mod3})

不难看出,任意两个数字相乘,都可以表示为上面这种形式(当然质数的个数可能不一样)。

由于我们最终要放在一组的数字,是不能够配对的,所以对于这个连乘形式最左边括号里的 a^{3n}b^{3n}c^{3n},我们可以直接忽略,而将重点放在右边的 a^{(x_{1}+x_{2})mod3}b^{(y_{1}+y_{2})mod3}c^{(z_{1}+z_{2})mod3},只要这部分的结果不为 1,这两个数字乘积开立方就不可能是整数——它们自然可以放在一起。而这部分结果不为 1 的充分必要条件是,(x_{1}+x_{2})mod3, (y_{1}+y_{2})mod3,(z_{1}+z_{2})mod3 分别都大于 0。

很显然,某个数字对 3 取模的结果只有三个:0、1、2,而这里我们要使上面的指数乘积结果大于 0,x_{1}mod3 和 x_{2}mod3 就必须相等。—— 因为 x_{1}mod3 要么为 1,要么为 2(为 0 的情况已经在上一步划到左边的 a^{3n}b^{3n}c^{3n} 里了),x_{2}mod3 也是一样。如果两者都为 1,结果就是 2;如果两者都为 2,结果为 4,再模 3 还是得到 1。但是如果一个是 1,一个是 2,结果为 3,模 3 就变成了 0。(y_{1}mod3 和 y_{2}mod3z_{1}mod3 和 z_{2}mod3 的情况也一样。)

这里“要么为 1,要么为 2”,正好对应了本章的主题——“二进制编码”。后面也确实可以使用二进制编码来进行优化。——这或许就是本题使用立方根作为配对条件的意义。

另一方面,如果两个数字在分别剥离了 a^{3n}b^{3n}c^{3n} 后的质因数不完全相同,比如 4 和 6,前者质因数是 2,后者是 2、3,那么这两个数也是不可能配对的——相当于 x_{1}mod3+0 。

OK,我们再来从头捋一下思路,考虑如何用代码实现:

第一步:对每个整数进行质因数分解,在分解的同时:

  1. 剥离出 a^{3n}b^{3n}c^{3n} 的部分(原因前面已提到,简单来说就是因为这部分在后面判断两数是否配对时,并不能影响结果),使其变成 a^{x_{1}mod3}b^{y_{1}mod3}c^{z_{1}mod3} 的形式。
  2. 对每个 a^{x_{1}mod3}b^{y_{1}mod3}c^{z_{1}mod3} 形式的整数进行编码,使用指数的二进制编码形式。比如 300 = 2^{2}3^{1}5^{2},对应的编码就是“101”,而 60 = 2^{2}3^{1}5^{1} 对应的编码就是“100”。
  3. 把具有相同质因数的整数放在一起。比如 300 = 2^{2}3^{1}5^{2} 和 60 = 2^{2}3^{1}5^{1} 就可以放在一起,但是不能和 100 = 2^{2}5^{2} 放在一起,因为它们质因数不完全相同。

可以看到,经过分组后的整数,组与组之间存在“生殖隔离”——不同组的任意两个数是不可能配对的。所以下一步,我们只要检查每个组中最多可以拿出多少个不能配对的数。

第二步:对每组的数字进行检查,存在以下情形和规则:

  1. 该组数字都是 1 ——说明这些数字在进行第一步的处理之前,全是立方数。也就意味着,这组数中,最多只能拿出 1 个数。因为立方数和立方数相乘,必然还是立方数。
  2. 对于某组含有相同 k 个质因数的数字,最多只有 2^{k} 种编码。根据前面分析的“不能够配对”原则,编码“101”的数字与编码“10”的数字可以配对(每个二进制位都不同)——可以配对的编码存在明显的特点。然后对每对可以配对的编码,取其中包含更多数字的编码所含数字。
  3. 如果某组中只含有一种编码,说明这些数字原本就相互无法配对,所以可以全部取出。
代码(部分)
arr = list(map(int, input().split()))
from collections import defaultdict, Counter
factors = defaultdict(list)
for i in arr:
    k, v = prime_factors(i) # 建议自行完成第一步
    factors[k].append(v)
res = 0
for k, v in factors.items():
    if k == 1: res += 1
    else:
        c = Counter(v)
        s = 2**len(k)-1
        for i in range(s//2+1):
            res += max(c[i], c[s-i])
print(res)

上述代码中,我没有给出实现第一步的质因数分解、编码、分组的部分,但其实并不难,大家可以自己尝试一下。

该算法的时间复杂度为 O(n*\sqrt m),m 为数列中每个数字的范围。根据题目描述,0<n<100000,每个数字的范围在1到2000000000之间,实际的理论时间复杂度在 1e9 左右。好在本题后面的测试用例都不算太大—— 最多只有 12872 个数 —— 才得以侥幸过关。

鉴于时间有限、以及个人思维的局限性,上述内容未必完备。倘若存在更优的第三种思路,还请路过的朋友不吝赐教。

至于怎样通过第一个错误用例,继而全部 AC?——秘密。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

请叫我问哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值