Acwing 蓝桥杯集训·每日一题 2023 记录

Acwing 蓝桥杯集训·每日一题

  • 前言
  • week1
    • 星期一:前缀和
      • AcWing 3956. 截断数组(每日一题)
        • 思路
        • 代码
        • 注意点
    • 星期二:差分
      • AcWing 3729. 改变数组元素(每日一题)
        • 思路
        • 代码
        • 注意点
    • 星期三:二分
      • AcWing 1460. 我在哪?(每日一题)
        • 思路
        • 代码
    • 星期四:双指针
      • AcWing 3768. 字符串删减(每日一题)
        • 思路
        • 代码
        • 注意
    • 星期五:递推
      • AcWing 3777. 砖块(每日一题)
        • 思路
        • 代码
        • 注意
  • week2
    • 星期一:递归
      • AcWing 1497. 树的遍历(每日一题)
        • 思路
        • 代码
    • 星期二:并查集
      • AcWing 1249. 亲戚(每日一题)
        • 思路
        • 代码
        • 注意
    • 星期三:哈希
      • AcWing 2058. 笨拙的手指(每日一题)
        • 思路
        • 代码
        • 注意
    • 星期四:单调队列
      • AcWing 299. 裁剪序列(每日一题)
        • 思路
        • 代码

前言

2023/03/25
蓝桥杯在即,我居然现在才开始刷y总的每日一题。每天更新,争取在蓝桥杯2023/04/08之前刷完!这篇博客主要是记录自己的刷题历程。参赛语言:python3。

做题记录
2023/03/25
week1的星期一、星期二
2023/03/26
week1的星期三~星期五,week2的星期一
2023/03/27
week2的星期二,星期三没写完
2023/03/28
week2的星期三写完,星期四没写完(被裁剪序列卡住了)
2023/03/29

week1

星期一:前缀和

AcWing 3956. 截断数组(每日一题)

思路

因为这个题标注了前缀和的知识点,所以我就努力往这个方向想(菜菜)。我想应该先用 O ( n ) O(n) O(n) 的时间算出前缀和数组,然后用二重循环去遍历两个数组交界点的位置,已知前缀和数组则可以用 O ( 1 ) O(1) O(1) 计算出三段子数组的元素和,如果三段都相等,则计数+1。

但是数据范围是 1 0 5 10^5 105 ,盲猜大概率会超时。果然超时了。滚去看y总的题解。

y总技巧:根据数据范围去看标准解法的时间复杂度。比如这题,数据范围是 1 0 5 10^5 105 ,则时间复杂度应该是 O ( n l o g n ) O(nlogn) O(nlogn)

y总的技术分享: 由数据范围反推算法复杂度以及算法内容

由于时间复杂度最多为 O ( n l o g n ) O(nlogn) O(nlogn) ,而枚举2个点时间复杂度是 O ( n 2 ) O(n^2) O(n2) ,所以最多只能枚举一个点。y总说按经验一般是枚举第2个点。枚举过程中,当给第2个点一个确定值 j j j 之后,满足条件的方案数是从第1个点到第 j − 1 j-1 j1 个点中,前缀和的值为 s [ n ] / / 3 s[n]//3 s[n]//3 的点数,其中 s [ n ] s[n] s[n] 是整个数组的元素总和。但是每次求方案数,不需要遍历第一个点,因为随着第2个点 j j j 往后移动1次,此时的方案数与上一次的方案数是有关联的,仅仅取决于第 j − 1 j-1 j1 个点的情况。(感觉讲不太清,看代码吧)

代码

N = 100010
s = [0]*N # 前缀和数组
n = int(input())
a = [int(x) for x in input().split()] # 原始数组
a = [0]+a # 下标从1开始
# 前缀和模板
for i in range(1, n+1):
    s[i] = s[i-1] + a[i]

if s[n]%3!=0: print(0)
else:
    res = 0; cnt = 0
    for j in range(2, n): # 第一段、第三段非空
        if s[j-1]==s[n]//3: cnt+=1
        if s[j]==s[n]//3*2: res+=cnt
    print(res)

注意点

  • 要保持第一段、第三段非空,所以枚举第2个点的范围必须从2到n-1。(读入的数组下标从1开始)
  • 因为三段子数组的元素和相等,也就是说整个数组的元素和必须能够整除3。可以用这个条件,在最开始进行特判。
  • 前缀和打表的模板,要求原始数组的下标从1开始。
  • 如果用c++写,记录答案的res必须是long long型,因为数组长度最长是 1 0 5 10^5 105,极端情况下,如果数组元素全为0,则任意挑选2个断点都满足条件,此时方案数是 C n 2 = 5 ⋅ 1 0 9 C_n^2 = 5·10^9 Cn2=5109,大于int的数据范围 2.1 ⋅ 1 0 9 2.1·10^9 2.1109

星期二:差分

AcWing 3729. 改变数组元素(每日一题)

思路

看题目脑袋空空,回顾了一下差分的知识点,差分的作用是用 O ( 1 ) O(1) O(1) 的时间完成对原始数组 [ l , r ] [l,r] [l,r] 内的每个元素都+1。尝试想了想思路:每次操作都往数组尾部插入一个0,则最终数组V是n个元素0。将位于数组 V 末尾的 a i a_i ai 个元素都变为 1,也就是将区间 [ i − a i + 1 , i + 1 ] [i-a_i+1, i+1] [iai+1,i+1] 内的元素变为1。想到差分模板,只要每次都把区间 [ i − a i + 1 , i + 1 ] [i-a_i+1, i+1] [iai+1,i+1] 内的元素+1,最后看该位置的元素是否大于0即可。

写的时候连样例都没过,菜菜。本来以为是思路有问题,看了y总的视频讲解,发现也是这思路,是我中间写错了。

代码

N = 200010
T = int(input())
for _ in range(T):
    n = int(input())
    a = [int(x) for x in input().split()] # 操作数组
    a = [0]+a
	# 差分操作
    b = [0]*(n+2) # 差分数组
    for i in range(1,n+1):
        a[i] = min(a[i], i) # 本来是这一步写错了
        b[i-a[i]+1] += 1
        b[i+1] -= 1
    # 求前缀和模板
    x = [0]*(n+1)
    for i in range(1, n+1):
        x[i] = x[i-1]+b[i]
    # 转化为答案
    res = [0]
    for i in range(1, n+1):
        if x[i]>0: res.append(1)
        else: res.append(0)
    print(' '.join(map(str, res[1:])))

注意点

  • 如果用C++写,则每一组测试数据开始,都要清空数组b,用memset(b, 0, (n+1)*4)更节约时间复杂度,否则易超时。
  • 如果用C++写,最后一步将前缀和数组转化为答案时,可以直接遍历前缀和数组,输出print("%d", !!x[i])。用!!操作,可以在0和1之间互相转换。

星期三:二分

AcWing 1460. 我在哪?(每日一题)

思路

没看懂这道题跟二分有什么关系,居然还是简单题。菜菜。

转化题意:找到一个最小长度K,使得字符串中任何一个长度为k的子串都不相同。(这个我都没想到

解法一:暴力。因为 N < = 100 N<=100 N<=100 ,所以四重循环的时间复杂度是 O ( n 4 ) O(n^4) O(n4) ,也不会超时,卡着能过。而且实际的时间复杂度没有这么高,因为有3重循环不需要枚举n次,循环中还有很多break。(暴力做法都没想到的蒟蒻本人

解法二:二分+哈希,可以把时间复杂度下降到 O ( n 2 l o g n ) O(n^2logn) O(n2logn)

  • 有二段性就能用二分做。二段性就是,区间存在一个分界点,使得左边都满足某个性质,右边都不满足(反过来也可以)。用二分法可以二分出二段性的分界点。不过这里的区间需要转化过来,不是很直接,区间是所有k的取值范围。
  • 暴力做法的内3层循环是为了判断是否有任意2个子串相同,可以用哈希来优化。建立一个哈希表,把所有子串往里加,看有没有重复。python里可以用set或者dict来做,c++里可以用unordered_map或者unordered_set来做。

代码

暴力:

# 暴力
n = int(input())
s = input()

for k in range(1, n+1): # k: 1-n
    flag = False # 是否有2个相同的子串
    for i in range(n-k+1): # i:0-n-1
        for j in range(i+1, n-k+1):
            same = True
            for u in range(k):
                if s[i+u]!=s[j+u]:same=False; break
            if same==True: flag=True; break
    if flag==False: print(k); break

二分+哈希:

# 二分+哈希
def check(k): # 判断长度为k的子串有无重复
   global n 
   st = set()
   for i in range(n-k+1):
      if s[i:i+k] in st: return False
      st.add(s[i:i+k])
   return True
      
if __name__=='__main__':
   global n
   n = int(input())
   s = input()
   
   l, r = 1, n
   while l<r:
       mid = (l+r)//2
       if check(mid): r=mid
       else: l=mid+1
   print(r)

星期四:双指针

AcWing 3768. 字符串删减(每日一题)

思路

虽然是个简单题,但也是自己ac的,开心!

题目只给了一个字符串,所以显然是2个指针去遍历同一个字符串,虽然数据范围很小,但是争取达到 O ( n ) O(n) O(n) 的时间复杂度。思路是,指针 i i i 存连续x子串的起始位置,指针 j j j 存连续x子串的结束位置。每次得到一段x子串后,要删去的x字符个数是该子串的长度-2。

代码

n = int(input())
s = input()
i, j, cnt = 0, 0, 0
while i<n and j<n:
    while j+1<n and s[j+1]=='x' and s[i]=='x':
        j+=1
    cnt += max(0, j-i+1-2)
    i=j+1; j=i
print(cnt)

注意

  • 注意读题,只有 x x x 子串不能超过3个,其他字符的子串是没有限制的。

星期五:递推

AcWing 3777. 砖块(每日一题)

思路

读完题目,os:递推是啥(蒟蒻)。

题解思路:最后的结果是,砖块都能变成白色、或者黑色、或者无解。假设从第1块砖开始判断,如果第1块砖满足要求,则不变色;如果不满足,则反转第1、2块砖。这个操作可能会影响第2块砖的颜色。但是我们可以从前往后对每一块砖进行判断,是否需要变色。为了使倒数第2块砖满足要求,所以最后一块砖不能单独操作。如果最后一块砖的颜色和目标色一致,则找到了满足条件的解。

就算每个砖块都需要反转,总操作数为 n-1,不会超过 3n 的限制。

为了记录合理方案,可以开一个数组/列表,每次发生反转,就把下标存进去。

代码

def update(c):
    if c=='W': return 'B'
    return 'W'
    
def check(c): # 尝试把砖块全变成颜色c
    global n
    k = 0
    ops = []
    q = list(a)
    for i in range(n-1):
        if q[i]!=c:
            q[i] = update(q[i]); q[i+1] = update(q[i+1]); k+=1; ops.append(i+1)
    if q[n-1]!=c: return False
    print(k)
    if k>0: print(' '.join(map(str, ops)))
    return True
    
if __name__=='__main__':
    T = int(input())
    for _ in range(T):
        global n
        n = int(input())
        a = input()
        if not check('B') and not check('W'): print(-1)

注意

  • 输出任意合理方案即可。

week2

星期一:递归

AcWing 1497. 树的遍历(每日一题)

思路

看到题目,甚至都不记得前序、后序、中序遍历是啥了,菜菜。

前序 / 中序 / 后序遍历:指的都是根节点的遍历顺序,左子树一定在右子树的前面。

整体思路是由后序遍历和中序遍历的序列构建出树,然后可以用bfs找出层序遍历结果,或者在构建树的过程中保存每一层的所有节点。

首先人工模拟一下构建树的过程:后序遍历中,最后一个节点一定是整棵树的根节点。由于题目中说每个节点的权值不同,所以可以在中序遍历序列中找到对应根节点的位置,此时它左边的节点都是它的左子树,它右边的节点都是它的右子树。再根据中序遍历中,左子树和右子树序列的长度,在后序遍历中也划分出左子树和右子树。以此类推,用递归分别对左子树区间和右子树区间去重复上述操作,直到构建出整棵树。

法一:保存每一层的节点。递归的参数比较多(本菜鸟是想不出来 ),包括中序序列和后序序列的左右端点,此时递归的子树深度。每次在后序序列中找到根节点的权值后,需要在中序序列找到对应的位置,所以新开一个数组p[],来记录中序序列中每个权值对应的下标。还需要开一个二维列表,来存每一个深度的所有节点。递归结束的条件就是左端点的值大于右端点,每次递归做的操作就是把该层的根节点保存在二维列表里。

法二:bfs。与法一不同的是,这里需要存储树,用2个列表l[]r[],分别存对应节点的左孩子和右孩子。递归过程中是保存左右孩子。bfs用一个队列deque实现。

代码

法一:保存每一层的节点。

N = 35
post, mid, p = [], [], [0]*N
level = [[0] for _ in range(N)] # 每一层的所有节点
def build(pl, pr, ml, mr, d):
    if pl>pr: return
    val = post[pr] # root
    k = p[val]
    level[d].append(val)
    build(pl, pl+k-ml-1, ml, k-1, d+1)
    build(pl+k-ml, pr-1, k+1, mr, d+1)

if __name__=='__main__':
    n = int(input())
    post = [int(x) for x in input().split()]
    mid = [int(x) for x in input().split()]
    for i in range(n):
        p[mid[i]] = i
    build(0, n-1, 0, n-1, 0)
    # print
    for l in level:
        if len(l)>1:
            print(' '.join(map(str, l[1:])),end=' ')

法二:bfs。

from collections import deque
N = 35
post, mid, p = [], [], [0]*N
l, r = [0]*N, [0]*N
def build(pl, pr, ml, mr, d):
    if pl>pr: return 0
    val = post[pr] # root
    k = p[val]
    
    l[val] = build(pl, pl+k-ml-1, ml, k-1, d+1)
    r[val] = build(pl+k-ml, pr-1, k+1, mr, d+1)
    return val

def bfs(x):
    q = deque()
    res = []
    q.append(x)
    while q:
        t = q.popleft()
        res.append(t)
        if l[t]>0: q.append(l[t])
        if r[t]>0: q.append(r[t])
    print(' '.join(map(str, res)))
    
if __name__=='__main__':
    n = int(input())
    post = [int(x) for x in input().split()]
    mid = [int(x) for x in input().split()]
    for i in range(n):
        p[mid[i]] = i
    build(0, n-1, 0, n-1, 0)
    bfs(post[n-1]) # 从根节点开始bfs

星期二:并查集

AcWing 1249. 亲戚(每日一题)

思路

是普通的并查集模板,无脑写模板。但是它卡输入,要写stdin.readline()

代码

from sys import *
N = 20010
p = list(range(N))

def find(x):
    if x!=p[x]: p[x] = find(p[x])
    return p[x]

def merge(a, b):
    pa = find(a); pb = find(b)
    if pa!=pb: p[pa] = pb
    
if __name__=='__main__':
    n, m = map(int, stdin.readline().split())
    for _ in range(m):
        a, b = map(int, stdin.readline().split())
        if a!=b: merge(a, b)
    q = int(stdin.readline())
    for _ in range(q):
        c, d = map(int, stdin.readline().split())
        pc = find(c); pd = find(d)
        print('Yes' if pc==pd else 'No')

注意

  • 所有输入都写成stdin.readline(),而且要导入库from sys import *,否则会超时。

星期三:哈希

AcWing 2058. 笨拙的手指(每日一题)

思路

读完题,我觉得这数据范围好大,都开到 1 0 9 10^9 109 了,而且对应的二进制数应该 < = 32 <=32 <=32 位,我觉得int肯定存不了。也不知道怎么哈希,于是寄。

y总说这题数据范围很小,因为二进制数可以用字符串存,32位就显得很小了(我居然完全想反了 orz)。因为题干说有且仅有1位数写错,所以二进制数最多也就枚举32次(二进制位不是0就是1),三进制数最多枚举30*2次(比30次应该还小一点)。题目说存在唯一解,那把所有二进制数和三进制数转换成十进制,只会有一个数出现2次。python可以用set来做哈希表,判断这个数是不是出现过。

代码

法一:直接用set表示哈希表。

from sys import *
import copy
def base(s, b): # 把b进制的字符列表s转换成十进制数返回
    nums = 0; l = len(s)
    for i in range(l):
        nums = nums * b + ord(s[i])-ord('0')
    return nums

if __name__=='__main__':
    s2 = list(input()); s3 = list(input())
    l2 = len(s2); l3 = len(s3)
    st = set()
    for i in range(l2):
        str = copy.deepcopy(s2); str[i] = '0' if str[i]=='1' else '1'
        if len(str)>1 and str[0]=='0': continue # 避免前导0
        st.add(base(str, 2))
    flag = False
    for i in range(l3):
        for j in ['0','1','2']:
            str = copy.deepcopy(s3)
            if str[i]!=j:
                str[i] = j
                if len(str)>1 and str[0]=='0': continue # 避免前导0
                x = base(str, 3)
                if x in st: print(x); flag = True; break
        if flag==True: break

法二:手写哈希表。

from sys import *
import copy
N = 103
h = [-1]*N
def find(x): # 开放寻址法: 找到x的哈希位置t
    t = x%N
    while h[t]!=-1 and h[t]!=x:
        t+=1
        if t==N: t = 0
    return t

def base(s, b): # 把b进制的字符串s转换成十进制数返回
    nums = 0; l = len(s)
    for i in range(l):
        nums = nums*b + ord(s[i])-ord('0')
    return nums

if __name__=='__main__':
    s2 = list(input()); s3 = list(input())
    l2 = len(s2); l3 = len(s3)
    for i in range(l2):
        str = copy.deepcopy(s2); str[i] = '0' if str[i]=='1' else '1'
        if len(str)>1 and str[0]=='0': continue
        x = base(str, 2)
        h[find(x)] = x
    flag = False
    for i in range(l3):
        for j in ['0','1','2']:
            str = copy.deepcopy(s3)
            if str[i]!=j:
                str[i] = j
                # print(len(str),' ', str[0])
                if len(str)>1 and str[0]=='0': continue
                x = base(str, 3)
                # print('x = ', x)
                if h[find(x)]!=-1: print(x); flag = True; break
        if flag==True: break

注意

  • 手写哈希时,哈希表的长度必须是质数。可以用暴力枚举的方法,找到>=数据范围 的最小质数,比如这道题是100003。
  • 哈希和字符串哈希的思路和模板差不多全忘了。我:蒟蒻。

补一下字符串哈希的模板题:

字符串哈希好难,初学时直接无脑背模板,隔了20天再来写模板题,带了些自己的思想,就报错一大堆,又是超时,又是超内存。以下是我犯过的错误:

假设我们把前缀哈希存到数组h[]中。

  • 每次算前缀哈希都要取模,不然就超内存。取模的 Q Q Q 是一个经验值,一般是 2 64 2^{64} 264
  • 把字符串当作 P P P 进制的数,转化成10进制。这里的 P P P 一般是经验值,取131或13331。并且假设不存在冲突。
  • 任何一个字符位都不能映射成0,所以要写ord(str[i]),不需要再减去什么ord('0')
  • 计算子串 [ l , r ] [l,r] [l,r] 的哈希值,需要求 P r − l + 1 P^{r-l+1} Prl+1 。这里也不能每次都去算一个幂次,会超时。正解是在打表前缀哈希时,顺便把 P P P 的次方打表在 p[] 数组里。
  • 计算p[]和计算子串的哈希值时,也需要对 Q Q Q 取模。不然会超内存。感觉取模在很大程度上是避免超内存的。

字符串哈希代码

maxn = 100010; P = 131; Q = 2**64
h, p = [0]*maxn, [1]*maxn
def compute(l, r):
    return (h[r]-h[l-1]*p[r-l+1]) % Q

if __name__=='__main__':
    n, m = map(int, input().split())
    str = ' ' + input()
    # 前缀哈希
    for i in range(1, n+1):
        h[i] = (h[i-1]*P + ord(str[i]))%Q
        p[i] = p[i-1]*P%Q
    
    for _ in range(m):
        l1, r1, l2, r2 = map(int, input().split())
        if compute(l1, r1)==compute(l2, r2): print('Yes')
        else: print('No')

这道模板题如果用dict去做就会TLE。

星期四:单调队列

AcWing 299. 裁剪序列(每日一题)

思路

没思路,看到它是困难题心生畏惧。

看完题解发现还是很难,dp + 双指针 + 单调队列 + multiset(python里还没有这玩意)。

理解一下题意:给定一个序列,要求分成若干段,每段的元素和

首先,结果不存在的情况,是序列中任意一个元素值>=M。其他情况都能算出结果。

使用Dp分析法:

  • 状态表示 f ( i ) f(i) f(i):所有前 i i i 个数的合法划分方案集合;属性是代价的最小值。
  • 状态计算:划分标准是最后一段的长度。对于每个 i i i ,最后一段的长度都是有限的。所以状态转移方程为 f [ i ] = m i n ( f [ i − k ] + a m a x k ) f[i] = min(f[i-k] + a_{maxk}) f[i]=min(f[ik]+amaxk) a m a x k a_{maxk} amaxk 是最后一段的最大值。

如果用朴素dp来做,应该是一个三重循环,第一重循环枚举所有的 i i i,第二重循环枚举最后一段的长度 k k k,第三重循环把最后一段遍历一次,求段中的最大值。时间复杂度是 O ( n 3 ) O(n^3) O(n3)。本题数据范围是 1 0 5 10^5 105 级别,因此时间复杂度要优化到 O ( n l o g n ) O(nlogn) O(nlogn)

接下来是dp优化:

  • 我们可以用双指针算法维护一段区间 [ j , i ] [j, i] [j,i],使得这是一段区间元素和不超过 M M M,且长度最大的区间。此时要设法求出 f [ i ] f[i] f[i]
  • f [ k ] f[k] f[k] 一定是一个不递减的序列,随着k值增大, f [ k ] f[k] f[k] 只能增大或保持不变。所以假设区间 [ j , i ] [j,i] [j,i] 的最大值下标为 k 1 k_1 k1,那么当最后一段与倒数第二段的分界点在 [ j , k 1 − 1 ] [j, k_1-1] [j,k11]内,此时 f [ i ] f[i] f[i] 的最小值一定是 f [ j − 1 ] + a k 1 f[j-1]+a_{k_1} f[j1]+ak1。以此类推,

代码

你可能感兴趣的:(编程题,蓝桥杯,算法,python,数据结构)