Python解题 - CSDN周赛第12期 - 蚂蚁的烦恼

问哥本期有幸all pass,而且用时50分钟内。不过回想起来,本期的四道题目的设计都或多或少不太严谨,或者说测试用例不够全面(后面会细说)。这样的话就极有可能造成虽然通过了测试,拿到了分数,但代码却是有缺陷的、不可靠的,无法解决实际问题。尤其对一些和算法关系不大的题目来说,通过了测试真的代表不了什么。


第一题:豚鼠排名榜

已知字符A,B,C。每个字符都有自己的权值q。现不知道权值q,只知道A,B,C的三次比较结果。输出A,B,C可能的排列顺序,如果不存在,则输出Impossible。

示例:

输入

A<B

A<C

C>B

输出ABC

分析

本题比较简单,只有三个字母,相互关系也只有三种。而ABC三个字母的排列组合也只有六种,所以哪怕是穷举,将每种组合的字母依次拿出来去比较三种关系式,最多比较 3*6=18 次就可以得到答案。

本题没有说明给出比较的结果不会重复,比如说如果给出的输入是A<B,B>A,A<C,是无法得到答案的。所以问哥觉得这是题目描述不太严谨的地方,感觉又像是从哪里翻译摘抄过来的题目。

虽然本题用穷举完全可以pass,但是如果你想玩点“花”的,可以往下看。

因为只有三个字母,可以设定三个字母的初始权重都为0,根据三次比较(两两不重复)的比较结果,较小的一方权重减去10,较大的一方权重加上10。(可以加减任意数字,这里只是举例)

  • 如果存在组合,则三个字母最终的权重一定是-20、0、20,因为每个字母都比较了两次,最小的字母比另外两个字母都小,所以减了两次10,于是权重变成-20,而最大的字母同理,加了两次10,权重变成20。中间的字母各加减了一次10,所以权重回到0。
  • 如果不存在组合,则权重必不是-20、0、20,(实际上,如果出现矛盾的组合,这样计算的结果,三个字符的权重都是0,也就是最后的权重相同。)

所以将三个字母最后的权重进行比较,如果存在相同的(重复),则表示不存在合理的组合。反之将字母按照权重从小到大排列输出即可。(针对如果出现给的输入结果出现重复的情况,则只要检查三个字符的权值是不是-20、0、20即可。)

参考代码

exp1 = input()
exp2 = input()
exp3 = input()

d = {"A":0,"B":0,"C":0}
for i in (exp1,exp2,exp3):
    if i[1]==">":
        d[i[0]]+=10
        d[i[2]]-=10
    else:
        d[i[0]]-=10
        d[i[2]]+=10
if len(set(d.values()))<3:
    print("Impossible")
else:
    print("".join(sorted(d.keys(),key=lambda x:d[x])))

第二题:字符串转换

已知两个字符串a,b。字符串b中包含数量不等的特殊符号“.”,“*”(字符串存在没有特殊符号或者全由特殊符号组成的情 况)。“.”表示该字符可以变成任意字符,“* ”表示该字符的前一个字符可以变成任意多个。现在我们想知道b可否通过特殊符号变成a。如a* 可以转化为a,aa,aaa,aaaa…如果可以,则输出“yes”,反之输出“no”。

分析

本题不记得示例了,但是看完题目就知道这题在考正则表达式。因为正则表达式里“.”就代表任意字符,而“*”也代表前面的字符可以有任意数量。所以直接把字符串b当做正则表达式的pattern,去搜索字符串a就好,如果能找到,且找到的结果与a完全一致,证明b的pattern完全等价于a,意味着可以按照题目的要求变成a,则输出yes,如果找不到,则输出no。

但是本题的测试用例依然存在不严谨的地方。最明显的,正则里的“*”是量词,必须跟在某个字符后面,但是本题的测试用例里没有考虑“*”跟在“*”后面的情况,但是却出现了“*”出现在字符头部的情况。按照这种思想,如果出现“*”跟在“*”后面的情况,应该也是输出“no”才是,比如,aaaaa和a****,用正则去搜索会报错。

不过因为会报错,也比较容易发现问题。本题只需要特判“*”出现在字符头部的情况,剩下的就可以正常用正则来pass了。

如果要判别是否存在“*”连在一起的情况,需要 O(n) 的复杂度去检查字符串,也许是为了降低难度(虽然用Python的正则来实现可以说简直没有难度),本题没有考到,这里也就不给出代码了。不过本题如果考察的是用手工实现正则表达式的匹配,或者考察更多正则里的符号,如“?![^]”,难度就上去了。有兴趣的童鞋可以试试这个系列

参考代码

a = input()
b = input()
import re
if b.startswith("*"):
    print("no")
else:
    c = re.search(b,a)
    if c and c.group()==a:
        print("yes")
    else:
        print("no")

第三题:蚂蚁家族

小蚂蚁群是一个庞大的群体,在这个蚂蚁群中有n只小蚂蚁,为了保证所有蚂蚁在消息传送的时候都能接收到消息,需要在他们之间建立通信关系。就是要求小蚂蚁都可以通过多只或者直接联系到其他人。已知几条小蚂蚁之间有通信关系,请问还需要再新建至少多少条关系?

输入:第一行n m,然后下面m行输入已存在的通信关系

示例

示例1示例2
输入

4 3

1 2

2 3

3 4

5 2

1 2

3 5

输出02

分析

本题实际上是本期里最难的,却又是最容易满分通过的,因为其测试数据存在隐含的有利条件,但是题目没有明说,那就是:

  1. 给出的每组通信关系都是左边小、右边大的;
  2. 每只蚂蚁都只有一个通信对象;
  3. 如果某只蚂蚁存在多个通信对象,则除了第一条以外都是环路(重复的)。

所以,由于存在上述隐含条件,通过此题变得极其简单,什么代码都不用写,直接给出 n-m-1 就能通过90%的测试用例。剩下的10%对应了第三条,存在环路,只要把通信关系左边的数字去重,得到新的个数m1,然后n-m1-1,就通过了。

参考代码1

通过本题所有测试的代码如下:

n, m = map(int, input().split())
m1 = set()
for i in range(m):
    m1.add(int(input().split()[0]))
print(n-len(m1)-1)

但是,正如问哥开头所说的,通过测试真的代表不了什么,因为该代码是有缺陷的!这是因为本题使用的测试用例很不严谨,存在了上述几条隐含的有利条件,使得问题变简单了,也漏掉了一些本可以找到正确答案的情况。比如,如果输入如下:

5 4

1 2

1 3

1 4

1 5

5只蚂蚁,存在4个通信关系,很显然,所有蚂蚁都可以通过1号蚂蚁与其它任意一只蚂蚁建立通信,所以需要增加的通信关系为0,也就是不需要增加通信关系,但是用这个代码是得不到正确答案的

深入思考

说回思路,本题很显然考察的是无向连通图。根据书本上关于连通图的基本知识,n个节点的连通图中至少存在n-1条边,如果存在多于n-1条边,则必存在环路。

比如上图里1、2、3都是连通图的形式,而图4则存在环路,导致节点4和5形成了“孤岛”,虽然总边数相同(都是n-1条),但却没有连通。

于是,如果没有环路的情况下,n-1(条边)减去m(条边)就是本题的答案,也就对应了本题测试数据里90%的情况。而剩下10%因为存在环路,所以要对m(条边)先去重。又因为本题测试数据里每个节点向下都只有一棵子树(隐含有利条件2),所以对节点去重就可以消除环路。

由于本题的测试数据只考察了图1和图4这两种情况,所以上面的代码就可以通过测试。可是如果测试数据包含了图2或图3这种情况,该怎么解题呢?

这种情况相对就要复杂不少,而且在比赛中既然已经all pass了,问哥就没有再去思考,所以后面这些讨论都是在赛后总结出来的,于是也没有去验证代码。这里问哥给出一种解法,是否准确还有待检验,但也算是一种思路吧。

由于可能存在多对多的边关系,比如1号蚂蚁和2/3/4/5号蚂蚁都有通信关系,这样的话使用上面的代码就没办法去除可能会形成环路的边了,实际上如果存在这种通信关系,想要去查找所有可能形成环路的边常常要深度搜索,并不轻松。

于是我们不妨换个思路,假设蚂蚁根据给出的通信关系组成一个个部落,而每个部落又相当于图里的一个节点(n个部落),所以只需要再添加部落个数减一(n-1)条边,就可以把节点全连通了,而只要节点之间连通,部落之间也就完全连通了。这时要注意的是,有可能有的蚂蚁不存在通信关系,所以单个蚂蚁就形成了自己的部落,这时也要参与到新的部落个数中来,所以最终的答案就是:部落个数+落单的蚂蚁个数-1

这样,我们也就无需去检查给出的哪些边是多余(形成环路)的了,因为多余的边(比如上图里的1--3)连接的本身就是部落里已经存在的蚂蚁,所以不会对部落的大小产生影响。

所以,根据上述思路,选择Python的集合来完成部落的建立非常适合,因为集合自动去重,时间复杂度低(in的查找为O(1))。

做法是这样:假设至少存在一对通信关系,则先将第一对通信关系的两只蚂蚁放进一个部落(集合)里,因为可能存在多个部落,所以将已存在的部落放进列表里。然后循环接收后面的通信关系,每接收到一对通信关系,就将列表中已存在的集合拿出来检查是否包含新的通信关系里至少一只蚂蚁,如果是,则将集合合并,如果不是,则已存在的集合再重新添加进列表,完成所有集合的比较后,再将新蚂蚁的集合添加进列表(这里使用的数据结构是队列,先进先出,所以如果在意时间复杂度的话,最好用python的内置deque来实现)。最后,再检查是否存在落单的蚂蚁(将所有蚂蚁的集合减去已存在的部落,得到的差集就是落单的蚂蚁,蚂蚁的总数减去列表里所有部落蚂蚁的个数,就是落单的蚂蚁),最后计算部落的个数加上落单蚂蚁的个数减去一。不考虑列表操作的情况下(或者使用队列,入队出队为常数级),时间复杂度为O(nm)未经检验,仅供参考

参考代码2

n, m = map(int, input().split())
res = [set(map(int, input().split()))]
for _ in range(1, m):
    a, b = map(int, input().split())
    new = {a,b}
    for _ in range(len(res)):
        temp = res.pop(0)
        if a in temp or b in temp:
            new.update(temp)
        else:
            res.append(temp)
    res.append(new)
single = n-sum(map(len, res))
print(len(res)+single-1)

12.06更新

看到有其他的选手在社区反馈本题测试数据里蚂蚁的编号存在大于n的情况,如果题目有意为之的话,说明此题的蚂蚁并不是按从1到n,而是任意n个不相同的数字来表示,所以就不能用二维数组来表示图的邻接矩阵了。上面的代码计算不同的蚂蚁部落没有问题,但最后计算落单的蚂蚁就不能使用差集了,而是直接减去所有部落里蚂蚁的数量(都是不同的蚂蚁),得到的就是落单蚂蚁的数量了(我们不必知道具体是哪些编号的蚂蚁落单了),所以倒数第二行的代码已按此修改。


第四题:小股炒股

已知n天后的股票行情,现在已有的本金是m,规定只能入手一次股票和抛售一次股票。最大收益(含本金)是?

输入:第一行n m,第二行n个整数表示每天的股票行情

示例:

示例1示例2
输入

4 9

7 5 6 3

6 7

4 1 2 9 10 3

输出1070

分析

示例的具体数字我不记得了,大概就是这个样子,主要是为了更好的解释题目的意思。

这题其实没什么可说的,没什么难度。题目也很友好地把难度降低了:只能买、卖一次。所以只要遍历行情列表,通过比较不同买入点和卖出点的收益,找出最大收益时的买点和卖点即可,时间复杂度是O(n^2)。必定存在更优化的方案,但在比赛的时候来不及细想,我记得题目提示了n\leqslant 1000,所以穷举完全可以pass。

要注意的是,因为卖出的时间点必在买入之后,所以内循环只要从买入那天的隔天开始,到最后一天结束即可。

参考代码1

n, m = map(int, input().split())
arr = list(map(int, input().split()))
res = m
for i in range(n):
    buy = arr[i]
    for j in range(i+1,n):
        sell = arr[j]
        if sell > buy:
            profit = (m//buy)*sell + m%buy
            res = max(res, profit)
print(res)

此题也存在测试数据的缺陷,比如代码在比较的过程中,只查找卖出和买入价格的最大比值(不计算最终收益),然后最后再通过该比值计算收益的话,也能通过测试。 但是如果数据是下面这个样子,两种方法得到的结果就不同了。

4 9

5 10 3 5

第1天买入、第2天卖出的价格比值为2,要大于第3天买入、第4天卖出的价格比值1.67。但是显然第3天买入、第4天卖出的收益(15)要高于前者(14)。这说明代码的测试数据存在考虑不全的情况,完全有机会使用不健全的代码“侥幸”通过测试。但是话说回来,存在即合理,谁能说这不是题目难度的一部分呢。

补充思考

今天有空出门喝了点西北风,脑子清醒不少,思考了最后一题O(n)的算法,思路如下:

对于股票价格严格递增的情况,自然只有一个答案:最低点买入,最高点(最后一天)卖出;另一个极端,股票价格严格递减的情况,则最大收益就是本金——无买入点;而对于其他情况,一旦找到了买入点,后续的价格走势只有下图四种情况(或组合):A代表前一个价格低点(买入点),B代表前一个价格高点(卖出点),C代表后一个价格低点(在卖出点之后出现),D代表后一个价格高点。

情况1和3的后一个低点C比前一个低点A还要低,2和4则要高;而情况2和3的后一个高点D比前一个高点B要高,1和4则要低。

于是我们可以分析一下这四种情况的计算方式,试着找出共性。具体做法如下:

1. 以第一天的价格作为可能的买入点buy,卖出点sell设置为-1(表示手上没有股票),然后开始遍历价格列表(O(n) 的基本做法);

2. 当遇到新的价格比buy要高,则比较新的价格与sell,如果新的价格比sell高,则更新sell为新的卖出价格(买入点不变,自然要找最高的价格再卖);

3. 如果新的价格比buy要低,先要看sell是否存在,如果不存在(说明手上没有股票),则将新的价格设定为买入价格buy(找最低点买入),如果sell已存在,则要先平仓(按照前面的低点买、高点卖),把sell设置为-1(表示手上没有股票),然后再将新的价格设为新的买入点buy。同时在平仓的过程中,比较最大利润是否发生变化;

4. 最后,如果价格循环结束,高点sell却大于0(不是-1),则表示手上还有股票,还要再做一次平仓操作,然后比较最大利润是否发生变化。

参考代码2

n, m = map(int, input().split())
arr = list(map(int, input().split()))

res = m
buy = arr[0]
sell = -1
for i in arr[1:]:
    if i < buy:
        if sell:
            profit = sell*(m//buy)+m%buy
            res = max(res, profit)
            sell = -1
        buy = i
    elif i > buy:
        sell = max(sell, i)
if sell > 0:
    profit = sell*(m//buy)+m%buy
    res = max(res, profit)
print(res)

1月8日补充

上面的思路还是太冗长了,秉持着“低买高卖”的最朴素的真理,存在更加简洁的 O(n) 的算法,更新代码如下:

n, m = map(int, input().split())
arr = list(map(int, input().split()))
res = 0
buy = arr[0]
for i in arr[1:]:
    if i > buy:
        res = max(res, i*(m//buy)+m%buy)
    else:
        buy = i
print(res)

早在半年前就在力扣刷过类似题,没想到换个样子就不认得了,唉。。。 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

请叫我问哥

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

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

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

打赏作者

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

抵扣说明:

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

余额充值