二叉搜索树(Binary Search Tree),又名二叉排序树(Binary Sort Tree)。是具有有以下性质的二叉树:
可以直接移步,侵删
在进行分析前读者需要知道不同遍历结果的特点:
leetcode官方题解
题目描述:
你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N 共有 N 层楼的建筑。
每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。
你知道存在楼层 F ,满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。
每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X 扔下(满足 1 <= X <= N)。
你的目标是确切地知道 F 的值是多少。
无论 F 的初始值如何,你确定 F 的值的最小移动次数是多少?
只有一个鸡蛋的时候:移动N次
有无限个鸡蛋:二分法logN
有两个鸡蛋:第一个鸡蛋等间隔移动,第二个鸡蛋按只有一个鸡蛋移动
有两个鸡蛋:不等间隔,使第一个鸡蛋和第二个鸡蛋的可能移动次数尽量相同。则让第一个鸡蛋的移动间隔变化,每多扔一次间隔减少1.
K个鸡蛋,N层楼:
从第x楼扔鸡蛋:
图中节点的度:图中的度:所谓顶点的度(degree),就是指和该顶点相关联的边数。
握手定理:
题目:给定一个会议时间安排的数组,每个会议时间都会包括开始和结束的时间 [[s1,e1],[s2,e2],…] (si < ei),请你判断一个人是否能够参加这里面的全部会议。
思路:
先按开始时间排好序,然后看后一个的开始的时间是不是在前一个会议结束之前。
class Solution(object):
def canAttendMeetings(self, intervals):
"""
:type intervals: List[List[int]]
:rtype: bool
"""
intvs = sorted(intervals, key = lambda x: x[0])
for idx, intv in enumerate(intvs):
if idx > 0:
if intv[0] < intvs[idx - 1][1]:
return False
return True
题目:
给定一个会议时间安排的数组,每个会议时间都会包括开始和结束的时间 [[s1,e1],[s2,e2],…] (si < ei),为避免会议冲突,同时要考虑充分利用会议室资源,请你计算至少需要多少间会议室,才能满足这些会议安排。
解题思路1:
首先按照开始时间进行排序 ,同时把结束时间加入到列表中,遍历所有的开始时间,然后只在结束时间的列表里找比这个开始时间晚结束的,表明其有重叠,同时记录结束时间列表长度。·
class Solution:
def minMeetingRooms(self, intervals: List[List[int]]) -> int:
occupied, res = [], 0
intervals.sort(key = lambda x:x[0])
for i in intervals:
start, end = i
occupied = [t for t in occupied if t > start]
occupied.append(end)
res = max(res, len(occupied))
return res
解题思路二:
class Solution(object):
def minMeetingRooms(self, intervals):
"""
:type intervals: List[List[int]]
:rtype: int
"""
if not intervals:
return 0
if not intervals[0]:
return 1
intervals = sorted(intervals, key = lambda x: x[1])
record = [0 for _ in range(intervals[-1][1] + 1)]
for interval in intervals:
# print record
begin, end = interval[0], interval[1]
record[begin] += 1
record[end] -= 1
for i, x in enumerate(record):
if i > 0:
record[i] += record[i - 1]
return max(record)
节点类:
class Node:
def __init__(self, data):
self.data = data
self.next = None
self.prev = None
def getData(self):
return self.data
def setData(self, data):
self.data = data
def getNext(self):
return self.next
def getPrev(self):
return self.prev
链表类:
class TwoWayList:
def __init__(self):
self.head = None # 头结点
self.tail = None # 尾结点
self.length = 0 # 链表长度
def isEmpty(self):
# 判断链表是否为空
return self.head == None
def append(self, item):
# 在链表尾部添加节点
if self.length == 0:
node = Node(item)
self.head = node
self.tail = node
self.length = 1
return
node = Node(item)
tail = self.tail
tail.next = node
node.prev = tail
self.tail = node
self.length += 1
def insert(self, index, item):
# 链表中插入节点
length = self.length
if (index<0 and abs(index)>length) or (index>0 and index>=length):
# 判断是否超出下标限制
return False
if index < 0:
# 如果负数索引的话,转成正数索引
index = index + length
if index == 0:
# 最开始插入
node = Node(item)
if self.head != None:
self.head.prev = node
else:
self.tail = node
node.next = self.head
self.head = node
self.length += 1
return True
if index == length - 1:
return self.append(item)
node1 = self.head
for i in range(0, index):
node1 = node1.next
node2 = node1.next
node = Node(item)
node.prex = node1
node.next = node2
node1.next = node
node2.prev = node
self.length += 1
return True
skip list:跳表
Cartesian Tree:笛卡尔树
B-Tree:平衡多路查找树
Splay Tree:伸展树
MergeSort:归并排序
Timsort:是一种混合、稳定高效的排序算法,源自合并排序和插入排序
Bubble Sort:冒泡排序
InsertionSort:插入排序
Selection Sort:选择排序
shell sort:希尔排序
Bucket Sort:桶排序
radix sort:基数排序
介绍详细
二叉树高度最高的情况是每一个层只有一个结点,此时高度为N
最小的情况是完全二叉树,高度是[log2N]+1,以2为底的对数取整后+1
所以高度是[log2N]+1 到 N
若单链表带头结点,那么判定它为空的条件是head->nextNULL;
若单链表不带头结点,那么判定它为空的条件则是headNULL。
直接转,侵删
向量的点乘,也叫向量的内积、数量积,对两个向量执行点乘运算,就是对这两个向量对应位一一相乘之后求和的操作,点乘的结果是一个标量。
点乘的几何意义是可以用来表征或计算两个向量之间的夹角,以及在b向量在a向量方向上的投影
两个向量的叉乘,又叫向量积、外积、叉积,叉乘的运算结果是一个向量而不是一个标量。并且两个向量的叉积与这两个向量组成的坐标平面垂直。
在三维几何中,向量a和向量b的叉乘结果是一个向量,更为熟知的叫法是法向量,该向量垂直于a和b向量构成的平面。
向量积(矢积)与数量积(标积)的区别
名称 | 标积/内积/数量积/点积 | 矢积/外积/向量积/叉积 |
---|---|---|
运算式(a,b和c粗体字,表示向量) | a·b=|a||b|·cosθ | a×b=c,其中 |c|=|a||b|·sinθ,c的方向遵守右手定则 |
几何意义 | 向量a在向量b方向上的投影与向量b的模的乘积 | c是垂直a、b所在平面,且以|b|·sinθ为高、|a|为底的平行四边形的面积 |
运算结果的区别 | 标量(常用于物理)/数量(常用于数学) | 矢量(常用于物理)/向量(常用于数学) |
大佬总结,直接转,有图,侵删
Python实现,侵删
十种常见排序算法可以分为两大类:
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
首先它把较大的数据集合分割成若干个小组(逻辑上分组),然后对每一个小组分别进行插入排序,此时,插入排序所作用的数据量比较小(每一个小组),插入的效率比较高
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
实现:
def quicksort(arr):
if not arr:
return []
key = arr[0]
left = quicksort([i for i in arr[1:] if i < key])
right = quicksort([i for i in arr[1:] if i >= key])
return left + [key] + right
改进:
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。
侵删
题目地址
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
二维动态规划:
- 状态定义:f[i][j]表示只看前i个物品,总体积是j的情况下,总价值最大是多少
- res = max(f[n][0~V])
- 状态转义f[i][j]
- 不选第i个物品 f[i][j] = f[i-1][j]
- 选第i个物品 f[i][j] = f[i-1][j-v[i]]+w[i] 只有当j大于V[i]的时候才能选择
- f[i][j] = max(f[i-1][j],f[i-1][j-v[i]]+w[i])
- 状态初始化:初始化f[0][0] = 0
- 时间复杂度O(n^n),空间复杂度O(n^n)
n, v = map(int, input().split())
goods = []
for i in range(n):
goods.append([int(i) for i in input().split()])
# 初始化,先全部赋值为0,这样至少体积为0或者不选任何物品的时候是满足要求
dp = [[0 for i in range(v+1)] for j in range(n+1)]
for i in range(1, n+1):
for j in range(1,v+1):
dp[i][j] = dp[i-1][j] # 第i个物品不选
if j>=goods[i-1][0]:# 判断背包容量是不是大于第i件物品的体积
# 在选和不选的情况中选出最大值
dp[i][j] = max(dp[i][j], dp[i-1][j-goods[i-1][0]]+goods[i-1][1])
print(dp[-1][-1])
n, v = map(int, input().split())
goods = []
for i in range(n):
goods.append([int(i) for i in input().split()])
dp = [0 for i in range(v+1)]
for i in range(n):
for j in range(v,-1,-1): # 从后往前
if j >= goods[i][0]:
dp[j] = max(dp[j], dp[j-goods[i][0]] + goods[i][1])
print(dp[-1])
- 如果我要求的不是尽可能最大的价值,而是刚好等于背包容量的最大价值,那么该如何去做呢?
f[0] = 0 f[i] = -inf
答:在初始化的时候去处理,只把f[0]初始化成0,其他的f初始化为负无穷,这样就可以确保所有的状态都是从f[0]转移过来,因为从其他地方转移过来的话,值为负无穷。
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
完全背包问题跟01背包问题最大的区别就是每一个物品可以选无数次,因此当我们考虑到第i个物品时,我们应该考虑的情况是:不选这个物品、选一次这个物品、选两次这个物品…,直到不能再选(选的次数k,k*v[i] > j,j为当前背包容量),然后再从这些情况中选最大的
状态定义:f[i]表示总体积是i的情况下,最大价值是多少
result = max(f[0…m])
状态转移:
一维动态规划:
n, v = map(int, input().split())
goods = []
for i in range(n):
goods.append([int(i) for i in input().split()])
dp = [0 for i in range(v+1)]
for i in range(n):
for j in range(v,-1,-1): # 从后往前
k = j//goods[i][0] # 能选多少次
# 从这些次里面取最大
dp[j] = max([dp[j- x* goods[i][0]] + x * goods[i][1] for x in range(k+1)])
# dp[j- x* goods[i][0]] 这个里边是不包含第i个物品的,因为倒序,所有后边可以加上i物品的价值
print(dp[-1])
一维动态规划(优化):
其实可以通过从前往后递推
一方面我们可以根据前一个状态(i-1)推出此时的状态,另一方面由于当前状态前面的值也是当前问题的子问题,因此我们也可以从前面的值推到后面的值。
n, v = map(int, input().split())
goods = []
for i in range(n):
goods.append([int(i) for i in input().split()])
dp = [0 for i in range(v+1)]
for i in range(n):
for j in range(v+1): # 这边表示从good[i][0]到v
if j >= goods[i][0]:
dp[j] = max(dp[j], dp[j-goods[i][0]] + goods[i][1])
# dp[j-goods[i][0]]这个是从前往后算的,所以已经计算了第i个物品,所以直接更新就可以
print(dp[-1])
描述:
有N件物品和一个容量为V的背包。
第i件物品的体积是vi,价值是wi,数量是si。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包流量,且总价值最大。
时间复杂度:NVS
n,v = map(int, input().split())
goods = []
for i in range(n):
goods.append([int(i) for i in input().split()])
dp = [0 for i in range(v+1)]
for i in range(n):
for j in range(v, -1, -1):
# 考虑两种情况的最小值
k = min(j//goods[i][0], goods[i][2])
dp[j] = max([dp[j-x*goods[i][0]] + x*goods[i][1] for x in range(k+1)])
print(dp[-1])
.
一维动态规划(转换01背包)
想法很简单,直接把背包中的物品展开,展成很多数量为1的物品,这样就转换为01背包问题。代码如下:
n,v = map(int, input().split())
goods = []
for i in range(n):
goods.append([int(i) for i in input().split()])
new_goods = []
# 展开
for i in range(n):
for j in range(goods[i][2]):
new_goods.append(goods[i][0:2])
goods = new_goods
n = len(goods)
# 01背包问题
dp = [0 for i in range(v+1)]
for i in range(n):
for j in range(v,-1,-1):
if j>= goods[i][0]:
dp[j] = max(dp[j], dp[j - goods[i][0]] + goods[i][1])
print(dp[-1])
优化方法1(二进制优化):
拆成一个一个的话会大大的增加计算复杂度。
利用二进制的方式进行优化。可以考虑如果给出一个数,最少用几个数可以把0-它之间的数全部表示出来。
eg:7->1 2 4
eg:10->1 2 4 3(10-4-2-1)这样可以保证只到10
每个位置的数是其二进制位的数
这样的话会分成log(s)份
时间复杂度:NVlog(V)
if __name__ == "__main__":
n, v = map(int, input().split())
goods = []
for i in range(n):
vi, wi, si = map(int, input().split())
j = 1
temp = si
while j <= temp:
temp -= j
goods.append([vi*j, wi*j])
j *= 2
if temp > 0:
goods.append([vi*temp, wi*temp])
dp = [0 for i in range(v + 1)]
for i in range(len(goods)):
for j in range(v, -1, -1):
if j >= goods[i][0]:
dp[j] = max(dp[j], dp[j - goods[i][0]] + goods[i][1])
print(dp[-1])
优化方法2:多重背包的单调队列
时间复杂度NV
n, m = map(int, input().split())
dp, q = [0] * 20005, [None] * 20005 # q = [[pos, val], ...]
for _ in range(n):
c, w, s = map(int, input().split())
for j in range(c):
hh = tt = 0
for k in range((m - j)//c + 1):
cur_val = dp[j + k * c] - k * w
while hh < tt and q[tt - 1][1] <= cur_val: tt -= 1
q[tt] = [k, cur_val]; tt += 1
if q[hh][0] < k - s: hh += 1
dp[j + c * k] = q[hh][1] + k * w
print(dp[m])
有 N 种物品和一个容量是 V 的背包。
物品一共有三类:
第一类物品只能用1次(01背包);
第二类物品可以用无限次(完全背包);
第三类物品最多只能用 si 次(多重背包);
每种体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
# 拆分成不同的背包问题,然后多重背包用二进制改进
if __name__ == "__main__":
n, v = map(int, input().split())
dp = [0]*(v+1)
goods = []
for i in range(n):
vi, wi, si = map(int, input().split())
if si < 0:goods.append([-1,vi,wi]) # 01背包
elif si == 0:goods.append([0,vi,wi]) # 完全背包
else:
j = 1
temp = si
while j <= temp:
temp -= j
goods.append([-1, vi * j, wi * j])
j *= 2
if temp > 0:
goods.append([-1, vi * temp, wi * temp])
for good in goods:
if good[0] < 0:
for j in range(v,good[1]-1,-1):
dp[j] = max(dp[j], dp[j-good[1]] + good[2])
else:
for j in range(good[1],v+1):
dp[j] = max(dp[j], dp[j - good[1]] + good[2])
print(dp[-1])
有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。
每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。
状态:dp[i][j]表示体积为i,重量为j时,最大价值是多少
if __name__ == "__main__":
n, v, m = map(int, input().split())
dp = [[0]*(m+1) for _ in range(v+1)]
goods = []
for i in range(n):
vi, mi, wi = map(int, input().split())
for j in range(v, vi - 1, -1):
for k in range(m, mi - 1, -1):
dp[j][k] = max(dp[j][k], dp[j - vi][k-mi] + wi)
print(dp[-1][-1])
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
接下来有 N 组数据:
每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;
if __name__ == "__main__":
n, v = map(int, input().split())
dp = [0]*(v+1)
goods = []
for i in range(n):
s = int(input())
for j in range(s):
vi, wi = map(int, input().split())
if len(goods)>j:
goods[j][0], goods[j][1] = vi, wi
else:
goods.append([vi,wi])
for j in range(v, -1, -1):
for k in range(s):
if j >= goods[k][0]:
dp[j] = max(dp[j], dp[j - goods[k][0]] + goods[k][1])
print(dp[-1])
有 N 个物品和一个容量是 V 的背包。
物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。
如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。
每件物品的编号是 i,体积是 vi,价值是 wi,依赖的父节点编号是 pi。物品的下标范围是 1…N。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品个数和背包容量。
接下来有 N 行数据,每行数据表示一个物品。
第 i 行有三个整数 vi,wi,pi,用空格隔开,分别表示物品的体积、价值和依赖的物品编号。
如果 pi=−1,表示根节点。 数据保证所有物品构成一棵树。
状态定义:dp[i][j] :选节点i的情况下,体积是j时,最大价值是多少
import sys
import math
########################处理输入##############################
x = sys.stdin.readlines()
n, m = map(int, x.pop(0).rstrip().split()) #物品数量、背包体积
y = [ [] for i in range(len(x))]
for i in range(len(x)):
y[i] = [int(j) for j in x[i].rstrip().split()]
f = [[0 for j in range(m+1)] for i in range(n+1)]
#########################代码主体##############################
h = [-1 for _ in range(n+1)]
e = [0 for _ in range(n+1)]
ne = [0 for _ in range(n+1)]
v = [0 for _ in range(n+1)]
w = [0 for _ in range(n+1)]
idx = 1 #边的序号
def addedge(a, b): #建立链式前向星,a起点指向b终点
if a >= 0 and b >= 0: #起点、终点序号都得合法; 防止-1这种列表序号
global idx
e[idx] = b #终点
ne[idx] = h[a] #同起点下一条边,构成链表效果
h[a] = idx #h的长度代表不同起点的数量, "当前边"就是以点a为起点的最后一条边
idx += 1
def dfs(u): #u代表子树的根节点,利用链式前向星进行dfs,起点代表树的父节点而终点代表子节点,叶节点指向-1
global f
i = h[u] #边的序号
while i != -1:
son = e[i] #子节点
dfs(son)
for j in range(m-v[u], -1, -1): #体积从大到小, 必拿本节点
for k in range(1, j+1): #分组背包问题,因为同个子树容量不同时,决策不同,决策之间互斥可以看作不同的物品
f[u][j] = max(f[u][j], f[u][j-k] + f[son][k]) #从子树更新最大价值情况
i = ne[i]
for i in range(m, v[u]-1, -1): #背包容量足够就加上当前节点
f[u][i] = f[u][i-v[u]] + w[u]
for i in range(0, v[u]): #背包容量不够就不加当前节点,但是也不能放任何子节点
f[u][i] = 0
def solution(n, m, items):
# dp[u,i,j] 代表选了以u为根的子树, 只考虑其前i个子树时, 背包容量为j时, 可获得的最大价值 -> f[u][j]
#每算某个节点,先算它的子节点
root = 0
for i in range(1, len(items)+1): # i代表节点序号
v[i], w[i], p = items[i-1]
if p == -1: root = i
else: addedge(p, i) #else可加可不加
dfs(root)
return f[root][m]
######################################################################################
print(solution(n, m, y))
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 最优选法的方案数。注意答案可能很大,请输出答案模 10^9+7 的结果。
解决思路就是要加一个记录方案数的数组
if __name__ == "__main__":
mod = pow(10, 9)+7
n, v = map(int, input().split())
dp = [0] + [float("-inf")] * v # 存储最大价值
nums = [1] + [0] * v # 存储最大方案数
goods = []
for i in range(n):
vi, wi = map(int, input().split())
for j in range(v, vi-1, -1):
t = max(dp[j], dp[j - vi] + wi) # t存储要更新的dp的值
s = 0 # 最大方案数
# 判断dp是从那个方案转化过来的,并进行方案数相加,如果都可以均相加
if t == dp[j]:s += nums[j]
if t == dp[j-vi] + wi:s += nums[j-vi]
if s >= mod: s -= mod # 取模
# 赋值
dp[j] = t
nums[j] = s
max_val = max(dp) # 求最大价值
res = 0
# 找到所有最大价值对应的方案数,进行相加
for i in range(v+1):
if max_val == dp[i]:
res += nums[i]
if res >= mod:res -= mod
print(res)
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N。
if __name__ == "__main__":
n, v = map(int, input().split())
goods = [[0,0]] # 因为编号从1开始
for i in range(n):
goods.append([int(i) for i in input().split()])
dp = [[0 for i in range(v+1)] for j in range(n+2)]
for i in range(n,0,-1):
for j in range(0, v + 1):
dp[i][j] = dp[i + 1][j]
if j >= goods[i][0]:
dp[i][j] = max(dp[i][j], dp[i + 1][j - goods[i][0]] + goods[i][1])
vol = v
for i in range(1,n+1):
if dp[i][vol] == dp[i+1][vol-goods[i][0]] + goods[i][1]:
print(i,end=" ")
vol -= goods[i][0]
对大佬文章的学习,侵删
动态规划问题的一般形式就是求最值。
既然是要求最值,核心问题是什么呢?求解动态规划的核心问题是穷举。
特别的地方:
啥叫「自顶向下」?注意我们刚才画的递归树(或者说图),是从上向下延伸,都是从一个规模较大的原问题比如说f(20),向下逐渐分解规模,直到f(1)和f(2)触底,然后逐层返回答案,这就叫「自顶向下」。
啥叫「自底向上」?反过来,我们直接从最底下,最简单,问题规模最小的f(1)和f(2)开始往上推,直到推到我们想要的答案f(20),这就是动态规划的思路,这也是为什么动态规划一般都脱离了递归,而是由循环迭代完成计算。
具有「最优子结构」是动态规划问题。要符合「最优子结构」,子问题间必须互相独立。
大佬讲解学习,侵删
解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:
# 回溯框架
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」
各种搜索问题其实都是树的遍历问题,而多叉树的遍历框架就是这样:
void traverse(TreeNode root) {
for (TreeNode child : root.childern)
// 前序遍历需要的操作
traverse(child);
// 后序遍历需要的操作
}
前序遍历的代码在进入某一个节点之前的那个时间点执行,后序遍历代码在离开某个节点之后的那个时间点执行。
这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。
有“通用解题方法”的美称
在包含问题所有解的解空间树中,按照深度优先搜索的策略,从根节点出发深度搜索解空间树:
也可以分成以下几个步骤:
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
其中两处…表示的更新窗口数据的地方,到时候你直接往里面填就行了。
而且,这两个…处的操作分别是右移和左移窗口更新操作,等会你会发现它们操作是完全对称的。
侵删
int binarySearch(int[] nums, int target) {
int left = 0, right = ...;
while(...) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}
分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。
搜索一个数,如果存在,返回其索引,否则返回 -1。
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1; // 注意
else if (nums[mid] > target)
right = mid - 1; // 注意
}
return -1;
}
1、为什么 while 循环的条件中是 <=,而不是 <?
2、为什么left = mid + 1,right = mid - 1?我看有的代码是right = mid或者left = mid,没有这些加加减减,到底怎么回事,怎么判断?
刚才明确了「搜索区间」这个概念,而且本算法的搜索区间是两端都闭的,即[left, right]。那么当我们发现索引mid不是要找的target时,下一步应该去搜索哪里呢?
当然是去搜索[left, mid-1]或者[mid+1, right]对不对?因为mid已经搜索过,应该从搜索区间中去除。
3、此算法有什么缺陷?
比如说给你有序数组nums = [1,2,2,2,3],target为 2,此算法返回的索引是 2,没错。但是如果我想得到target的左侧边界,即索引 1,或者我想得到target的右侧边界,即索引 3,这样的话此算法是无法处理的。
int left_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0;
int right = nums.length; // 注意
while (left < right) {
// 注意
int mid = (left + right) / 2;
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid; // 注意
}
}
return left;
}
1、为什么 while 中是<而不是<=?
while(left < right)终止的条件是left == right,此时搜索区间[left, left)为空,所以可以正确终止。
2、为什么没有返回 -1 的操作?如果nums中不存在target这个值,怎么办?
「左侧边界」有什么特殊含义:
nums中小于给定数的元素有几个。
另一种写法:
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
// 搜索区间为 [left, right]
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
// 搜索区间变为 [mid+1, right]
left = mid + 1;
} else if (nums[mid] > target) {
// 搜索区间变为 [left, mid-1]
right = mid - 1;
} else if (nums[mid] == target) {
// 收缩右侧边界
right = mid - 1;
}
}
// 检查出界情况
if (left >= nums.length || nums[left] != target)
return -1;
return left;
}
int right_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0, right = nums.length;
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] == target) {
left = mid + 1; // 注意
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
return left - 1; // 注意
}
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 这里改成收缩左侧边界即可
left = mid + 1;
}
}
// 这里改为检查 right 越界的情况,见下图
if (right < 0 || nums[right] != target)
return -1;
return right;
}
1、为什么最后返回left - 1而不像左侧边界的函数,返回left?而且我觉得这里既然是搜索右侧边界,应该返回right才对。
首先,while 循环的终止条件是left == right,所以left和right是一样的,你非要体现右侧的特点,返回right - 1好了。
至于为什么要减一,这是搜索右侧边界的一个特殊点,关键在这个条件判断:
if (nums[mid] == target) {
left = mid + 1;
// 这样想: mid = left - 1
因为我们对left的更新必须是left = mid + 1,就是说 while 循环结束时,nums[left]一定不等于target了,而nums[left-1]可能是target。
至于为什么left的更新必须是left = mid + 1,同左侧边界搜索,就不再赘述。
第一个,最基本的二分查找算法:
第二个,寻找左侧边界的二分查找:
第三个,寻找右侧边界的二分查找:
二分查找法不仅仅可以用在有序数组元素上的查找,只要其具有一定单调性
排除法的理念:把待搜索的目标值留在最后判断,在循环体内不断地把不符合题目的要求的子区间排除掉,在退出循环之后,因为只剩下一个数没看到,进行单独判断。
一般步骤:
# 1
if check(mid):
right = mid
else:
left = mid + 1
# 2
if check(mid):
right = mid - 1
else:
left = mid
根据边界收缩的行为,修改取中间数的行为
退出循环后,看是否需要多nums[left]是否是目标元素,进行一次检查
注意: