测评链接:https://lx.lanqiao.cn/problemset.page?code=ALGO
【题目描述】
共有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 枚印章的概率,则有以下状态转移方程:
而合法状态需要满足 ,故可对 状态转移过程、最终答案的搜索 进行剪枝
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}')
【题目描述】
给定一个 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
所以基本的思路就是:
字典序初始化为:list(range(1, n + 1)),求下一个字典序的函数为 next_perm。以 [8, 3, 7, 6, 5, 4, 2, 1] 为例,这个函数完成的工作就是:
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)
【题目描述】
逗志芃在干了很多事情后终于闲下来了,然后就陷入了深深的无聊中。不过他想到了一个游戏来使他更无聊。他拿出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])
【题目描述】
一个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])
【题目描述】
将一个数N分为多个正整数之和,即 N = a1+a2+a3+…+ak,定义 M = a1*a2*a3*…*ak 为N的潜能。
给定 N,求它的潜能 M。
由于 M 可能过大,只需求 M 对 5218 取模的余数。
【输入格式】
输入共一行,为一个正整数N
【输出格式】
输出共一行,为 N 的潜能 M 对 5218 取模的余数
【样例】
输入 | 输出 |
10 | 36 |
【评测用例规模与约定】
100% |
【解析及代码】
这个题要求的应该是最大潜能,先试着拆解一些数:
总结就是,尽量拆出 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)
【题目描述】
在一个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()))