文章目录
1. 枚举
1.3 称硬币(POJ1013)
有12枚硬币。其中有11枚真币和1枚假币。假币和真 币重量不同,但不知道假币比真币轻还是重。现在, 用一架天平称了这些币三次,告诉你称的结果,请你 找出假币并且确定假币是轻是重(数据保证一定能找 出来)。
输入
每组数据有三行,每行表示一次称量的结果。银币标号 为A-L。每次称量的结果用三个以空格隔开的字符串表示: 天平左边放置的硬币 天平右边放置的硬币 平衡状态。其中平衡状态用up'',
down’’, 或 ``even’'表示, 分 别为右端高、右端低和平衡。天平左右的硬币数总是相等的。
输出
输出哪一个标号的银币是假币,并说明它比真币轻还是重。
输入样例
ABCD EFGH even
ABCI EFJK up
ABIJ EFGH even
输出样例
K is the counterfeit coin and it is light.
解题思路:
对于每一枚硬币先假设它是轻的,看这样是否符合 称量结果。如果符合,问题即解决。如果不符合,就 假设它是重的,看是否符合称量结果。把所有硬币都 试一遍,一定能找到特殊硬币。
假设是在CoinisFake函数里,用第二个输入bool变量,作为判断的标准。值得学习这种思想。
代码
# 创建矩阵的方法
left = [[0] for i in range(3)]
right = [[0] for i in range(3)]
result = [[0] for i in range(3)]
def CoinisFake(curr, light, left, right,result):
for i in range(3):
if light:
pleft = left[i]
pright = right[i]
else:
pleft = right[i]
pright = left[i]
while result[i][0]:
if result[i][0]=='u':
if curr not in pright:
return False
break
if result[i][0]=='d':
if curr not in pleft:
return False
break
if result[i][0]=='e':
if curr in pleft or curr in pright:
return False
break
return True # 注意indent!需要在i循环之外。
left[0] = ['A','B','C','D']
left[1] = ['A','B','C','I']
left[2] = ['A','B','I','J']
right[0] = ['E','F','G','H']
right[1] = ['E','F','J','K']
right[2] = ['E','F','G','H']
result[0] = 'even'
result[1] = 'up'
result[2] = 'even'
list_AtoL = list(map(chr, range(ord('A'), ord('L') + 1) ) )
# 注意:使用ord作为编码,得到A到L的list
for curr in list_AtoL:
if CoinisFake(curr, True, left, right,result):
print(curr, " is the counterfeit coin and it is light.")
elif CoinisFake(curr, False, left, right,result):
print(curr, " is the counterfeit coin and it is heavy.")
1.4 熄灯问题
解题思路:
用二进制位来记录灯的开关(01)变动。
也可以用位运算来记录全部的状态。
6. 动态规划(一)
6.1 数字三角形
输入格式:
5 //三角形行数。下面是三角形
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
要求输出最大和
递归思路
解题思路:
动态规划。需要反转思路,类似于递归,但并不是由前推往后,而是由后推往前的过程。
用二维数组存放数字三角形。
D ( r , j ) D( r, j) D(r,j) : 第 r r r行第 j j j 个数字( r , j r,j r,j从1开始算)
M a x S u m ( r , j ) MaxSum(r, j) MaxSum(r,j) : 从 D ( r , j ) D(r,j) D(r,j)到底边的各条路径中,最佳路径的数字之和。
问题:求 M a x S u m ( 1 , 1 ) MaxSum(1,1) MaxSum(1,1), 是典型的递归问题。
D ( r , j ) D(r, j) D(r,j)出发,下一步只能走 D ( r + 1 , j ) D(r+1,j) D(r+1,j)或者 D ( r + 1 , j + 1 ) D(r+1, j+1) D(r+1,j+1)。故对于 N N N行的三角形:
if ( r == N) //只能是本身
MaxSum(r,j) = D(r,j)
else // 由下面两者决定
MaxSum( r, j) = Max{ MaxSum(r+ 1,j), MaxSum(r+1,j+1) }+ D(r,j)
改进:时间复杂度问题
递归牺牲时间,所以改成递推,需要对结果进行存储。
如果每算出一个 M a x S u m ( r , j ) MaxSum(r,j) MaxSum(r,j)就保存起来,下次用到其值的时候直接取用,则可免去重复计算。那么可以用 O ( n 2 ) O(n^2) O(n2)时间完成计算。因为三角形的数字总数是 n ( n + 1 ) / 2 n(n+1)/2 n(n+1)/2
代码:
##import sys
##n = int(input())
##triangle = [[0] for i in range(n)]
##for i in range(n):
## triangle[i] = list(map(int, input().strip().split()))
triangle1 = [[7], [3,8],[8,1,0], [2,7,4,4],[4,5,2,6,5]]
n = len(triangle1)
# 递归程序(会超时)
def MaxSum(i,j,n,triangle):
if i == n-1:
return triangle[i][j]
return max(MaxSum(i+1,j,n,triangle), MaxSum(i+1,j+1,n,triangle)) + triangle[i][j]
import time
time_start=time.time()
print(MaxSum(0,0,n,triangle1))
time_end=time.time()
time1 = time_end-time_start
print('totally cost of first method',time1)
# 改进:存储之前的结果
def MaxSum2(i,j,n,triangle):
# 只要该结果已经存储,直接return
if mat_maxsum[i][j] != -1:
return mat_maxsum[i][j]
if i == n-1:
mat_maxsum[i][j] = triangle[i][j]
else:
mat_maxsum[i][j] = max(MaxSum2(i+1, j, n, triangle), MaxSum2(i+1, j+1, n, triangle))\
+ triangle[i][j]
return mat_maxsum[i][j]
time_start=time.time()
mat_maxsum = [[-1] * n for i in range(n)]
print(MaxSum2(0,0,n,triangle1))
time_end=time.time()
time2 = time_end-time_start
print('totally cost of first method', time2)
print('时间倍数: ', round(time1/time2,2))
30
totally cost of first method 0.013000726699829102
30
totally cost of first method 0.008000373840332031
时间倍数: 1.63
改进2:节约空间
triangle1 = [[7], [3,8],[8,1,0], [2,7,4,4],[4,5,2,6,5]]
n = len(triangle1)
import time
time_start=time.time()
mat_maxsum = [[-1] * n for i in range(n)]
for j in range(n):
mat_maxsum[n-1][j] = triangle1[n-1][j]
for i in range(n-1-1, 0-1, -1):
for j in range(i+1):
mat_maxsum[i][j] = max(mat_maxsum[i+1][j], mat_maxsum[i+1][j+1])\
+ triangle1[i][j]
print(mat_maxsum[0][0])
time_end=time.time()
print('totally cost of first method', time_end-time_start)
改进3:直接把mat_maxsum放到原矩阵当中。
没必要用二维maxSum数组存储每一个MaxSum(r,j),只要从底层一行行向上
递推,那么只要一维数组maxSum[100]即可,即只要存储一行的MaxSum值就
可以。进一步考虑,连maxSum数组都可以不要,直接用D的第n行替代maxSum即可。
节省空间,时间复杂度不变
代码:
triangle1 = [[7], [3,8],[8,1,0], [2,7,4,4],[4,5,2,6,5]]
n = len(triangle1)
# 节约空间1
import time
time_start=time.time()
mat_maxsum = [[-1] * n for i in range(n)]
for j in range(n):
mat_maxsum[n-1][j] = triangle1[n-1][j]
for i in range(n-1-1, 0-1, -1):
for j in range(i+1):
mat_maxsum[i][j] = max(mat_maxsum[i+1][j], mat_maxsum[i+1][j+1])\
+ triangle1[i][j]
print(mat_maxsum[0][0])
time_end=time.time()
print('totally cost of first method', time_end-time_start)
triangle2 = triangle1.copy()
# 节约空间2
time_start=time.time()
mat_maxsum2 = triangle2[n-1]
for i in range(n-1-1, 0-1, -1):
for j in range(i+1):
mat_maxsum2[j] = max(mat_maxsum2[j], mat_maxsum2[j+1])+triangle2[i][j]
print(mat_maxsum[0][0])
time_end=time.time()
print('totally cost of second method', time_end-time_start)
动态规划思路
动态规划解题一般思路
该2级标题所属内容均为引用。
- 将原问题分解为子问题
把原问题分解为若干个子问题,子问题和原问题形式相同或类似,只不过规模变小了。子问题都解决,原问题即解决(数字三角形例)。
子问题的解一旦求出就会被保存,所以每个子问题只需求解一次。 - 确定状态
在用动态规划解题时,我们往往将和子问题相关的各个变量的一组取值,称之为一个“状态”。一个“状态”对应于一个或多个子问题,所谓某个“状态”下的“值”,就是这个“状态”所对应的子问题的解。
所有“状态”的集合,构成问题的“状态空间”。“状态空间”的大小,与用动态规划解决问题的时间复杂度直接相关。在数字三角形的例子里,一共有 N × ( N + 1 ) / 2 N×(N+1)/2 N×(N+1)/2个数字,所以这个问题的状态空间里一共就有 N × ( N + 1 ) / 2 N×(N+1)/2 N×(N+1)/2个状态。
整个问题的时间复杂度是状态数目乘以计算每个状态所需时间。
在数字三角形里每个“状态”只需要经过一次,且在每个状态上作计算所花的时间都是和N无关的常数。 - 确定一些初始状态(边界状态)的值
以“数字三角形”为例,初始状态就是底边数字,值就是底边数字值。 - 确定状态转移方程
定义出什么是“状态”,以及在该 “状态”下的“值”后,就要找出不同的状态之间如何迁移――即如何从一个或多个“值”已知的“状态”,求出另一个“状态”的“值” (“人人为我”递推型)。状态的迁移可以用递推公式表示,此递推公式也可被称作“状态转移方程”。
【对建立“状态”非常重要!】动态规划的特点
- 问题具有最优子结构性质。 如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质。
- 无后效性。当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种手段或经过哪条路径演变到当前的这若干个状态,没有关系。
6.2 例题2:最长上升子序列
输入数据
输入的第一行是序列的长度N (1 <= N <= 1000)。第二行给
出序列中的N个整数,这些整数的取值范围都在0到10000。
输出要求
最长上升子序列的长度。
输入样例
7
1 7 3 5 9 4 8
输出样例
4
解析:
代码:
#import sys
#n = int(input())
#line = list(map(int, input().strip().split()))
n = 7
line = [1, 7, 3, 5, 9, 4, 8]
maxLen = list([1]* n)
def main():
for i in range(1,n):
for j in range(i):
if line[i] > line[j]:
maxLen[i] = max(maxLen[i], maxLen[j]+1 )
print(max(maxLen))
main()
6.3 最长公共子序列
给出两个字符串,求出这样的一个最长的公共子序列的长度:子序列中的每个字符都能在两个原串中找到,而且每个字符的先后顺序和原串中的先后顺序一致。
输入
abcfbc abfcab
programming contest
abcd mnp
输出
4
2
0
分析:
输入两个串
s
1
,
s
2
s1,s2
s1,s2,设
M
a
x
L
e
n
(
i
,
j
)
MaxLen(i,j)
MaxLen(i,j)表示:
s
1
s1
s1的左边
i
i
i个字符形成的子串,与
s
2
s2
s2左边的
j
j
j个字符形成的子串的最长公共子序列的长度(
i
,
j
i,j
i,j从0开始算)
M
a
x
L
e
n
(
i
,
j
)
MaxLen(i,j)
MaxLen(i,j) 就是本题的“状态”
假定 l e n 1 = s t r l e n ( s 1 ) , l e n 2 = s t r l e n ( s 2 ) len1 = strlen(s1),len2 = strlen(s2) len1=strlen(s1),len2=strlen(s2), 那么题目就是要求 M a x L e n ( l e n 1 , l e n 2 ) MaxLen(len1,len2) MaxLen(len1,len2)
显然:
M
a
x
L
e
n
(
n
,
0
)
=
0
(
n
=
0
…
l
e
n
1
)
MaxLen(n,0) = 0 ( n= 0…len1)
MaxLen(n,0)=0(n=0…len1)
M
a
x
L
e
n
(
0
,
n
)
=
0
(
n
=
0
…
l
e
n
2
)
MaxLen(0,n) = 0 ( n=0…len2)
MaxLen(0,n)=0(n=0…len2)
递推公式:
if ( s1[i-1] == s2[j-1] ) //s1的最左边字符是s1[0]
MaxLen(i,j) = MaxLen(i-1,j-1) + 1;
else
MaxLen(i,j) = Max(MaxLen(i,j-1),MaxLen(i-1,j) );
时间复杂度 O ( m n ) O(mn) O(mn), m , n m,n m,n是两个字串长度。
代码:
word1 = 'programming'
word2 = 'contest'
len1 = len(word1)
len2 = len(word2)
# ------------- 用二维数组的情况 --------- #
# maxLen矩阵比字符串多一位,用以储存第一位“0”个字母
maxLen = [[0]*(len2+1) for _ in range(len1+1)]
# 这两个循环:两者任一是0个字母。无交集,结果为0
for i in range(len1+1):
maxLen[i][0] = 0
for j in range(len2+1):
maxLen[0][j] = 0
# i,j代表了第i,j个字母(1~len1 or len2),换到word1&2里需要减1获得位置。
for i in range(1,len1+1):
for j in range(1,len2+1):
if word1[i-1] == word2[j-1]:
maxLen[i][j] = maxLen[i-1][j-1] + 1
else:
maxLen[i][j] = max(maxLen[i][j-1], maxLen[i-1][j])
print(maxLen[len1][len2])
# ------------- 只用一维数组的情况 --------- #
word3 = 'abcfbc'
word4 = 'abfcab' # 答案是4
def maxLength(word1, word2):
n1 = len(word1)
n2 = len(word2)
dp = list(0 for _ in range(n1+1))
start = 1
temp = 1
for i in range(1,n1+1):
for j in range(start,n2+1):
if word1[i-1] == word2[j-1]:
dp[i] = dp[i-1] + 1
temp = j-1
break
else:
dp[i] = max(dp[i],dp[i-1])
start = temp +1
return dp
print(maxLength(word1,word2), maxLength(word1,word2)[len(word1)]) # 2
print(maxLength(word3,word4),maxLength(word3,word4)[len(word3)]) # 4
6.4 最佳加法表达式
有一个由1…9组成的数字串.问如果将m个加号插入到这个数字串中,在各种可能形成的表达式中,值最小的那个表达式的值是多少?
# 遇到大数,可能存在时间复杂问题
def Numof(start, end, number): # 按照数据结构来看,0位开始
alist = number[start:end+1]
result = 0
for i in alist:
result = result *10 + int(i)
return result
def main(num, line):
lenline = len(line)
dp = list([-1] * (lenline+1) for _ in range(num+1))
for i in range(1,lenline+1):
dp[0][i] = Numof(0,i-1,line)
if lenline <= num:
return None
else:
for m in range(1,num+1):
for n in range(m+1,lenline+1):
temp = 10e9
for i in range(m, n):
# 最好是画表格辅助
temp = min(temp,dp[m-1][i] + Numof(i+1-1, n-1,line))
dp[m][n] = temp
return dp[num][lenline]
line = '123456'
print(main(2, line))
print(main(1, line))
num2 = 4
line2 = '12345'
print(main(num2,line2))
7. 动态规划(二)
7.1 Help Jimmy(POJ1661)
场景中包括多个长度和高度各不相同的平台。地面是最低的平台,高度为零,长度无限。Jimmy老鼠在时刻0从高于所有平台的某处开始下落,它的下落速度始终为1米/秒。当Jimmy落到某个平台上时,游戏者选择让它向左还是向右跑,它跑动的速度也是1米/秒。当Jimmy跑到平台的边缘时,开始继续下落。 Jimmy每次下落的高度不能超过MAX米,不然就会摔死,游戏也会结束。设计一个程序,计算Jimmy到地面时可能的最早时间。
输入数据
第一行是测试数据的组数
t
t
t(
0
<
=
t
<
=
20
0 <= t <= 20
0<=t<=20)。每组测试数据的第一行是四个整数
N
,
X
,
Y
,
M
A
X
N, X, Y, MAX
N,X,Y,MAX,用空格分隔。 N是平台的数目(不包括地面),X和Y是Jimmy开始下落的位置的横竖坐标,
M
A
X
MAX
MAX是一次下落的最大高度。
接下来的N行每行描述一个平台,包括三个整数,
X
1
[
i
]
,
X
2
[
i
]
X1[i], X2[i]
X1[i],X2[i]和
H
[
i
]
H[i]
H[i]。
H
[
i
]
H[i]
H[i]表示平台的高度,
X
1
[
i
]
X1[i]
X1[i]和
X
2
[
i
]
X2[i]
X2[i]表示平台左右端点的横坐标。
1
<
=
N
<
=
1000
1 <= N <= 1000
1<=N<=1000
−
20000
<
=
X
-20000 <= X
−20000<=X
X
1
[
i
]
,
X
2
[
i
]
<
=
20000
X1[i], X2[i] <= 20000
X1[i],X2[i]<=20000
0
<
H
[
i
]
<
Y
<
=
20000
(
i
=
1..
N
)
0 < H[i] < Y <= 20000( i = 1..N)
0<H[i]<Y<=20000(i=1..N)
所有坐标的单位都是米。Jimmy的大小和平台的厚度均忽略不计。如果Jimmy恰好落在某个平台的边缘,被视为落在平台上。所有的平台均不重叠或相连。测试数据保Jimmy一定能安全到达地面。
输出要求
对输入的每组测试数据,输出一个整数,Jimmy到地面时可能的最早时间。
输入样例
1
3 8 17 20
0 10 8
0 10 13
4 14 3
输出样例
23
Jimmy跳到一块板上后,可以有两种选择,向左走,或向右走。走到左端和走到右端所需的时间,是很容易算的。如果我们能知道,以左端为起点到达地面的最短时间,和以右端为起点到达地面的最短时间,那么向左走还是向右走,就很容选择了。
因此,整个问题就被分解成两个子问题, 即Jimmy所在位置下方第一块板左端为起点到地面的最短时间,和右端为起点到地面的最短时间。这两个子问题在形式上和原问题是完全一致的。将板子从上到下从1开始进行无重复的编号(越高的板子编号越小,高度相同的几块板子,哪块编号在前无所谓),那么,和上面两个子问题相关的变量就只有板子的编号。
不妨认为Jimmy开始的位置是一个编号为0,长度为0的板子,假设LeftMinTime(k)表示从k号板子左端到地面的最短时间,RightMinTime(k)表示从k号板子右端到地面的最短时间,那么,求板子k左端点到地面的最短时间的方法如下:
- 代码(待更新)
7.3 神奇的背包
有一个神奇的口袋,总的容积是40,用这个口袋可以变出一些物品,这些物品的总体积必须是40。John现在有n( 1≤n ≤ 20)个想要得到的物品,每个物品的体积分别是a1, a2……an。John可以从这些物品中选择一些,如果选出的物体的总体积是40,那么利用这个神奇的口袋, John就可以得到这些物品。现在的问题是, John有多少种不同的选择物品的方式。
输入
输入的第一行是正整数n (1 <= n <= 20),表示不同的物品的
数目。接下来的n行,每行有一个1到40之间的正整数,分别
给出a1, a2……an的值。
输出
输出不同的选择物品的方式的数目。
输入样例
3
20
20
20
输出样例
3
##n = int(input())
##alist = []
##for _ in range(n):
## alist.append(int(input()))
n = 4
alist = [20,10,10,20] # 答案是3:20+10+10,20+20,10+10+20
def main(n, alist):
ways = [[0]*(n+1) for i in range(41)]
ways[0][0] = 1
for i in range(1, n+1):
ways[0][i] = 1
# 从前k种物品中选择一些,凑成体积w的做法数目
for w in range(1,41):
for k in range(1, n+1):
# 如果前k-1个凑足,就不选第k个
ways[w][k] = ways[w][k-1]
# 如果可以选第k个,说明前k-1个是凑不齐的组合
if w - alist[k-1] >= 0:
ways[w][k] += ways[w-alist[k-1]][k-1]
return ways#[40][n]
print(main(n, alist))