蓝桥杯【算法训练】Python 实现

测评链接:https://lx.lanqiao.cn/problemset.page?code=ALGO

印章  100

【题目描述】

        共有n种图案的印章,每种图案的出现概率相同。小A买了m张印章,求小A集齐n种印章的概率

【输入格式】

        一行两个正整数n和m

【输出格式】

        一个实数P表示答案,保留4位小数

【样例】

输入 输出
2 3 0.7500

【评测用例规模与约定】

100% 1 ≤ n, m ≤ 20

【解析及代码】 

这道题的难点在于状态的传递是树状 (二叉树) 的,所以使用 DFS 来进行概率的计算

这个递归函数需要记录:还可以买印章的次数、已集齐的印章、当前状态的基础概率

加上一些边界条件,对这个树状搜索的过程进行剪枝,求所有叶结点的概率和即可

n, m = map(int, input().split())
pe = 1 / n


def solve(way, score, pb):
    ''' way: 还可以买印章的次数
        score: 已集齐的印章
        pb: 当前状态的基础概率'''
    if score == n: return pb
    # 满足则搜索状态
    p = 0
    if pb and way and 0 < n - score <= way:
        # 抽到重复印章的概率
        ps = score * pe
        # 拓展成功状态、失败状态
        p += solve(way - 1, score + 1, pb * (1 - ps))
        p += solve(way - 1, score, pb * ps)
    return p


print(f"{solve(m, 0, 1):.4f}")

除了上述做法,动态规划也可以完成此题 (不会像 DFS 因递归深度过大而报错,但浪费了更多的内存空间)

以 dp[i][j] 表示抽了 j 枚印章之后集齐 i 枚印章的概率,则有以下状态转移方程:

dp[i + 1][j + 1] = \frac{n-i}{n}\cdot dp[i][j] + \frac{i}{n} \cdot dp[i+1][j]

而合法状态需要满足 j \geq i,故可对 状态转移过程、最终答案的搜索 进行剪枝

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

for i in range(1, n + 1):
    # 抽到重复印章的概率
    p = (i - 1) / n
    for j in range(i, m + 1):
        dp[i][j] = dp[i - 1][j - 1] * (1 - p) + dp[i][j - 1] * p

print(f'{sum(dp[-1][n:]):.4f}')

数字游戏  90

【题目描述】

        给定一个 1~N 的排列 a[i],每次将相邻两个数相加,得到新序列,再对新序列重复这样的操作,显然每次得到的序列都比上一次的序列长度少1,最终只剩一个数字。

        例如:

        3 1 2 4

        4 3 6

        7 9

        16

        现在如果知道N和最后得到的数字sum,请求出最初序列a[i],为1~N的一个排列。若有多种答案,则输出字典序最小的那一个。数据保证有解。

【输入格式】

        第1行为两个正整数 n,sum

【输出格式】

        一个1~N的一个排列

【样例】

输入 输出
4 16 3 1 2 4

【评测用例规模与约定】

100% 0 < n ≤ 10

【解析及代码】 

“相邻两个数相加”得到下一个数,典型的杨辉三角。杨辉三角第4层为:contri = [1, 3, 3, 1]

记输出 w = [3, 1, 2, 4],用线性代数的知识可得:contri @ w.T = 16

所以基本的思路就是:

  • 利用杨辉三角每个数都是组合数的性质,求出杨辉三角第 n 层
  • 从字典序最小的序列开始枚举,验证输出是否满足条件

字典序初始化为:list(range(1, n + 1)),求下一个字典序的函数为 next_perm。以 [8, 3, 7, 6, 5, 4, 2, 1] 为例,这个函数完成的工作就是:

  • 从右到左开始查找,因为 3 < 右边第一个数,所以记 3 的索引为 left
  • 从右到左开始查找比 3 大的数,得到 4 的索引记为 right
  • 交换 left 和 right 对应的数,此时序列变为 [8, 4, 7, 6, 5, 3, 2, 1]
  • 可以看到 left 右侧全是逆序的 (即4的右侧),所以逆转 seq[left + 1: ] 得到 [8, 4, 1, 2, 3, 5, 6, 7]
import math


def next_perm(seq):
    ''' 找到下个字典序
        exp: 8 3 7 6 5 4 2 1
               |       |    '''
    n, l = len(seq), -1
    for i in range(n - 2, -1, -1):
        # 找到顺序区的右边界
        if seq[i] < seq[i + 1]:
            l = i
            break
    if l == -1: return None
    for r in range(n - 1, l, -1):
        # 找到交换位
        if seq[l] < seq[r]:
            seq[l], seq[r] = seq[r], seq[l]
            # 逆转逆序区
            seq[l + 1:] = reversed(seq[l + 1:])
            return seq


n, m = map(int, input().split())
# 获取杨辉金字塔的第 n 层, 作为贡献度
contri = [math.comb(n - 1, k) for k in range(n)]

w = list(range(1, n + 1))
# 校验当前排列是否满足条件
while sum(a * b for a, b in zip(w, contri)) != m:
    w = next_perm(w)
print(*w)

无聊的逗  100

【题目描述】

        逗志芃在干了很多事情后终于闲下来了,然后就陷入了深深的无聊中。不过他想到了一个游戏来使他更无聊。他拿出n个木棍,然后选出其中一些粘成一根长的,然后再选一些粘成另一个长的,他想知道在两根一样长的情况下长度最长是多少

【输入格式】

        第一行一个数n,表示n个棍子。第二行n个数,每个数表示一根棍子的长度

【输出格式】

        一个数,最大的长度

【样例】

输入 输出
4
1 2 3 1
3

【评测用例规模与约定】

100% n ≤ 15

【解析及代码】 

典型的背包问题,以 dp[i][j] 表示 “已经考虑前 i 个木棍” 、“最长棒” - “次长棒” = j 时 “最长棒”与“次长棒”等长部分的长度

每次把木棍加入考虑范围时,有几种选择:

  • 不与“最长棒”、“次长棒”叠加
  • 与“最长棒”叠加
  • 与“次长棒”叠加,又分两种情况:“次长棒”仍然是“次长棒”;“次长棒”变为“最长棒”,“最长棒”变为“次长棒”
n = int(input())
sticks = list(map(int, input().split()))
# 要组合出等长棒, “最长棒” - “次长棒” <= sum(sticks) // 2
volumn = sum(sticks) // 2 + 1
# 记录相同的长度
dp = [[-1] * volumn for _ in range(n + 1)]
dp[0][0] = 0


def deliver(i, j, value):
    dp[i + 1][j] = max(dp[i + 1][j], value)


for i, stick in enumerate(sticks):
    for j in range(volumn):
        cur = dp[i][j]
        # 当前等长部分不为负值,即合法
        if cur >= 0:
            # 1. 不与”最长棒”、“次长棒”叠加, 直接传递
            deliver(i, j, cur)

            # 2. 与“最长棒”叠加, ”最长棒“-”次长棒“ 增加, 等长部分不变
            jn = j + stick
            if jn < volumn: deliver(i, jn, cur)

            # 3. 与“次长棒”叠加
            jn = j - stick
            # jn >= 0: ”次长棒“仍为”次长棒“, 等长部分增加 stick
            # jn <  0: “次长棒”变为“最长棒”, “最长棒”变为“次长棒”, 等长部分增加 j
            deliver(i, abs(jn), cur + (stick if jn >= 0 else j))

print(dp[-1][0])

跳马  100

【题目描述】

        一个8×8的棋盘上有一个马初始位置为(a,b),他想跳到(c,d),问是否可以?如果可以,最少要跳几步?

【输入格式】

        一行四个数字a,b,c,d

【输出格式】

        如果跳不到,输出-1;否则输出最少跳到的步数

【样例】

输入 输出
1 1 2 3 1

【评测用例规模与约定】

100% 0 < a,b,c,d ≤ 8,且都是整数

【解析及代码】 

使用邻接表存储这个稀疏图,然后使用 Dijkstra 解决

import heapq

xs, ys, xe, ye = map(lambda num: int(num) - 1, input().split())
# 马可移动的步数
move = [(-1, -2), (-2, -1), (-1, 2), (2, -1), (1, 2), (2, 1), (1, -2), (-2, 1)]
# 棋格索引 -> 行列位置
idx2loc = lambda i: (i // 8, i % 8)
# 行列位置 -> 棋格索引
loc2idx = lambda r, c: r * 8 + c

# 邻接表
adj = [{} for _ in range(64)]
for i in range(64):
    r, c = idx2loc(i)
    for r_, c_ in move:
        # 得到马的下一个位置
        r_ += r
        c_ += c
        # 判断马的下一个位置是否合法
        if 0 <= r_ < 8 and 0 <= c_ < 8:
            adj[i][loc2idx(r_, c_)] = 1


def dijkstra(source, adj):
    ''' 单源最短路径 (不带负权)
        source: 源点
        adj: 图的邻接表'''
    n = len(adj)
    # 记录单源最短路, 未访问标记
    info = [[float('inf'), True] for _ in range(n)]
    info[source][0] = 0
    # 记录未完成搜索的点 (优先队列)
    undone = [(0, source)]
    while undone:
        # 找到离源点最近的点作为中间点 m
        m = heapq.heappop(undone)[1]
        if info[m][1]:
            info[m][1] = False
            # 更新单源最短路
            for i in filter(lambda j: info[j][1], adj[m]):
                tmp = info[m][0] + adj[m][i]
                if info[i][0] > tmp:
                    info[i][0] = tmp
                    heapq.heappush(undone, (tmp, i))
    return info


source = loc2idx(xs, ys)
end = loc2idx(xe, ye)
print(dijkstra(source, adj)[end][0])

数的潜能  100

【题目描述】

        将一个数N分为多个正整数之和,即 N = a1+a2+a3+…+ak,定义 M = a1*a2*a3*…*ak 为N的潜能。

        给定 N,求它的潜能 M。

        由于 M 可能过大,只需求 M 对 5218 取模的余数。

【输入格式】

        输入共一行,为一个正整数N

【输出格式】

        输出共一行,为 N 的潜能 M 对 5218 取模的余数

【样例】

输入 输出
10 36

【评测用例规模与约定】

100% 1\leq N<10^{18}

【解析及代码】 

这个题要求的应该是最大潜能,先试着拆解一些数:

  • 2 = 1 + 1,则因数由 2 → 1 * 1,2 不拆
  • 3 同理不拆,4 可拆成 2 + 2 效果不变
  • 5 = 2 + 3,则因数由 5 → 2 * 3,要拆
  • 6 = 3 + 3 或者 2 + 2 + 2,明显第一种拆法收益更高

总结就是,尽量拆出 3,不拆出 1,然后利用 pow 函数提供的模运算加速便可

n, mod = int(input()), 5218
time, res = divmod(n, 3)

# 输入1,输出1
if n == 1:
    result = 1
# 余数为2,正常处理
elif res == 2:
    result = pow(3, time, mod) * 2
# 余数为1,取出一个3和1合并成4
elif res == 1:
    result = pow(3, time - 1, mod) * 4
# 没有余数,正常处理
else:
    result = pow(3, time, mod)

print(result)

车的放置  100

【题目描述】

        在一个n*n的棋盘中,每个格子中至多放置一个车,且要保证任何两个车都不能相互攻击,有多少中放法 (车与车之间是没有差别的)

【输入格式】

        包含一个正整数n

【输出格式】

        一个整数,表示放置车的方法数

【样例】

输入 输出
2 7

【评测用例规模与约定】

100% n ≤ 8

【解析及代码】 

首先想到的思路是状态压缩,每一个行、列都只能有一辆车

设 n = 8,则二进制 11101010 表示在第 2、4、6、7、8 列的某个位置各有一辆车

一行一行逐层放车,每次只能放一辆,例如第 6 行可以出现 00000000、00101011 等等,但不能出现 11111111,照这个思路先把状态做一个分层

以 n=3 为例,最终 state 如下 (为方便呈现结果,转为二进制 str,实际应用 int)

初始:   {'000': 1}
第1层: {'000': 0, '100': 0}
第2层: {'000': 0, '100': 0, '110': 0, '101': 0}
第3层: {'000': 0, '100': 0, '110': 0, '101': 0, '111': 0}

状态转移的规则是:每经过一行,可以加的车数 <= 1

所以判断条件即是:

last | cur == cur and bin(cur - last).count('1') <= 1

第一个条件用于剔除:类似 last = 010, cur = 101 的情况

第二个条件用于剔除:类似 last = 1100,cur = 1111 的情况,即添加了2辆 (>1) 车

n = int(input())
# 索引 n 处的字典对应前 n 行的填充状态
state = [{} for _ in range(n + 1)]

for c in range(2 ** n):
    m = bin(c).count('1')
    for i in range(m, n + 1): state[i][c] = 0
# 初始化未开始的状态为 1
state[0][0] = 1

for i in range(1, n + 1):
    for cur in state[i]:
        v = 0
        for last in state[i - 1]:
            if last | cur == cur and bin(cur - last).count('1') <= 1:
                v += state[i - 1][last]
        state[i][cur] = v

print(sum(state[-1].values()))

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