🎁2020第十一届蓝桥杯python组国赛真题
🏆国赛真题目录
文章目录
- 🎁2020第十一届蓝桥杯python组国赛真题
- 试题A.美丽的2⭐️
- 🍰1.题目
- 👑2.思路分析
- 💯3.代码实现
- 试题B.合数个数⭐️
- 🍰1.题目
- 👑2.思路分析
- 💯3.代码实现
- 试题C.阶乘约数⭐️⭐️⭐️
- 🍰1.题目
- 👑2.思路分析
- 💯3.代码实现
- 试题D.本质上升序列⭐️⭐️⭐️
- 🍰1.题目
- 👑2.思路分析
- 💯3.代码实现
- 试题E.玩具蛇⭐️⭐️⭐️
- 🍰1.题目
- 👑2.思路分析
- 💯3.代码实现
- 试题F.天干地支⭐️
- 🍰1.题目
- 👑2.思路分析
- 💯3.代码实现
- 试题G.重复字符串⭐️⭐️⭐️
- 🍰1.题目
- 👑2.思路分析
- 💯3.代码实现
- 试题H.答疑⭐️⭐️⭐️
- 🍰1.题目
- 👑2.思路分析
- 💯3.代码实现
- 试题I.补给⭐️⭐️⭐️⭐️⭐️
- 🍰1.题目
- 👑2.思路分析
- 💯3.代码实现
- 试题J.蓝跳跳⭐️⭐️⭐️⭐️⭐️⭐️
- 🍰1.题目
- 👑2.思路分析
- 💯3.代码实现
试题A.美丽的2⭐️
🍰1.题目
美丽的2
👑2.思路分析
难度:⭐️
标签:枚举&字符串
🎇思路:暴力法
🔱思路分析:
本题为填空题,只需要遍历 1 → 2020 1→2020 1→2020即可:
- 将数字转为字符串 ( s t r ) (str) (str)
- 如果 ′ 2 ′ '2' ′2′ 在字符串 s s s中,则结果 + 1 +1 +1
💯3.代码实现
cnt=0
for i in range(1,2021):s=str(i)if '2' in s:cnt+=1
print(cnt) # 563
输出结果:
试题B.合数个数⭐️
🍰1.题目
合数个数
👑2.思路分析
难度:⭐️
标签:枚举
🎇思路①:试除法
🔱思路分析:
本题为填空题,因此可以直接使用暴力法得出结果
- 由于 1 1 1和 2 2 2均为素数,所以只需遍历 3 → 2020 3→2020 3→2020
- 当遍历到一个数时,我们对其进行判断:
①用 2 → x 2→\sqrt{x} 2→x进行试除
②如果整除,则说明有除了 1 1 1和本身外的其他因子存在,为合数;否则,为素数- 返回值为 1 1 1,则为合数, c n t + 1 cnt+1 cnt+1
🎇思路②:素数筛
🔱思路分析:
对于任意一个素数,它的正整数倍 ( ≥ 2 ) (≥2) (≥2)一定是合数
- 构造 v i s vis vis数组,先初始化 v i s vis vis为 T r u e True True( T r u e True True为素数, F a l s e False False为合数)
- 遍历 2 → 2020 2→2020 2→2020,若当前的数 x x x为 T r u e True True(未被标记),则为素数,对他进行处理:
①遍历 x x x的倍数 i = 2 x , 3 x , . . . i=2x,3x,... i=2x,3x,...
②如果 i i i未被标记过(防止合数重复标记,如2的3倍为6,3的2倍也为6,则统计的合数个数会增多),则标记vis[i]=False
,并让 c n t + 1 cnt+1 cnt+1
图解:
💯3.代码实现
1.试除法实现
from math import sqrtdef not_prime(x):for i in range(2,int(sqrt(x))+1):if x%i==0:return 1 # 为合数return 0 # 为素数cnt=0
for x in range(3,2021):if not_prime(x):cnt+=1
print(cnt) # 合数个数:1713
2.素数筛实现
vis=[True]*2021 # 默认都是素数
cnt=0
for x in range(2,2021):if vis[x]: # 如果是素数(没有被标记过)for i in range(x+x,2021,x): # 将[x,2020]中素数x的倍数全部标记为合数if vis[i]==True:cnt+=1vis[i]=False
print(cnt) # 1713
输出结果:
试题C.阶乘约数⭐️⭐️⭐️
🍰1.题目
阶乘约数
👑2.思路分析
难度:⭐️⭐️⭐️
标签:数论——唯一分解定理
🎇思路①:暴力法
🔱思路分析:
要得到所有的因子,不难想到用暴力法统计,但是!!不出意外地超时了
step:
- 遍历 i = 1 → 100 ! i=1→\sqrt{100!} i=1→100!,不断试除
- 如果能被整除,我们对该数进行讨论:
①如果 i = 100 ! i=\sqrt{100!} i=100!,则这里因子只增加了一个,cnt+=1
②如果 i i i为其他数,则这里因子必然有两个,cnt+=2
不通过代码: ❌
from math import sqrt
num=1
for i in range(2,101):num*=icnt=0
for j in range(1,int(sqrt(num))+1):if num%j==0:if j==int(sqrt(num)):cnt+=1else:cnt+=2
print(cnt)
🎇思路②:唯一分解定理
🔱思路分析:
我们先来介绍一个与此题相关的数学定理:
🚀 整数的唯一分解定理:
即正整数n(>1)可分解为: n = p 1 α 1 ∗ p 2 α 2 ∗ . . . ∗ p k α k n=p_1^{α1}*p_2^{α2}*...*p_k^{αk} n=p1α1∗p2α2∗...∗pkαk
其中, p 1 < p 2 < … < p k p1<p2<…<pk p1<p2<…<pk皆素数, α i ( i = 1 , 2 , … , k ) αi (i=1,2,…,k) αi(i=1,2,…,k)皆为正整数,而正整数n的正因数个数为: n u m = ( α 1 + 1 ) ∗ ( α 2 + 1 ) ∗ . . . ∗ ( α k + 1 ) num=(α1+1)*(α2+1)*...*(αk+1) num=(α1+1)∗(α2+1)∗...∗(αk+1),也就是将素数的次方加1后求乘积
为什么?
因为对于 p 1 p_1 p1来说,它的指数为 α 1 α1 α1,则分解时它可以有 α 1 + 1 α1+1 α1+1种取法;同理, α 2 α2 α2有 α 2 + 1 α2+1 α2+1种取法;…;所以,总的因数个数为 ( α 1 + 1 ) ∗ ( α 2 + 1 ) ∗ . . . ∗ ( α k + 1 ) (α1+1)*(α2+1)*...*(αk+1) (α1+1)∗(α2+1)∗...∗(αk+1)
什么意思呢?
举个例子:
我们要求100的正因数个数:
- 将100分解为: 100 = 2 2 ∗ 5 2 100=2^2*5^2 100=22∗52
- 则 p 1 = 2 , α 1 = 2 , p 2 = 5 , α 2 = 2 p_1=2,α1=2,p_2=5,α2=2 p1=2,α1=2,p2=5,α2=2,所以正因数的个数为: ( 2 + 1 ) ∗ ( 2 + 1 ) = 9 (2+1)*(2+1)=9 (2+1)∗(2+1)=9
- 这 9 9 9个数即为: 1 , 2 , 4 , 5 , 10 , 20 , 25 , 50 , 100 {1,2,4,5,10,20,25,50,100} 1,2,4,5,10,20,25,50,100
有了上面的定理支撑,我们就可以用代码实现了:
step:
- 由于 n ! = 1 ∗ 2 ∗ . . . ∗ n n!=1*2*...*n n!=1∗2∗...∗n,所以,我们先找到 [ 1 , n ] [1,n] [1,n]中的质数,记录在数组 s s s中
- 质因数分解:将每一个数都分解为质数的乘积,用字典记录 s s s中每一个素数的得到次数,最终即为指 α i αi αi
- 最后用公式: ( α 1 + 1 ) ∗ ( α 2 + 1 ) ∗ . . . ∗ ( α k + 1 ) (α1+1)*(α2+1)*...*(αk+1) (α1+1)∗(α2+1)∗...∗(αk+1)即可
💯3.代码实现
唯一分解定理实现:
from math import *def is_prime(x):for i in range(2,int(sqrt(x))+1):if x%i==0:return 0 # 为合数return 1 # 为素数s=[2] # 记录[1,100]的素数
for i in range(3,101):if is_prime(i):s.append(i)p=dict()
for i in s:p[i]=0 # 将质数的次数初始化为0# 质因数分解
for i in range(1,101):for a in s:while i%a==0:i=i/ap[a]+=1# 求正因数个数
res=1
for d in p.values():res*=(d+1)
print(res)
输出结果:
试题D.本质上升序列⭐️⭐️⭐️
🍰1.题目
本质上升序列
👑2.思路分析
难度:⭐️⭐️⭐️
标签:dp动态规划
🎇思路:线性dp
🔱思路分析:
要求升序字符串,即字符串中从左到右满足线性关系(后一个大于前一个),因此,我们可以用动态规划从前到后不断统计个数
step:
- 字符串为 s s s, s [ k ] s[k] s[k]即代表字符串中第 k + 1 k+1 k+1个字符
- 动态规划数组 d p [ i ] dp[i] dp[i]: d p [ i ] dp[i] dp[i]表示以字符 s [ i ] s[i] s[i]结尾的满足本质上升序列的字符串的个数,其中 i ∈ [ 1 , n − 1 ] i∈[1,n-1] i∈[1,n−1],初始化 d p [ ] = 1 dp[]=1 dp[]=1
- 遍历 i i i从 1 1 1到 n − 1 n-1 n−1,对于每一个 i i i,我们遍历i之前的所有字符 s [ j ] s[j] s[j]:
①如果当前 s [ i ] > s [ j ] s[i]>s[j] s[i]>s[j]:则表明,以 s [ j ] s[j] s[j]结尾的满足本质上升序列的字符串,在结尾加上 s [ i ] s[i] s[i]后,也满足升序条件,且为一种新的字符串情况,因此:dp[i]+=dp[j]
②如果当前 s [ i ] = s [ j ] s[i]=s[j] s[i]=s[j]:则表明,对于 s [ j ] s[j] s[j]之前的字符,以 s [ j ] s[j] s[j]结尾和 s [ i ] s[i] s[i]结尾的本质是一样的,只是换了尾字符的位置,所以,要减去这些重复的情况:dp[i]-=dp[j]
③如果当前 s [ i ] < s [ j ] s[i]<s[j] s[i]<s[j]:则表明,不能直接向以 s [ j ] s[j] s[j]结尾的字符串后添加 s [ i ] s[i] s[i],因为不满足升序条件了,所以跳过,继续判断下一个 s [ j ] s[j] s[j]:continue
- 最后,统计 d p dp dp数组 [ 0 , n − 1 ] [0,n-1] [0,n−1]的和,即为所有情况
为什么要初始化 d p dp dp为 1 1 1?
- d p [ 0 ] dp[0] dp[0]需要:因为以s[0](第一个字符)结尾的情况必定有 1 1 1种,且后续 d p [ 1 → n ] dp[1→n] dp[1→n]的值都是由 d p [ 0 ] → d p [ i − 1 ] dp[0]→dp[i-1] dp[0]→dp[i−1]递推而来的,所以初始化 d p [ 0 ] = 1 dp[0]=1 dp[0]=1
- 默认以 s [ i ] s[i] s[i]结尾的满足条件的字符串都是一种情况(也就是单个字符的情况):我们在遍历时,只遍历了 s [ i ] s[i] s[i]之前的 s [ 0 → ( i − 1 ) ] s[0→(i-1)] s[0→(i−1)]的情况,而没有考虑单个 s [ i ] s[i] s[i]自身,所以,要初始化为1,如果字符串 s s s中有单个重复的情况,也会在 s [ i ] = s [ j ] s[i]=s[j] s[i]=s[j]时被减去
以字符串 ′ l a n q i a o ′ 'lanqiao' ′lanqiao′为例:
特别注意,当 s [ 5 ] = s [ 1 ] = ′ a ′ s[5]=s[1]='a' s[5]=s[1]=′a′时,应该执行dp[5]-dp[1]
,这里可以直观地看到,就是减去’a’这一情况,避免了重复字符串的出现
以此类推,最终相加即可得到总数为: 1 + 1 + 3 + 6 + 2 + 0 + 8 = 21 1+1+3+6+2+0+8=21 1+1+3+6+2+0+8=21
💯3.代码实现
线性dp实现:
s='tocyjkdzcieoiodfpbgcncsrjbhmugdnojjddhllnofawllbhfiadgdcdjstemphmnjihecoapdjjrprrqnhgccevdarufmliqijgihhfgdcmxvicfauachlifhafpdccfseflcdgjncadfclvfmadvrnaaahahndsikzssoywakgnfjjaihtniptwoulxbaeqkqhfwl'
n=len(s)
dp=[1]*n # dp[i]表示以s[i]结尾的字符串中本质上升序列的数目(单个字符都满足)
for i in range(1,n):for j in range(i): # [0,i-1]if s[i]>s[j]: # 如果s[i]>s[j],则可以看做以s[j]结尾的字符串之后再添加一个s[i]便可以得到新的一种本质上升序列dp[i]+=dp[j]elif s[i]==s[j]: # 如果s[i]=s[j],则表示dp[j]的情况和dp[i]部分重复了,所以要减去这些情况dp[i]-=dp[j]else:continue # 如果s[i]<s[j],继续下一轮
print(sum(dp)) # 3616159
输出结果:
试题E.玩具蛇⭐️⭐️⭐️
🍰1.题目
玩具蛇
👑2.思路分析
难度:⭐️⭐️⭐️
标签:dfs深度优先遍历
🎇思路:dfs深搜
🔱思路分析:
题目要求找到所有摆放蛇各个身段的情况,由于蛇身是连续的,也就是相邻的身段只能朝上下左右摆放,不能是对角线,所以,我们可以对蛇头(序号为 1 1 1)在每一个方格处都进行 d f s dfs dfs深搜操作,最终得到所有满足条件的情况
注意:对于相同形状,但存在在相同方格中,蛇的位置不同时,为不同的方案
图示:
step:
- 将 4 x 4 4x4 4x4方格中每一个格子都作为一次蛇头的位置(序号为1)进行 d f s dfs dfs
- d f s ( x , y , s t e p ) dfs(x,y,step) dfs(x,y,step):从蛇头出发,表示当前蛇尾所在位置为 ( x , y ) (x,y) (x,y),且蛇的身长为 s t e p step step:
①结束条件:如果蛇的身长达到16step=16
,则表示该方案可行,cnt+=1
②深搜四个方向:如果蛇尾向该方向扩展后,仍在4x4方格内,且该方格未被标记过时: 标记 + 深搜 + 回溯 标记+深搜+回溯 标记+深搜+回溯 - 最后 c n t cnt cnt的值即为情况总数
注意:在 4 x 4 4x4 4x4方格中,每一轮以新的方格作为蛇头时,要重新构造 v i s [ 4 ] [ 4 ] vis[4][4] vis[4][4]数组,因为要清空之前 d f s dfs dfs留下的标记
💯3.代码实现
dfs实现:
def judge(x,y):if x>=0 and x<=3 and y>=0 and y<=3 and vis[x][y]==False: # 如果该点还未被访问过return Truereturn Falsedef dfs(x,y,step):global cnt,vis# 结束条件if step==16:cnt+=1returnfor d in [(1,0),(-1,0),(0,1),(0,-1)]: # 对四个方向进行深搜nx,ny=x+d[0],y+d[1]if judge(nx,ny):# 标记vis[nx][ny]=True# dfsdfs(nx,ny,step+1)# 回溯vis[nx][ny]=Falsecnt = 0
for i in range(4):for j in range(4):vis = [[False] * 4 for _ in range(4)] # 存4x4方格图(注意:每次到一个新的点,必须要清空vis数组,重新dfs)vis[i][j] = True # 对当前点(蛇头)进行标记dfs(i,j,1) # 对点(i,j)进行深搜,1表示:经过了(i,j)位置,为第一步
print(cnt)
输出结果:
试题F.天干地支⭐️
🍰1.题目
天干地支
👑2.思路分析
难度:⭐️
标签:列表
🎇思路:模拟
🔱思路分析:
本题要求任意年份的天干地支 ( [ 1 , 9999 ] ) ([1,9999]) ([1,9999]),由于天干的周期为 10 10 10,地支的周期为 12 12 12,因此,我们可以根据周期的差异分别计算天干和地支
step:
- 构造天干和地支的列表
这里有一个小技巧 t i p s tips tips:我们可以先算出公元 0 0 0年的天干地支 → → →为庚申年
于是,我们可以将天干地支列表的首元素变为 ′ g e n g ′ 'geng' ′geng′和 ′ s h e n ′ 'shen' ′shen′,之后则按照正常顺序放置,这样,我们就可以实现 年份除以相应周期的余数 = = =对应列表的下标 - 年份分别对 10 10 10和 12 12 12取余:
t=year%10
;d=year%12
再将对应列表下标中的元素取出,拼在一起即为结果
💯3.代码实现
列表操作实现:
Tiangan=['geng','xin','ren','gui','jia','yi','bing','ding','wu','ji']
Dizhi=['shen','you','xu','hai','zi','chou','yin','mou','chen','si','wu','wei']
year=int(input())
t=year%10
d=year%12
print(Tiangan[t]+Dizhi[d])
输出结果:
试题G.重复字符串⭐️⭐️⭐️
🍰1.题目
重复字符串
👑2.思路分析
难度:⭐️⭐️⭐️
标签:枚举
🎇思路:字符串分组
🔱思路分析:
要令字符串 s = s= s=某字符串 s ′ s' s′重复 k k k次,且满足对字符串 s s s 的修改次数最少,因此,我们可以将字符串 s s s分成连续的 k k k 个子串,每一个子串的大小为 L = l e n ( s ) / / k L=len(s)//k L=len(s)//k (如果有余数则无解),我们对这 k k k个子串对齐摆放,按列处理即可
step:
-
先将字符串 s s s 切片处理,也就是取长度为 L = l e n ( s ) / / k L=len(s)//k L=len(s)//k 的子串,如果有余数 l e n ( s ) % k ! = 0 len(s)\%k!=0 len(s)%k!=0,则无解;反之,则继续进行下一步
-
由于新字符串的长度为 L L L,也就是:
将 [ 0 , l e n ( s ) − 1 ] [0,len(s)-1] [0,len(s)−1]分成 [ 0 , L − 1 ] , [ L , 2 L − 1 ] , . . . , [ l e n ( s ) − L , l e n ( s ) − 1 ] [0,L-1],[L,2L-1],...,[len(s)-L,len(s)-1] [0,L−1],[L,2L−1],...,[len(s)−L,len(s)−1]
如何得到这样一个 k k k x L L L 阶的矩阵呢?
可以依次以 s [ 0 → ( L − 1 ) ] s[0→(L-1)] s[0→(L−1)]为起始, L L L为步长遍历整个字符串并取出对应位置的元素:即可得到一列的元素
-
我们按列进行处理,每得到一列的元素,我们将其放入字典中,判断哪一个字符出现的次数最多(为 n u m num num),则我们将出现次数少的其他元素全部改为该元素即可满足在该位置上修改的次数最少,其中,每一列至少需要修改的次数为: c n t = k − n u m cnt=k-num cnt=k−num
因为总共有 k k k行,所以减去包含次数最多元素的 n u m num num行后,剩下的 k − n u m k-num k−num行需要修改
-
循环遍历 0 → L − 1 0→L-1 0→L−1列,统计每一列至少需要修改次数之和即为 r e s res res
图解:
e . g e.g e.g 以字符串 s = ′ a b c a b a b c a c a a ′ s='abcababcacaa' s=′abcababcacaa′ 为例,假设 k = 4 k=4 k=4,也就是重复出现4次
我们可以得到: k = 4 , L = 12 / / 4 = 3 k=4,L=12//4=3 k=4,L=12//4=3,按照上述方法,则字符串 s s s 分组为 4 4 4 x 3 3 3 阶矩阵
我们对每一列进行字典统计操作,统计出现次数最多的字符和对应的次数
所以,至少需要修改: 2 + 2 + 1 = 5 2+2+1=5 2+2+1=5 次,即将其变为 ′ a b a ′ 'aba' ′aba′ 重复 4 4 4次得到 s s s
💯3.代码实现
字符串分组实现:
k=int(input())
s=input()
res=0
if len(s)%k!=0: # 不能被整除print(-1) # 无解
else:l=len(s)//k # l是列数for i in range(l): # 分别取首位置为[0,l-1]dic = {} # 清空字典for j in range(i,len(s),l): # 以l为步长 构造同一列下的字符集字典dic[s[j]]=dic.get(s[j],0)+1val=dic.values()num=max(val) # 找到同一列中出现次数最多的数res+=k-num # k是行数 k-num即为要修改的最小数字个数print(res)
输出结果:
试题H.答疑⭐️⭐️⭐️
🍰1.题目
答疑
👑2.思路分析
难度:⭐️⭐️⭐️
标签:贪心
🎇思路:贪心算法
🔱思路分析:
首先,由贪心算法可知,我们只需要让答疑消耗总时间越短的人越先答疑,即可实现发送消息的时刻之和最短
step:
- 将发送消息前后所需要的时间视为两个部分:即 x i → 发消息 → y i x_i → 发消息 → y_i xi→发消息→yi,总时间为 z i = x i + y i z_i=x_i+y_i zi=xi+yi,嵌套列表存放每一个人的 [ x i , y i , z i ] [x_i,y_i,z_i] [xi,yi,zi]
- 依据 z i z_i zi进行从小到大的排序,当前同学发消息的时刻为 t i m e + x i time+x_i time+xi,遍历所有嵌套列表,每一轮循环结束后,都让时刻处于当前同学完全结束的时刻,也就是 t i m e = z 0 + z 1 + . . . + z i time=z_0+z_1+...+z_i time=z0+z1+...+zi,之后继续下一个同学的答疑
- 最终,时刻之和即为 r e s res res
证明:
现在我们来证明,为什么这道题可以用贪心算法:
d e f def def:首先,每个人的 s i s_i si 和 a i a_i ai 都可以视为一个部分(也就是前期准备),记为 d d d;收拾时间即为 e e e
规定 P i P_i Pi 表示第 i i i 个人, x i x_i xi和 y i y_i yi分别对应了第 i i i个人的 d d d和 e e e , z i = x i + y i z_i=x_i+y_i zi=xi+yi 对应第 i i i个人的答疑总时间
假设当前序列为 P 1 P 2 . . . P i P i + 1 . . . P n P_1P_2...P_iP_{i+1}...P_n P1P2...PiPi+1...Pn,令 j = i + 1 j=i+1 j=i+1
易得,交换 P i P_i Pi 和 P j P_j Pj 顺序并不会影响除了二者之外的发送消息时间,所以交换后变化的只有二者的发送时间,而影响最终结果
假设除了 i , j i,j i,j 之外的人的发送时间之和为 S S S,则 r e s = S + P i 发送时间 + P j 发送时间 res=S+P_i发送时间+P_j发送时间 res=S+Pi发送时间+Pj发送时间
记 x 1 + y 1 + x 2 + y 2 + . . . + x i − 1 + y i − 1 x_1+y_1+x_2+y_2+...+x_{i-1}+y_{i-1} x1+y1+x2+y2+...+xi−1+yi−1 为 S i − 1 S_{i-1} Si−1
①交换顺序前: P i P_i Pi 的发送时间为 S i − 1 + x i S_{i-1}+x_i Si−1+xi , P j P_j Pj的发送时间为 S i − 1 + x i + y i + x j S_{i-1}+x_i+y_i+x_j Si−1+xi+yi+xj , r e s = S + ( S i − 1 + x i ) + ( S i − 1 + x i + y i + x j ) res=S+(S_{i-1}+x_i)+(S_{i-1}+x_i+y_i+x_j) res=S+(Si−1+xi)+(Si−1+xi+yi+xj)
②交换顺序后: P i P_i Pi 的发送时间为 S i − 1 + x j + y j + x i S_{i-1}+x_j+y_j+x_i Si−1+xj+yj+xi , P j P_j Pj 的发送时间为 S i − 1 + x j S_{i-1}+x_j Si−1+xj , r e s ′ = S + ( S i − 1 + x j + y j + x i ) + ( S i − 1 + x j ) res'=S+(S_{i-1}+x_j+y_j+x_i)+(S_{i-1}+x_j) res′=S+(Si−1+xj+yj+xi)+(Si−1+xj)
对比 r e s res res 和 r e s ′ res' res′,代入 z i , z j z_i,z_j zi,zj 后可以看出:
r e s = S + 2 S i − 1 + x i + x j + res=S+2S_{i-1}+x_i+x_j+ res=S+2Si−1+xi+xj+ z j z_j zj
r e s ′ = S + 2 S i − 1 + x i + x j + res'=S+2S_{i-1}+x_i+x_j+ res′=S+2Si−1+xi+xj+ z i z_i zi
所以,影响结果的只有每个人答疑花费的总时间 ( z i z_i zi 和 z j z_j zj),因此,只需要将总时间小的放在前面进行答疑即可(即对 z z z 进行排序)
💯3.代码实现
贪心算法实现:
n=int(input())
li=[]
for i in range(n):x,y,z=map(int,input().split())li.append([x+y,z,x+y+z])
li.sort(key=lambda x:x[2]) # 根据总时间排序,短的放前面
time=0
res=0
for i in range(n):time+=li[i][0] # 加上前期准备时间res+=timetime+=li[i][1] # 加上离开时间
print(res)
输出结果:
试题I.补给⭐️⭐️⭐️⭐️⭐️
🍰1.题目
补给
👑2.思路分析
难度:⭐️⭐️⭐️⭐️⭐️
标签: 状压 d p + F l o y d 状压dp+Floyd 状压dp+Floyd算法
🎇思路:状态压缩 d p + F l o y d dp+Floyd dp+Floyd算法
🔱思路分析:
本题为经典的"旅行商"问题:从起点出发,找一条经过所有n个点再回到起点的最短路径长度,于是,我们可以建模为带权无向图
step:
1. F l o y d Floyd Floyd算法:
F l o y d Floyd Floyd算法思想是:由小图逐渐扩展为大图,不断向已有点的集合中加入一个新的点,依次判断该点的加入是否会对集合中的点之间的最短路径产生影响
-
用邻接矩阵 w [ i ] [ j ] w[i][j] w[i][j]储存路径:表示从点 i i i到点 j j j的最短路径
初始时: w [ i ] [ j ] w[i][j] w[i][j]表示点 i i i到点 j j j之间的直接路径长度
结束时: w [ i ] [ j ] w[i][j] w[i][j]表示点 i i i到点 j j j之间的最短路径长度
注意:
①没有直接相连的两点默认值为无穷大
②w[i][i]=0
-
将第 1 1 1到 n n n个点依次加入点集 S S S中,对图中每一个点对 ( i , j ) (i,j) (i,j)进行遍历,其中 i ∈ [ 1 , n ] , j ∈ [ 1 , n ] i∈[1,n],j∈[1,n] i∈[1,n],j∈[1,n],如果 w [ i ] [ j ] > w [ i ] [ k ] + w [ k ] [ j ] w[i][j]>w[i][k]+w[k][j] w[i][j]>w[i][k]+w[k][j],也就是 i → k → j i→k→j i→k→j的距离比 i i i直接到 j j j的距离小,于是更新 i i i 到 j j j 的最短路径 w [ i ] [ j ] w[i][j] w[i][j]
-
至此,我们就得到了无向图中任意两点间的最短距离
代码实现 F l o y d Floyd Floyd:
def Floyd():global wfor i in range(n): # 点0到n-1for j in range(n): # 表示点i到点jfor k in range(n): # 引入第三点kw[i][j]=min(w[i][j],w[i][k]+w[k][j])
如图所示:
2. 状态压缩 d p dp dp
对于上述问题,也就是给定一张n个点的带权无向图,点为 0 0 0 ~ n − 1 n-1 n−1,现在已知任意两个点的最短路径,要求起点0到达每一个点后再回来的最短路径长度,也就是 0 0 0 ~ n − 1 n-1 n−1个点都要经过
①确认状态:
d p dp dp动态规划:不难发现,我们只需要知道:①哪些点已经走过;②目前停在那个点上 即可,而此时不用关心已经遍历过的点之间的具体顺序,因为我们已经得到了经过当前点集 S S S中的点的最短路径
于是,我们设状态为 S S S, d p [ S ] [ j ] dp[S][j] dp[S][j]表示起点 0 0 0到点 j j j,且经过点的点集 S S S 时的最短路径长度
则有 d p [ S ] [ j ] = d p [ S − j ] [ k ] + w [ k ] [ j ] dp[S][j]=dp[S-j][k]+w[k][j] dp[S][j]=dp[S−j][k]+w[k][j],其中 S − j S-j S−j表示点集 S S S中去除掉点 j j j后且包含点 k k k的集合, w [ k ] [ j ] w[k][j] w[k][j] 即为已经求得的点 k k k 到点 j j j 的最短路径长度
所以,当前新加入的点为 j j j,我们依次遍历 S − j S-j S−j中的点 k k k,找到 S − j S-j S−j到 j j j最短的一条路,再更新 d p [ S ] [ j ] dp[S][j] dp[S][j]的值即可,以此类推, S S S 则由 0 0 0 逐渐扩展到 n − 1 n-1 n−1
图解算法:
②状态压缩 d p dp dp:
那么,我们应该如何表示这个集合 S S S呢?
这里就是状态压缩 d p dp dp的核心:用二进制数 0 / 1 0/1 0/1表示是否经过该点, 1 1 1表示经过, 0 0 0表示未经过
如 01101 01101 01101,表示经过了点 0 0 0、 2 2 2和 3 3 3,此时点集为: S = 0 , 2 , 3 S={0,2,3} S=0,2,3
所以, [ 1 , ( 1 < < n ) − 1 ] [1,(1<<n)-1] [1,(1<<n)−1] 为每一个状态的点集 S S S ,最终为 111...1 ( n 个 1 ) 111...1 (n个1) 111...1(n个1),表示所有点都经过了
-
初始化:
dp[1][0]=0
,表示只有起点时,到起点的最短距离自然为 0 0 0
-
对每个状态下的 S S S,遍历所有点 [ 0 , n − 1 ] [0,n-1] [0,n−1],记为 j j j,判断当前 j j j 是否在当前点集 S S S 中:
①如果 j j j不在:即S>>j & 1==0
(按位与 ‘&’)
则继续找下一个在集合中的 j j j 点;
②如果 j j j在:即S>>j & 1!=0
则找到没有点 j j j 时的集合 S − j S-j S−j,遍历 S − j S-j S−j 中的所有点 k k k,即满足:S^(1<<j)>>k &1 !=0
(按位异或 ‘^’)
从而找到一个k,得到一条最短路径 w [ k ] [ j ] w[k][j] w[k][j],则有:dp[S][j]=dp[S-j][k]+w[k][j]
代码实现:
dp=[[float('inf')]*21 for _ in range(1<<21)]
dp[1][0]=0
for S in range(1,1<<n):for j in range(n):if S>>j & 1!=0: # 找到在集合S中的点jfor k in range(n):if S^(1<<j)>>k &1 !=0: # 找到在集合S-j中的点kdp[S][j]=min(dp[S][j],dp[S^(1<<j)][k]+w[k][j])
- 最后,找到经过所有点时的最佳停留点 i ( i ≠ 0 ) i(i≠0) i(i=0),使得此时走过的距离
dp[(1<<n)-1][i]
,加上点 i i i 到起点 0 0 0的距离w[i][0]
最短
res=float('inf')
for i in range(1,n):res=min(res,dp[(1<<n)-1][i]+w[i][0]) # 最短距离=min(到最终停留点i的距离+i到0的距离)
💯3.代码实现
状压 d p dp dp+ F l o y d Floyd Floyd算法实现:
from math import *
def dis(a,b):d=sqrt(pow(a[0]-b[0],2)+pow(a[1]-b[1],2))return ddef Floyd():global wfor i in range(n): # 点0到n-1for j in range(n): # 表示点i到点jfor k in range(n): # 引入第三点kw[i][j]=min(w[i][j],w[i][k]+w[k][j])n,D=map(int,input().split())
w=[[float('inf')]*n for _ in range(n)] # 邻接矩阵记录最短路径
xy=[] # 记录坐标点
for i in range(n):xy.append(list(map(float,input().split())))# 最短路径
for i in range(0,n):for j in range(i+1,n):d=dis(xy[i],xy[j]) # 传入两点的坐标if d<=D: # 如果两点的距离不超过最大飞行距离 更新w[i][j] = dw[j][i] = d
Floyd()# 状态压缩
dp=[[float('inf')]*n for _ in range(1<<n)]
dp[1][0]=0
for S in range(1,1<<n):for j in range(n):if S>>j & 1!=0: # 找到在集合S中的点jfor k in range(n):if S^(1<<j)>>k &1 !=0: # 找到在集合S-j中的点kdp[S][j]=min(dp[S][j],dp[S^(1<<j)][k]+w[k][j])res=float('inf')
for i in range(1,n):res=min(res,dp[(1<<n)-1][i]+w[i][0]) # 最短距离=min(到最终停留点i的距离+i到0的距离)
print("%.2f"%res)
输出结果:(python本身运行较慢,只能通过60%)
试题J.蓝跳跳⭐️⭐️⭐️⭐️⭐️⭐️
🍰1.题目
蓝跳跳
👑2.思路分析
难度:⭐️⭐️⭐️⭐️⭐️⭐️
标签: d p dp dp动态规划 +矩阵快速幂
🎇思路: d p dp dp +矩阵快速幂
🔱思路分析:
本题要求方案总数,因此考虑用 d p dp dp或者 d f s dfs dfs解决
此时会存在两个问题:
① d p [ L ] [ k ] dp[L][k] dp[L][k],需要的空间很大( L = 1 0 18 L=10^{18} L=1018)
② L L L 太大,会超时
但不要着急,我们来一一解决
step:
1.动态规划 d p dp dp
我们考虑直接 d p dp dp:
定义 d p [ i ] [ j ] dp[i][j] dp[i][j] , i i i表示当前跳到的位置, i i i从 1 1 1递推到 L L L, j j j 则表示走到 i i i 位置时最后一次走的步长为 j j j
则状态转移方程为:
d p [ i ] [ j ] = { ∑ d = 1 k d p [ i − j ] [ d ] j < p ∑ d = 1 p − 1 d p [ i − j ] [ d ] j ≥ p dp[i][j]=\begin{cases} ∑_{d=1}^{k}dp[i-j][d] & j<p \\ \\ ∑_{d=1}^{p-1}dp[i-j][d] & j≥p \end{cases} dp[i][j]=⎩ ⎨ ⎧∑d=1kdp[i−j][d]∑d=1p−1dp[i−j][d]j<pj≥p
也就是说,如果到位置 i i i 时:
- 最后一步的步长 j < p j<p j<p:那么可以选择 [ 1 , k ] [1,k] [1,k] 中的任意一个距离跳跃到 i − j i-j i−j 位置,因为对于当前状态下来说,无论如何都不可能存在连续两次跳跃距离 > p >p >p
- 最后一步的步长 j ≥ p j≥p j≥p:那么此时只能选择 [ 1 , p − 1 ] [1,p-1] [1,p−1] 中的一个距离跳跃到 i − j i-j i−j 位置,因为这一步超过了 p p p,那么前一步就一定不能超过 p p p
此时的时间复杂度即为: O ( k p L ) O(kpL) O(kpL)
最终结果为: r e s = ∑ d = 1 k d p [ L ] [ d ] res=∑_{d=1}^kdp[L][d] res=∑d=1kdp[L][d]
接下来进行状态压缩<空间优化>:不难发现,对于 j j j,只有 < p <p <p 和 ≥ p ≥p ≥p 两种状态,我们不需要直到 j j j 的具体值,因此,将状态 j j j 压缩为 0 / 1 0/1 0/1,于是:
定义 d p [ i ] [ 0 / 1 ] dp[i][0/1] dp[i][0/1],表示当前跳到位置 i i i, 0 / 1 0/1 0/1表示最后一步是否小于 p p p 的计数
状态转移方程变为:
d p [ i ] [ d ] = { ∑ j = 1 p − 1 d p [ i − j ] [ 0 ] + d p [ i − j ] [ 1 ] d = 0 ∑ j = p k d p [ i − j ] [ 0 ] d = 1 dp[i][d]=\begin{cases} ∑_{j=1}^{p-1}dp[i-j][0] + dp[i-j][1] & d=0 \\ \\ ∑_{j=p}^{k}dp[i-j][0] & d=1 \end{cases} dp[i][d]=⎩ ⎨ ⎧∑j=1p−1dp[i−j][0]+dp[i−j][1]∑j=pkdp[i−j][0]d=0d=1
此时的时间复杂度为: O ( k L ) O(kL) O(kL)
最终结果为: r e s = d p [ L ] [ 0 ] + d p [ L ] [ 1 ] res=dp[L][0]+dp[L][1] res=dp[L][0]+dp[L][1]
2.矩阵快速幂
由上分析,我们发现,其状态转移 O ( k L ) O(kL) O(kL) 是线性的,也就是说,对于当前状态 i i i,只和前 k k k 个状态有关,且可以写成矩阵乘法的形式,具体地,我们写成 2 k 2k 2k x 2 k 2k 2k 矩阵
例如,当 k = 3 , p = 2 k=3,p=2 k=3,p=2 时:
我们先列出线性方程组:
{ d p [ i ] [ 1 ] = d p [ i − 2 ] [ 0 ] + d p [ i − 3 ] [ 0 ] d p [ i ] [ 0 ] = d p [ i − 1 ] [ 1 ] + d p [ i − 1 ] [ 0 ] d p [ i − 1 ] [ 1 ] = d p [ i − 1 ] [ 1 ] d p [ i − 1 ] [ 0 ] = d p [ i − 1 ] [ 0 ] d p [ i − 2 ] [ 1 ] = d p [ i − 2 ] [ 1 ] d p [ i − 2 ] [ 0 ] = d p [ i − 2 ] [ 0 ] \begin{cases} \ dp[i][1]=dp[i-2][0]+dp[i-3][0] \\ \ dp[i][0]=dp[i-1][1]+dp[i-1][0] \\ \ dp[i-1][1]=dp[i-1][1] \\ \ dp[i-1][0]=dp[i-1][0] \\ \ dp[i-2][1]=dp[i-2][1] \\ \ dp[i-2][0]=dp[i-2][0] \\ \end{cases} ⎩ ⎨ ⎧ dp[i][1]=dp[i−2][0]+dp[i−3][0] dp[i][0]=dp[i−1][1]+dp[i−1][0] dp[i−1][1]=dp[i−1][1] dp[i−1][0]=dp[i−1][0] dp[i−2][1]=dp[i−2][1] dp[i−2][0]=dp[i−2][0]
再表示为对应的矩阵乘法,也就实现了状态转移:
也就是由状态 i − 1 i-1 i−1 扩展到了状态 i i i
( d p [ i ] [ 1 ] d p [ i ] [ 0 ] d p [ i − 1 ] [ 1 ] d p [ i − 1 ] [ 0 ] d p [ i − 2 ] [ 1 ] d p [ i − 2 ] [ 0 ] ) = ( 0 0 0 1 0 1 1 1 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 ) ( d p [ i − 1 ] [ 1 ] d p [ i − 1 ] [ 0 ] d p [ i − 2 ] [ 1 ] d p [ i − 2 ] [ 0 ] d p [ i − 3 ] [ 1 ] d p [ i − 3 ] [ 0 ] ) \left( \begin{matrix} dp[i][1] \\ dp[i][0] \\ dp[i-1][1] \\ dp[i-1][0] \\ dp[i-2][1] \\ dp[i-2][0] \end{matrix} \right) = \left( \begin{matrix} 0 & 0 & 0 & 1 & 0 & 1\\ 1 & 1 & 0 & 0 & 0 & 0\\ 1 & 0 & 0 & 0 & 0 & 0\\ 0 & 1 & 0 & 0 & 0 & 0\\ 0 & 0 & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & 1 & 0 & 0 \end{matrix} \right) \left( \begin{matrix} dp[i-1][1] \\ dp[i-1][0] \\ dp[i-2][1] \\ dp[i-2][0] \\ dp[i-3][1] \\ dp[i-3][0] \\ \end{matrix} \right) dp[i][1]dp[i][0]dp[i−1][1]dp[i−1][0]dp[i−2][1]dp[i−2][0] = 011000010100000010100001000000100000 dp[i−1][1]dp[i−1][0]dp[i−2][1]dp[i−2][0]dp[i−3][1]dp[i−3][0]
此时,时间复杂度为: O ( 8 k 3 l o g L ) O(8k^3logL) O(8k3logL)
(这里还没有做出来,先挖个坑吧…)
💯3.代码实现
动态规划 d p dp dp 实现:
k,p,L=map(int,input().split())
dp=[[0]*2 for _ in range(L+1)]
dp[0][0]=1
for i in range(1,L+1):# 1.考虑步长小于p时for j in range(1,p):if i-j<0:breakdp[i][0]+=(dp[i-j][0]+dp[i-j][1])%20201114# 2.考虑步长大于等于p时for j in range(p,k+1):if i-j<0:breakdp[i][1]+=dp[i-j][0]%20201114res=sum(dp[L])%20201114
print(res)
输出结果: