2025蓝桥杯PythonA组省赛题解

2025蓝桥杯PythonA组省赛题解

前往github博客获得更佳观看体验

一定注意,由于在编写本题解时还没有在线题目。所以:

本题解仅供参考,在题意、思路、code上都可能发生错误!

A. RGB三色

题意

我们可以用三个0~255之间的数(r,g,b)来表示一个颜色,如(0,0,255) 表示蓝色。

那么请问所有的颜色中,有多少种颜色是“偏蓝色”

我们定义当且仅当$b > r $ 并且 b > g b > g b>g ( r , g , b ) (r,g,b) (r,g,b) 是偏蓝色的。

思路 & 代码

直接暴力枚举! 复杂度 25 6 3 = 16777216 256^3 = 16777216 2563=16777216

ans = 0
for r in range(256):
    for g in range(256):
        for b in range(256):
            if(b > g and b > r):
                ans += 1
print(ans) # 5559680

方法二:

使用公式计算:

如果我们给定b,那么r和g可以取 [ 0 , b − 1 ] [0,b-1] [0,b1] 中的任何一个值,有 b 2 b^2 b2 种可能。于是我们就有如下公式:
a n s = ∑ 255 b = 1 b 2 ans = \underset{b = 1}{\overset{255}{\sum}} b^2 ans=b=1255b2

ans = 0
for b in range(1,256):
    ans += b * b
print(ans) # 5559680

B. IPv6的缩写长度

题意

给出IPv6的缩写规则:

IPv6由8段组成,每段4个16进制数。

省略规则如下:

  1. 对于一段的四个十六进制数,前导零可以省略。
  2. 如果一段中只包含零,必须保留一个零。
  3. 可以将一段连续的0省略,用:: 替代,但是整个IPv6只能缩写一段。

下面给出一些例子

缩写前缩写后
2001:0db8:0000:0000:0000:ff00:0042:83292001:db8::ff00:42:8329
0000:0000:0000:0000:0000:0000:0000:0001::1
0000:0000:0000:0000:0000:0000:0000:0000::
ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffffffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
2001:0db8:85a3:0000:0000:8a2e:0370:73342001:db8:85a3::8a2e:370:7334

那么请问:

所有的IPv6地址,把他们的长度加起来是多少?

由于答案太大,我们对 1 0 9 + 7 10^9+7 109+7 取模。

思路

直接枚举肯定是不可以的。

我们考虑按段分类枚举:

对于八个段中的每一个段,我们分为以下五种情况:

  1. 这一段是0 ,共1个数符合情况
  2. 这一段是一位数,即三位前导零,共(000f - 0 = 15) 个数符合情况
  3. 这一段是两位数,即两位前导零,共(00ff - 000f = 15*16) 个数符合情况
  4. 这一段是三位数,即一位前导零,共(0fff - 00ff = 15*16*16) 个数符合情况
  5. 这一段是四位数,没有前导零,共(ffff - 0fff = 15*16*16*16) 个数符合情况

当我们固定了八段所有的情况后, 他的长度就可以求出来了,于是我们对每段枚举这五种情况。循环的时间复杂度是 5 8 5^8 58

之后我们假设这个IPv6地址如下:a0:a1:a2:a3:a4:a5:a6:a7

那么我们就可以计算得到在缩写前的总长度是 l e n g t h p r e = ( l 0 + l 1 + l 2 + . . . + l 7 ) + 7 length_{pre} = (l_0+l_1+l_2+...+l_7) + 7 lengthpre=(l0+l1+l2+...+l7)+7 (七个冒号) (这里 a i = 0 a_i = 0 ai=0 l i = 1 l_i=1 li=1 ,其他时候 l i = a i l_i = a_i li=ai )

在缩写时我们对每段连续0区间进行计算,得到他的缩小程度。我们取其中的最大值 l e n g t h o m i t length_{omit} lengthomit即可。

如何计算?假设我们得到了一个区间 [ l , r ] [l,r] [l,r]

  1. 如果 l = 0  and  r = 7 l = 0 \text{ and }r = 7 l=0 and r=7 , l e n g t h o m i t = 13 length_{omit} = 13 lengthomit=13
  2. 若不满足第一项:如果 l = 0  or  r = 7 l = 0 \text{ or } r = 7 l=0 or r=7 , l e n g t h o m i t = 2 ∗ ( r − l + 1 ) − 2 length_{omit} = 2 * (r - l + 1) - 2 lengthomit=2(rl+1)2
  3. 如果不满足第一、二项; l e n g t h o m i t = 2 ∗ ( r − l + 1 ) − 1 length_{omit} = 2 * (r - l + 1) - 1 lengthomit=2(rl+1)1

这样我们就计算出来这个地址的长度,那么这个地址的总数则是每段所包含的数的数量,把他们乘到一起。 c n t a l l = Π i = 0 7 c n t ( a i ) cnt_{all} = \Pi_{i = 0}^7 cnt(a_i) cntall=Πi=07cnt(ai)

那么整个这个类型的IPv6的地址总长度,当然就是 c n t a l l ∗ ( l e n g t h p r e − l e n g t h o m i t ) cnt_{all} * (length_{pre} - length_{omit}) cntall(lengthprelengthomit) 了。

注:我们枚举八个段,可以使用八重循环(太丑陋!)

也可以直接枚举一个“五进制数”,从0 到 5 8 5^8 58 , 然后对他进行进制拆分

代码

我们在调试时可以把N设置的比较小,然后输出所有的情况来查看计算是否有问题。

N = 8
ans = 2 # 这里直接把全零的情况提取出来,后面的range从1开始
cnt = [1,15,15*16,15*16*16,15*16*16*16]
MOD = int(1e9+7)
for x in range(1,5**N):
    a = [] # a[i]表示第i位是几
    for right in range(N):
        a.append(x%5)
        x //= 5

    # 接下来找连续的0串
    left = -1
    length_omit = 0 
    for right in range(N):
        # 这里枚举的区间是(left,right],区间长度是right-left
        if(a[right] == 0):
            if(left == -1 or right == N-1):
                length_omit = max(length_omit,2 *(right-left)-2)
            else:
                length_omit = max(length_omit,2 *(right-left)-1)
        else :
            left = right
    length_pre = 0
    for i in range(N):
        if(a[i] == 0):
            length_pre += 1
        else:
            length_pre += a[i]
    length_pre += (N-1)
    cnt_all = 1
    for i in range(N):
        cnt_all = cnt_all * cnt[a[i]] % MOD
    # print(a)
    # print(length_pre,length_omit)
    ans += cnt_all * (length_pre - length_omit) % MOD
    ans %= MOD
print(ans) # 905307083

C. 2025

题意

给出 n , w n,w n,w, 求出n行m列的2025矩阵:

  1. 第1行是2025的不断重复

  2. 从第2行开始,每行都是上一行左移一位

n = 4,m = 10时如下:

2025202520
0252025202
2520252025
5202520252

思路

我们发现,左下-右上对角线上的元素是相同的。他们满足 j + i = c j + i = c j+i=c ,即列和行的和是定值。

代码

import sys
Input = sys.stdin.readline
n,m = map(int,Input().split())

ch = [2,0,2,5]
for i in range(n):
    for j in range(m):
        print(ch[(i+j)%4],end="")
    print("")

D 1到n的二进制拼接

题意

把1到n这些数,转换为二进制,然后进行拼接操作。

问怎样拼接能使得数值尽可能大? 把这个01串再转换回十进制。

例如:n = 3时, 1 , 2 , 3 1,2,3 1,2,3 的二进制分别为 1 , 10 , 11 1,10,11 1,10,11 ,进行拼接后最大为11110 ,转换为十进制为30

n ≤ 10000 n \le 10000 n10000

思路

把代码分为三部分,然后每部分分别处理即可。

  1. 第一步,二进制转换,我们使用字符串来保存。

对每个数进行按位分离(每位为2),然后拼接成字符串即可。

  1. 第二步,找最大的拼接。

这是一个经典例题最大数(leetcode) , 简单地说就是对字符串进行排序,排序规则是“对于相邻的两个串, 如果 s a + s b > s b + s a s_a + s_b > s_b + s_a sa+sb>sb+sa,那么 s a s_a sa s b s_b sb 的前面" ,详细证明可以看leetcode例题的题解。

  1. 第三步,对拼接后的字符串进行二进制转十进制。

从最高位枚举然后乘起来即可,我们python不需要考虑溢出问题。

代码

n = int(input())

s_lst = []
for i in range(1,n+1):
    a = []
    x = i
    while(x):
        a.append(str(x%2))
        x //= 2
    s_lst.append("".join(a[::-1])) # 注意这里要倒过来

# 自定义排序推荐重写类的___lt__ 方法
class Node():
    def __init__(self,s):
        self.s = s
    def __lt__(self,other):
        return self.s+other.s > other.s+self.s

node_lst = []
for i in s_lst:
    node_lst.append(Node(i))
# 排序
node_lst.sort()
# 拼接
ans_str = "" 
for i in node_lst:
    ans_str += i.s

# 进制转换
ans = 0
for i in ans_str:
    ans = ans * 2 + int(i)
print(ans)

E. 彩色瓶子

题意

给你n个瓶子,以及整数k。

每隔k个他们的瓶子颜色就是一样的。即对于所有的i,满足 c o l o r i = c o l o r i + k color_i = color_{i + k} colori=colori+k , 每个瓶子中都有一定量的水。记作 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an

你可以任意次地将第 i i i个瓶子中的整数量的水倒入相同颜色的瓶子 j j j中 ,唯一的要求是 i < j i < j i<j

那么请问,在任意次操作后, 所有瓶子中含水量的最小值,最大的可能是多少。

思路

最大的最小值,我们要首先尝试二分答案,在本题,二分求瓶子的最小值x。

我们的check函数应该是这样的:

我们对每个水瓶进行如下处理:

  1. 如果当前水瓶的水超过x,那么把多余的水存下来,供后面使用。
  2. 如果当前水瓶的水不够x,则用之前相同颜色水瓶存的水补满,补不满则宣告这个x是不可能的。

如果check(x) == True,我们则可以考虑令x更大一些

如果check(x) == False ,我们则考虑令x更小一些。

于是这样就能找到满足check(x) == True的最大的x了。

代码

import sys
input = sys.stdin.readline
n,k = list(map(int,input().split()))
a = list(map(int,input().split()))


def check(x):
    global n,a,k
    have = [0] * k 
    for i in range(n):
        if(a[i] >= x):
            have[i % k] += a[i] - x
        else:
            if(have[i%k]+a[i] >= x):
                have[i%k] -= x - a[i]
            else:
                return False
    return True

l = 1
r = int(1e9)
while(l < r):
    mid = (l + r + 1) // 2
    if(check(mid)):
        l = mid
    else:
        r = mid - 1
print(l)

F.拼好数

题意

给出n个正整数。 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an

你可以将他们拼接为若干个组。 要求如下:

  1. 每组最多由三个数拼接而成
  2. 拼接完成后“6”的数位数量不少于6个。

那么请问最多可以分为多少组?

n ≤ 1000 n \le 1000 n1000

思路

我们不关心 a a a数组之间的顺序关系,并且只关心 a i a_i ai 中包含6的数量。所以我们可以统计一下出现 ( 0 , 1 , 2 , 3 , 4 , 5 , 6 + ) (0,1,2,3,4,5,6+) (0,1,2,3,4,5,6+)个6的数分别有多少个。

注意

  1. 当一个数中出现了不少于6个6时,那么只需要一个这个数就可以使得答案成立。我们可以把这一类数归为同一类。这一类不需要参与计算,他们单独一组就是最优的。
  2. 当一个数中出现了0个6时,这个数是没有用的,我们可以直接扔掉。

于是我们就得到了cnt数组。其中 c n t [ i ] cnt[i] cnt[i] 表示出现了i个6有多少个数。

于是我们就可以考虑使用搜索来求了,那么如何进行状态转移?action为选择了一个合适的组。我的做法是预处理出所有可选的operator。得到如下数组(直接三重循环即可):

ope = [[0, 1, 5], [0, 2, 4], [0, 2, 5], [0, 3, 3], [0, 3, 4], [0, 3, 5], [0, 4, 4], [0, 4, 5], [0, 5, 5], [1, 1, 4], [1, 1, 5], [1, 2, 3], [1, 2, 4], [1, 2, 5], [1, 3, 3], [1, 3, 4], [1, 3, 5], [1, 4, 4], [1, 4, 5], [1, 5, 5], [2, 2, 2], [2, 2, 3], [2, 2, 4], [2, 2, 5], [2, 3, 3], [2, 3, 4], [2, 3, 5], [2, 4, 4], [2, 4, 5], [2, 5, 5], [3, 3, 3], [3, 3, 4], [3, 3, 5], [3, 4, 4], [3, 4, 5], [3, 5, 5], [4, 4, 4], [4, 4, 5], [4, 5, 5], [5, 5, 5]]
# 我们可以将这里的0视为不选,也可以视为选择一个包含0个6的数,因为“包含0个6的数”可以是任意多个。

然后我们就可以通过这些操作来进行状态转移了。

一个状态可以描述为五元组 ( c n t [ 1 ] , c n t [ 2 ] , . . . , c n t [ 5 ] ) (cnt[1],cnt[2],...,cnt[5]) (cnt[1],cnt[2],...,cnt[5]) 的取值。

之后我们进行搜索即可。

优化

本体的关键在于优化,下面详细讲一下优化策略:

  1. 最刚需的优化就是记忆化搜索:

我们发现对于某个状态,我们可能会重复了很多很多次,比如初始状态为 ( 0 , 0 , 3 , 3 , 3 ) (0,0,3,3,3) (0,0,3,3,3) ,那么如下两种序列都可以得到 ( 0 , 0 , 1 , 1 , 3 ) (0,0,1,1,3) (0,0,1,1,3) , 于是我们就会计算两次(甚至更多次)状态 ( 0 , 0 , 1 , 1 , 3 ) (0,0,1,1,3) (0,0,1,1,3)

序列一:
( 0 , 0 , 3 , 3 , 3 ) → o p e ( 0 , 3 , 3 ) ( 0 , 0 , 1 , 3 , 3 ) → o p e ( 0 , 4 , 4 ) ( 0 , 0 , 1 , 1 , 3 ) ( 0 , 0 , 3 , 3 , 3 ) → o p e ( 0 , 3 , 4 ) ( 0 , 0 , 1 , 3 , 3 ) → o p e ( 0 , 4 , 4 ) ( 0 , 0 , 1 , 1 , 3 ) (0,0,3,3,3) \overset{ope(0,3,3)}{\to} (0,0,1,3,3)\overset{ope(0,4,4)}{ \to} (0,0,1,1,3) \\ (0,0,3,3,3) \overset{ope(0,3,4)}{\to} (0,0,1,3,3)\overset{ope(0,4,4)}{ \to} (0,0,1,1,3) \\ (0,0,3,3,3)ope(0,3,3)(0,0,1,3,3)ope(0,4,4)(0,0,1,1,3)(0,0,3,3,3)ope(0,3,4)(0,0,1,3,3)ope(0,4,4)(0,0,1,1,3)
所以我们每次计算出一个状态的结果后,立刻把他放到字典当中。 d i c ( s t a t e ) = v a l u e dic(state) = value dic(state)=value .

当下次再一次遇到这个状态时,我们就可以直接返回这个值了。

  1. 剪枝

本题的剪枝主要是计算答案的上界。 假设我们要从状态 u u u 转移到状态 v v v v = ( s 1 , s 2 , s 3 , s 4 , s 5 ) v = (s_1,s_2,s_3,s_4,s_5) v=(s1,s2,s3,s4,s5)

那么状态v的贡献一定不会超过这两个值:

首先,必须选择至少两个数来组成一组,所以
a n s v ≤ s 1 + s 2 + . . . s 5 2 ans_v \le \frac{s_1+s_2+...s_5}{2} ansv2s1+s2+...s5

其次,每组必须有至少6个6.所以
a n s v ≤ s 1 + 2 ∗ s 2 + . . . + 5 ∗ s 5 6 ans_v \le \frac{s_1 + 2 * s_2 + ...+ 5*s_5}{6} ansv6s1+2s2+...+5s5
当我们在取到等号时,仍然无法做到 a n s v + 1 > a n s u ans_v + 1 > ans_u ansv+1>ansu 即更新状态u的答案,那么我们再对v搜索就是徒劳的。这时候应该直接跳过本次搜索。

代码

import sys
input = sys.stdin.readline
n = int(input()) 
a = list(map(int,input().split()))
# n = 1000
# a = [6,66,666,6666,66666] * 200
ope = []
for i in range(6):
    for j in range(i,6):
        for k in range(j,6):
            if(i + j + k >= 6):
                ope.append((i,j,k))

cnt = [0] * 7
for i in range(n):
    x = a[i]
    cc = 0
    while(x):
        if(x % 10 == 6):
            cc += 1
        x //= 10
    cnt[min(cc,6)] += 1

cnt[0] = int(1e12)
# 给cnt[0]一个足够大的值,代表我们可以选择任意数量的无效数。
# 可以减少特判

visit = {} # 记忆化字典
def dfs():    
    # 如果当前完全凑不够6个6就该返回了。
    cc = 0
    for i in range(1,6):
        cc += cnt[i] * i
    if(cc < 6):
        return 0
    ans = 0 # 当前dfs最大可能的取值
    gt = visit.get(tuple(cnt[1:6]),None)
    if(gt != None): # 如果之前计算过,直接返回
        return gt
    for x,y,z in ope:
        ## 尝试选择这组ope
        cnt[x] -= 1
        cnt[y] -= 1
        cnt[z] -= 1
        ok = 0 # 这组ope是否是合法的     
        if(cnt[x] >= 0 and cnt[y] >= 0 and cnt[z] >= 0):
            ok = 1 # 是合法的
        if(ok):
            # 两个剪枝
            c1 = 0
            for i in range(1,6):
                c1 += cnt[i]
            # 是否通过剪枝
            c2 = 0
            for i in range(1,6):
                c2 += cnt[i] * i
            if(c1//2 + 1 > ans and c2 //6 + 1 > ans):
                ans = max(ans,dfs()+1)
        # 不管有没有进入dfs,都要返回现场
        cnt[x] += 1
        cnt[y] += 1
        cnt[z] += 1

    # 最后记得更新visit
    visit[tuple(cnt[1:6])] = ans
    return ans
print(dfs()+cnt[6]) #  别忘了加cnt[6]

"""
6
6 66 666 6666 66666 666666

4
666 66 666666 123456

6
6 6 6 6 6 6
"""

G. 登山

题意

有一个n行m列的矩阵 h h h 。描绘了一个地图上每个格子的高度。 h [ i ] [ j ] h[i][j] h[i][j] 表示第 i i i行第 j j j 列的高度。

你当前的节点位置为 ( p , q ) (p,q) (p,q)每次可以选择下面操作之一:

  1. 选择一个 i > p i>p i>p 的节点,并且 h [ i ] [ q ] < h [ p ] [ q ] h[i][q] < h[p][q] h[i][q]<h[p][q] , 走到节点 ( i , q ) (i,q) (i,q)
  2. 选择一个 i < p i<p i<p 的节点,并且 h [ i ] [ q ] > h [ p ] [ q ] h[i][q] > h[p][q] h[i][q]>h[p][q] , 走到节点 ( i , q ) (i,q) (i,q)
  3. 选择一个 j > q j>q j>q 的节点,并且 h [ p ] [ j ] < h [ p ] [ q ] h[p][j] < h[p][q] h[p][j]<h[p][q] , 走到节点 ( p , j ) (p,j) (p,j)
  4. 选择一个 j < q j<q j<q 的节点,并且 h [ p ] [ j ] < h [ p ] [ q ] h[p][j] < h[p][q] h[p][j]<h[p][q] , 走到节点 ( p , j ) (p,j) (p,j)

设你从(x,y) 节点出发,能够到达的最高的高度为 v x , y v_{x,y} vx,y , 本题你需要求的是从所有的节点出发能到达的最高高度的总和,数学表示为:
∑ n x = 1 ∑ m y = 1 v i , j \underset{x = 1}{\overset{n}{\sum}}\underset{y = 1}{\overset{m}{\sum}} v_{i,j} x=1ny=1mvi,j
保证 1 ≤ n , m ≤ 1 0 4 , n ∗ m ≤ 1 0 6 1 \le n,m \le 10^4 , n*m \le 10^6 1n,m104,nm106

思路

首先我们可以发现:

节点之间的路径是无向的,即如果可以从 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) 移动到 ( x 2 , y 2 ) (x_2,y_2) (x2,y2) ,那么反过来一定可以从 ( x 2 , y 2 ) (x_2,y_2) (x2,y2) 移动到 ( x 1 , y 1 ) (x_1,y_1) (x1,y1)

即节点之间的边均为无向边。那么又可以得到如下的结论:

如果 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) ( x 2 , y 2 ) (x_2,y_2) (x2,y2) 之间有(无向)边, 那么 v x 1 , y 1 = v x 2 , y 2 v_{x_1,y_1} = v_{x_2,y_2} vx1,y1=vx2,y2

于是我们边很容易地想到了染色法求连通块的属性,具体为使用bfs/dfs搜索连通块,或者使用并查集来维护点的联通关系。

一个会超时的反例

以dfs为例,我们的伪代码如下:

height = 0
cnt = 0
def dfs(x,y):
    height = max(height,h[x][y])
    cnt += 1
    for i in range(n):
        if 满足条件 and vis == 0:
            vis[x2][y] = 1
            dfs(x2,y)
    for i in range(m):
        if 满足条件 and vis == 0:
            vis[x][y2] = 1
            dfs(x,y2)
最终这个连通块的贡献就是height * cnt

该dfs的复杂度如下:

我们对每个节点都要进入一次dfs循环( v i s [ x ] [ y ] vis[x][y] vis[x][y]从0到1的那一次),因此一共有 n ∗ m n*m nm 个dfs。

而对于每一个dfs函数,我们都使用了两个循环来找答案。

总的时间复杂度是 O ( n ∗ m ∗ ( n + m ) ) O(n*m*(n+m)) O(nm(n+m))

在本题这是不被允许的。

复杂度的问题在于,在我们染色时,很多时候遇到的都是之前已经遇到过的节点。而这些节点只会花时间判断if为False然后结束,并没有高效地更新答案。

正确的思路

我们改用并查集维护颜色,并尝试借用单调属性进行优化。过程如下:

我们发现,本体的难点在于计算出哪些节点之间是联通的。

我们先逐行分析,取出第一行 h [ 1 ] h[1] h[1] , 我们叫他 a a a , 那么根据行动规则:

a [ i ] a[i] a[i] 会和所有 j < i , a [ j ] > a [ i ] j < i, a[j] > a[i] j<i,a[j]>a[i] 的节点连通。(说人话就是这个节点会和前面所有比他大的节点连通) 。

假设我们的a数组如下:

1 3 5 4 7 2 9 8 10 \text{1 3 5 4 7 2 9 8 10} 1 3 5 4 7 2 9 8 10

我们枚举每个元素。下面详细讲解过程:

  1. 遇到了1,3,5 。他们之间不能连接。此时有三个小组(1),(3),(5)
  2. 遇到了4, 他可以和(5) 连接 ,此时三个小组(1),(3),(5,4)
  3. 遇到了7,他不能和任何组(如果7比一个组中的最大值要大,那么就可以和这一组连接)连接,此时四个小组(1),(3),(5,4),(7)
  4. 遇到了2,能和(3),(5,4),(7)连接, 这三个组合并,现在两个组(1),(3,5,4,7,2)
  5. 遇到了9,无法连接,现在三个组(1),(3,5,4,7,2),(9)
  6. 遇到了8,和9连接,(1),(3,5,4,7,2),(9,8)
  7. 遇到了10,无法连接(1),(3,5,4,7,2),(9,8),(10)

这样我们就分好了组。同组之间的节点可以相互到达。

接下来对每行都进行如上处理。然后对每列再进行处理。行列合并后就得到了连通块。

如何保证时间复杂度?

我们知道我们只关心每组最大的那个成员,所以我们称它为“代表”

每组选择一个代表,可以证明他们本身就是有序关系的(如果后面一个组的max小于前面某个组的max,那么就会触发合并操作),于是我们倒序地找所有比x大的组,将这些组和x一起合并。

于是总的复杂度是 O ( n ∗ m ) O(n*m) O(nm) 的。

代码

本题代码来自我的队友csdn主页

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

mp = [[0] * (m+5) for _ in range(n+5)]
for i in range(1, n+1):
    mp[i][1:m+1] = map(int, input().split())

f = [0] * (n*m+5)
maxh = [0] * (n*m+5)
for i in range(1, n*m+1):
    f[i] = i
    maxh[i] = mp[(i-1)//m+1][(i-1)%m+1]

def to_idx(x, y):
    return (x-1)*m+y-1+1

def findf(x):
    if f[x] == x:
        return x
    else:
        f[x] = findf(f[x])
        return f[x]

def merge(u, v):
    fu, fv = findf(u), findf(v)
    maxh[fu] = max(maxh[fu], maxh[fv])
    f[fv] = fu


for i in range(1, n+1):
    stack = list()  # 高度 并查集位置
    for j in range(1, m+1):
        idx = to_idx(i, j)
        while stack and stack[-1][0] > mp[i][j]:
            merge(stack[-1][1], idx)
            stack.pop()
        stack.append((mp[i][j], idx))

for j in range(1, m+1):
    stack = list()  # 高度 并查集位置
    for i in range(1, n+1):
        idx = to_idx(i, j)
        while stack and stack[-1][0] > mp[i][j]:
            merge(stack[-1][1], idx)
            stack.pop()
        stack.append((mp[i][j], idx))

ans = 0
for i in range(1, n+1):
    for j in range(1, m+1):
        fa = findf(to_idx(i, j))
        ans += maxh[fa]
    #     print(maxh[fa], end=" ")
    # print()

print(ans/(n*m))

"""
2 2
1 4
4 3

2 3
2 4 1
4 2 3

"""

H.原料采购

题意

在一条直线道路上,你在道路的最左边(原点), 在原点的右边有n个采购点。

对于第i个采购点,他的单价是 a i a_i ai元 ,一共有 b i b_i bi 个物品, 距离原点 c i c_i ci

你需要采购总共m个物品。公路的行驶单价为 o o o ,这意味着每当你在公路上行走了 2 ∗ x 2*x 2x米。你就会花费 o ∗ x o * x ox 元。(这里的单价是往返单价,所以你到达距离x再返回,则会 花费 o ∗ x o*x ox 元 )

那么请问,购买恰好m个物品,最少需要花费多少钱。

思路

很经典的反悔贪心。(解决的问题是可能会在未来遇到更好条件(在本体是更低的单价),而可能又必须在现在就执行的问题)

假设我们要在前i个采购点采购物品。那么路费就是 o ∗ c i o * c_i oci

此时我们可以在这i个采购点中选择单价最小的m个物品来购买。假设购买清单如下: { ( c o s t t , b u y t ) } k \{(cost_t,buy_t)\}_k {(costt,buyt)}k

表示买了 b u y t buy_t buyt 个单价为 c o s t t cost_t costt 的物品,这样的元组一共有k个(即有k种不同的单价)。

我们便可以算出他们的总花费 C O S T i = ∑ t = 1 k c o s t t ∗ b u y t COST_i = \sum_{t=1}^k cost_t * buy_t COSTi=t=1kcosttbuyt (这里体现了贪心,即尽量选择单价小的物品)

当我们再多考虑一个采购点时,我们先假设购买第 i + 1 i+1 i+1 个采购点的所有物品。 那么这时候可能会发生购买的物品超过了m。这时候我们需要“退货” ,假设需要退货 r e f u n d i refund_i refundi件物品, 我们就选择已购买的物品中单价最大的 r e f u n d i refund_i refundi 个退货就好了。找单价最大的物品可以用单调队列(也就是堆)实现(这里体现了反悔)

重复此过程,答案即为 ∑ n i = k ( C O S T i + o ∗ c i ) \underset{i = k} {\overset{n}\sum} (COST_i + o * c_i) i=kn(COSTi+oci) , 这里的i要从k开始,k的含义为,全买下来所有前k个采购点的物品,恰好大于等于m个物品。(即k-1就不够了)

代码

import sys
from queue import PriorityQueue
input = sys.stdin.readline
INlist = lambda: list(map(int,input().split()))
n,m,o = INlist()
cgd = [] # 采购点
for i in range(n):
    cgd.append(INlist())
class Node():
    def __init__(self,buy,cost):
        self.buy = buy
        self.cost = cost
    def __lt__(self,other):# 花费高的在前面,优先出队
        return self.cost > other.cost 

pq = PriorityQueue()

buy_cnt = 0 #买了几件物品了
cost_cnt = 0 # 购买花费了多少
ans = float('inf') # 总花费
for i in range(n):
    pq.put(Node(cgd[i][1],cgd[i][0]))# 全买下来
    buy_cnt += cgd[i][1]
    cost_cnt += cgd[i][0] * cgd[i][1]
    if(buy_cnt > m):
        while(buy_cnt > m):
            node = pq.get()
            t = min(node.buy,buy_cnt - m)
            buy_cnt -= t
            cost_cnt -= node.cost * t
            node.buy -= t
            if(node.buy > 0) : # 还有剩余,还要购买
                pq.put(node)
        ans = min(ans,cost_cnt+o*cgd[i][2]) # 总花费=商品花费+路程花费

print(ans)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值