Python解题 - CSDN周赛第14期 - 单词编码

本期其实没啥好写的,都是数学题,和算法关系不大,唯手熟尔。而且又出现了同一天的每日一练中包含了赛题,这算不算官方泄题呢?看来下次在竞赛之前先做完每日一练大有益处呢。


第一题:字符串全排列

对K个不同字符的全排列组成的数组,面试官从中随机拿走了一个,剩下的数组作为输入, 请帮忙找出这个被拿走的字符串?

比如[“ABC”, “ACB”, “BAC”, “CAB”, “CBA”] 返回 “BCA”

第一行输入整数nn=K!-1),下面n行,每行输入一组剩下的字符串组合。

示例1
输入

5

ABC

ACB

BAC

CAB

CBA

输出BCA

分析

一般来讲第一题都是简单题,所以本题虽然使用全排列查找的算法复杂度是O(n!),但因为数据范围不大,依然可以pass。于是只要穷举出输入字母的所有组合,然后找出少了哪一组,就能得到答案。Python可以直接使用内置的排列组合函数permutations,速度会快上不少。

本题n可以等于0,也就是说K=1,一个字母的组合,那自然是什么都不缺,所以特判,输出一个空字符串既可。

参考代码1

n = int(input())
vector = [input().strip() for _ in range(n)]
if n == 0:
    print("")
else:
    from itertools import permutations
    res = set()
    for i in permutations(vector[0]):
        res.add("".join(i))
    res.difference_update(set(vector))
    print(res.pop())

优化

然鹅,虽然通过了,我们不应该就此满足。仔细观察一下,就能很快找出本题的“窍门”:

如果K个字母的组合没有缺失(总共有K!个组合),那么这些组合从左到右的每个位置上,每个字母都出现了K-1次。比如ABC三个字母的全排列组合ABC,ACB,BAC,BCA,CAB,CBA,第一位出现的是AABBCC,第二位出现的是BCACAB,第三位出现的是CBCABA每个位置的每个字母都出现了两次。于是,如果缺少一组的话,那么只要检查所有组合每位上的字母,出现次数最少的就是答案。这里需要加一个特判,当只有两个字母的时候(n=1),由于只有两种组合,所以只要翻转过来输出字符串即可。

参考代码2

n = int(input())
vector = [input().strip() for _ in range(n)]
if n == 0:
    print("")
elif n == 1:
    print(vector[0][::-1])
else:
    result = ""
    for i in zip(*vector):
        result += min(i, key=i.count) # 找出出现次数最少的字母
    print(result)

第二题:小Q新式棋盘

已知棋盘大小为n*n。每个位置都有自己的权值q。该棋盘中有多少对行权值和小于列权值和?

示例1示例2
输入

3

1 2 3

1 2 3

1 2 3

3

4 5 6

2 3 4

3 2 1

输出35

分析

本题只记得第一个示例,但原理很简单,就是分别计算每行、每列的数字之和,然后比较有多少行的数字之和,小于列的数字之和。作为第二题,数据范围自然也不大,所以O(n^2)嵌套循环的暴力穷举算法就能pass。

参考代码1

n = int(input())
vector = [list(map(int, input().split())) for _ in range(n)]
row = [sum(i) for i in vector]
col = list(map(sum, zip(*vector)))
result = 0
for i in range(n):
    for j in range(n):
        if row[i] < col[j]:
            result += 1
print(result)

优化

同样,我们不能就此满足,本题其实还有更快的算法。先对行权值和列权值进行排序,然后从小到大依次比较,找到行权值比列权值小的位置,然后累加剩下的列权值个数即可。

拿示例二举例,我们计算行权值得到列表[15, 9, 6],排序得到[6, 9, 15],计算列权值得到列表[9, 10, 11],比较两个列表的第一个数字,6小于9,所以累加列权值列表9所在位置(包括9)往后的所有数字的个数3(因为9在第一位,所以如果小于第一个9,那么就代表着所有比它后面所有的数都小),然后再比较行权值列表的第二个数字9,因为不小于列权值列表的第一个数字9,所以比较第二个数字10,由于9小于10,所以累加10后面的数字之和2(包括10自己),答案是5。

如果不算计算行、列权值之和的时间复杂度的话,sort排序的时间复杂度是O(nlogn),比较两个等长列表的时间复杂度是O(n)(其实是2*n,但是惯例舍去系数),所以整体的时间复杂度是O(nlogn)(只保留最高位)。

参考代码2

n = int(input())
vector = [list(map(int, input().split())) for _ in range(n)]
row = sorted(sum(i) for i in vector)
col = sorted(map(sum, zip(*vector)))
i = j = 0
result = 0
while i < n and j < n:
    while j < n and row[i] >= col[j]:
        j += 1
    result += n-j
    if j == n: break # 所有行权值都比列权值大,就没必要继续比下去了
    i += 1
print(result)

第三题:因数-数字游戏

小Q的柠檬汁做完了,掏出了自己的数字卡牌,想要和别人做数字游戏,可是她又不想要输掉游戏。她制定好规则,每次每个人只能把这个牌换成它的因子的某个牌,但是这个因子不能是1或者整数本身。现在给出整数n,两个人开始做游戏,谁无法再给出因子牌则该人胜利。如果该整数无因子牌直接视为先手胜利。请判断先手在最优策略状态下能否必胜,如果能则输出1,不能则输出2。

示例

示例1示例2
输入

6

30

输出21

分析

本题出现在了同一天的每日一练中。。。官方泄题嫌疑。

描述看着十分复杂,无牌可出竟然算胜利,以至于计算的过程中老是绕不过来弯。。。其实这道题非常简单。题目已经说了“这个因子不能是1或者整数本身”,这不摆明就是质数(素数)嘛。所以把题意换个说法就是,剩下一个质数(素数)给对手,对方就获胜了。。。(还是很别扭)。结合输出条件(必胜输出1,反之输出2),可以得出以下条件:

  1. 如果该数是质数(素数),小Q胜,输出1(起手无牌可出)
  2. 如果该数有2个质因数,小Q输,输出2(因为小Q只能打出一个质因数,剩下另一个质数给对方)
  3. 如果该数有3个或以上的质因数,小Q胜,输出1(因为小Q可以打出任意两个质因数的乘积替换该数,把上面第二种情况留给对手)

所以,就是简单的判断分支结构。顺便提一句小技巧,计算某数是否是质数(素数)的时候,只要依次试除到该数的平方根即可。

参考代码

n = int(input())
if n < 4: # 如果该数是1、2、3就不用比了,无牌可出
    print(1)
else:
    for i in range(2, int(n**0.5)+1):
        if n%i == 0:
            p = []
            while n > 1:
                while n%i == 0:
                    n //= i
                    p.append(i)
                if len(p) > 2: # 如果有3个以上的质因数,先手胜
                    print(1)
                    break
                i += 1
            else:
                print(2) # 只有2个质因数,比如6,后手胜
            break
    else:
        print(1) # 该数是质数,起手胜

第四题:编码

编码工作常被运用于密文或压缩传输。这里我们用一种最简单的编码方式进行编码:把一些有规律的单词编成数字。字母表中共有26个字母{a,b,…,z},这些特殊的单词长度不超过6且字母按升序排列。把所有这样的长度相同的单词放在 一起,按字典顺序排列(a...z,ab...az,bc...bz....)一个单词的编码就对应着它在整个序列中的位置。你的任务就是对于所给的单词,求出它的编码,如果没有则输出0。

示例:

示例1示例2
输入

ab

glq

输出271882

分析

本题稍微有点烧脑,但难度并不高,只是比较麻烦。因为“单词长度不超过6”,所以即使穷举,列出所有单词,相信也能过(最后一个单词uvwxyz的编码是313911,所以编码范围就是1到313911)。而本题可以编码的单词字母不允许重复,也必须是按照字母升序排序,所以加个特判,如果输入单词有重复字母,或者不是升序排列,则输出0(就不需要穷举才能证明找不到了)。

但是问哥喜欢找规律,所以这题不满足于穷举(这题明摆着是计算题啊),于是在草稿纸上画出下图(省略右边e到z的列):

所有单词的排列顺序应该如上图所示,不难发现,每个表格中的单词数量都等于其上一行右边一列算起的个数之和。比如第二行第一列a开头的所有两位单词的个数,就等于第一行第二列字母b及后面字母的个数,也就是25。而第三行第一列a开头的所有三位单词个数,等于第二行bc开始的单词个数,加上cd开始的单词个数,加上de开始的单词个数。。。加上yz开始的单词个数。第四行同理。。。

所以,如果本题算做动态规划的话,把上面二维表的每个格子视作某个字母(列)开始的n位单词(行)的个数,可以得到方程如下:

dp[i][j] = sum(dp[i-1][j+1:])

只有第一行,也就是一位字母的那一行,需要全部设置为1,这样就可以通过上面这个公式得到n位单词的一张个数表,而我们要计算单词的排序(编码),只要查表计算就能完成。

计算方法是:从左向右依次取出字母,通过查表找到以该字母开始的n位单词的个数,然后加上所有在它之前的同样长度的单词个数,然后重复此过程,直到最后一个字母。如果设每一位字母在字母表中的位置(顺序)为a[n],则可得公式如下(公式表达不一定准确,以描述为准 :P):

\sum_{i=0}^{n}sum(dp[n-i-1][:a[i]])

此公式的数学证明比较麻烦,这里就略过了(其实也比较简单,就是统计某个字母开始的第一个单词前面出现了多少单词),但即便不用证明,单纯的观察就可以得到上述规律。

拿示例二的单词glq为例,我们根据前面的制表公式可以得到下面这张表:

然后我们依次查找第一位字母g所在的列,累加它所在行它之前的所有数字(包括它自己),也就是图中从300加到171,然后再取第二位字母l,累加它所在行之前的数字(从25加到14),最后再取第三位字母q,累加它之前的所有数字(17个1),最后得到的结果就是答案:1882。

参考代码

word = input().strip()
n = len(word)
if "".join(sorted(list(word))) != word or len(set(word)) != n:
    print(0)
else:
    dp = [[0]*26 for _ in range(n)]
    for i in range(26):
        dp[0][i] = 1
    for i in range(1, n):
        for j in range(25):
            dp[i][j] = sum(dp[i-1][j+1:])
    result = 0
    for i in range(n):
        a = ord(word[n-i-1])-ord("a")+1
        result += sum(dp[i][:a])
    print(result)
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

请叫我问哥

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

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

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

打赏作者

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

抵扣说明:

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

余额充值