算法图解与算法分析
- 1.1 数学基础
- 1.2 模型
- 1.3 要分析的问题
- 1.4 运行时间计算
- 1.4.1 一般法则
- 1.4.2 例子
- 1.4.3 最大子序列和问题的解
- 1.4.4 运行时间中的对数
- 二分搜索
- 欧几里得算法
- 幂运算
- 1.4.5 检验你的分析
- 2.1数据结构
- 2.2查找
- 二分查找
- 2.3排序
- 选择排序
- 快速排序(分而治之、递归)
- 2.4散列表
- 散列函数
- 实现
- 性能
- 2.5广度优先搜索
- 2.6狄克斯特拉算法
- 2.7贪婪算法
- 集合覆盖问题
- 旅行商问题
- NP完全问题
- 2.8动态规划
- 背包问题
- 背包问题FAQ
1.1 数学基础
算法分析需要一套正式的系统架构,我们先从一些数学定义和法则开始:
这些定义的目的是要在函数间建立一种相对的级别
定义1:如果存在正常数c和n0,使得当N≥n0时 T \boldsymbol{T} T(N) ⩽ \leqslant ⩽c f \boldsymbol{f} f(N),则记为 T \boldsymbol{T} T(N)=O( f \boldsymbol{f} f(N))
定义2:如果存在正常数c和n0,使得当N≥n0时 T \boldsymbol{T} T(N) ⩾ \geqslant ⩾c g \boldsymbol{g} g(N),则记为 T \boldsymbol{T} T(N)= Ω \Omega Ω( g \boldsymbol{g} g(N))
定义3: T \boldsymbol{T} T(N)= Θ \Theta Θ( h \boldsymbol{h} h(N)),当且仅当 T \boldsymbol{T} T(N)=O( h \boldsymbol{h} h(N))和 T \boldsymbol{T} T(N)= Ω \Omega Ω( h \boldsymbol{h} h(N))
定义4:如果对所有的常数c存在n0使得当N>n0时 T \boldsymbol{T} T(N)<c p \boldsymbol{p} p(N),则记为 T \boldsymbol{T} T(N)=o( p \boldsymbol{p} p(N))。非正式的定义为:如果 T \boldsymbol{T} T(N)=O( p \boldsymbol{p} p(N))且 T \boldsymbol{T} T(N) ≠ \neq = Θ \Theta Θ( p \boldsymbol{p} p(N)),则 T \boldsymbol{T} T(N)=o( p \boldsymbol{p} p(N))
法则1:如果 T 1 \boldsymbol{T_{1}} T1(N)=O( f \boldsymbol{f} f(N))且 T 2 \boldsymbol{T_{2}} T2(N)=O( g \boldsymbol{g} g(N)),那么
(a) T 1 \boldsymbol{T_{1}} T1(N)+ T 2 \boldsymbol{T_{2}} T2(N)=O( f \boldsymbol{f} f(N)+ g \boldsymbol{g} g(N)) \quad\quad (直观非正式表达为max(O( f \boldsymbol{f} f(N),O( g \boldsymbol{g} g(N)))
(b) T 1 \boldsymbol{T_{1}} T1(N) T 2 \boldsymbol{T_{2}} T2(N)=O( f \boldsymbol{f} f(N) g \boldsymbol{g} g(N))
法则2:如果 T \boldsymbol{T} T(N)是一个k次多项式,则 T \boldsymbol{T} T(N)= Θ \Theta Θ(Nk)
法则3:对任意常数,logkN=O(N)
下表为比较后的函数增长率:
注意:
1、将常数项和低阶项放进大O是不好的习惯,低阶项一般可以被忽略,常数可以丢弃
2、可以通过计算极限
lim
N
→
∞
\lim_{N\to\infty }
limN→∞
f
\boldsymbol{f}
f(N)/
g
\boldsymbol{g}
g(N)来确定两个函数相对增长率,必要的时候可以使用洛必达法则,该极限有四种可能的值:
- 极限为0:意味着 f \boldsymbol{f} f(N)=o( g \boldsymbol{g} g(N))
- 极限为c ≠ \neq = 0,意味着 f \boldsymbol{f} f(N)= Θ \Theta Θ( g \boldsymbol{g} g(N))
- 极限为 ∞ \infty ∞,意味着 g \boldsymbol{g} g(N)=o( f \boldsymbol{f} f(N))
- 极限摆动
1.2 模型
为了便于分析问题,我们假设一个模型计算机。它执行任何一个基础指令都消耗一个时间单元,并且假设它有无限的内存。
1.3 要分析的问题
算法要分析的最重要资源就是运行时间,我们这里考虑的影响运行时间的因素主要是所使用的算法和算法的输入。算法对于输入N所花费的时间一般定义为平均情形和最坏情形的运行时间。平均情形常常反应典型的结果,最坏情形则代表对任何可能的输入在性能上的一种保证。若无特别说明,我们所需要的量是最坏情况下的运行时间(提供界限并且计算相对容易)。
1.4 运行时间计算
1.4.1 一般法则
为了简化分析,约定:不存在特定的时间单位,所要做的就是计算大O运行时间
for循环: 一个for循环运行时间至多是该循环内语句的运行时间乘以迭代次数
嵌套循环:从里向外分析这些循环。
顺序语句:将各个语句的运行时间求和即可,可用法则1的(a)
If/Else语句:不超过判断再加上不同条件下运行时间较长者的总运行时间
1.4.2 例子
// 书上例程
// 计算i^3的累加求和
int sum (int N)
{
int i, PartialSum;
PartialSum = 0; /*1*/
for(i = 1; i <= N; i++) /*2*/
PartialSum += i * i * i;/*3*/
return PartialSum; /*4*/
}
这里针对每行进行分析:
- 花费1个时间单元:1个赋值
- 花费1+N+1+N=2N+2个时间单元:1个赋值、N+1次判断、N次加法
- 花费N(2+1+1)=4N个时间单元:2个乘法、1个加法、1个赋值,执行N次
- 花费1个时间单元:1个返回
合计花费1+2N+2+4N+1=6N+4个时间单元。
但是实际上我们不用每次都这样分析,因为面对成百上千行的程序时,我们不可能每一行都这样分析。只需计算最高阶。能够看出for循环占用时间最多。因此时间复杂度为O(N)
1.4.3 最大子序列和问题的解
最大子序列和问题:给定整数A1,A2,…,AN(可能有负数),求
∑
k
=
1
j
\sum_{k=1}^{j}
∑k=1jAk的最大值(为方便起见,当所有整数均为负数时,则最大子序列和为0)
例如,-2,11,-4,13,-5,-2,答案为20(从A2到A4)
算法1:三个for循环,穷举式地尝试所有可能
O(N3)
int maxsubsum1(const vector<int> &a)
{
int maxsum = 0;
for(int i = 0;i < a.size(); i++) //定义子序列起点
{
for (int j = i; j < a.size(); j++) //定义子序列终点
{
int thissum = 0;
for (int k = i; k <= j; k++) //子序列元素依次相加
thissum += a[k];
if (maxsum < thissum)
maxsum = thissum;
}
}
return maxsum;
}
算法2:与算法1相比,根据
∑
k
=
i
j
\sum_{k=i}^{j}
∑k=ijAk=Aj+
∑
k
=
i
j
−
1
\sum_{k=i}^{j-1}
∑k=ij−1Ak,可以去除了k的循环
O(N2)
int maxsubsum(const vector<int>& a)
{
int maxsum = 0;
for (int i = 0; i < a.size(); i++)
{
int thissum = 0;
for (int j = i; j < a.size(); j++)
{
thissum += a[j]; //可以使用之前得到的结果,避免二次计算
if (maxsum < thissum)
maxsum = thissum;
}
}
return maxsum;
}
算法3:使用分治策略。‘分’为将数据分为左右两部分,即将问题分成两个大致相等的子问题,然后递归的将他们求解;‘治’为将两个子问题的解合并到一起,并可能再做少量的附加工作,最后得到整个问题的解。这个问题中,最大子序列和可能出现三种情况:左半部分,右半部分,跨越左半部分和右半部分。第三种情况的最大子序列和为包含左半部分最后一个元素的最大子序列和加上包含右半部分第一个元素的最大子序列和的总和。
大致计算时间复杂度:定义T(1)为单位时间长度
T(1)=1
T(N)=2
×
\times
×T(N/2)+N
\quad
T(N/2)=2
×
\times
×T(N/4)+N/2
→
\quad\rightarrow\quad
→T(N)=4
×
\times
×T(N/4)+2N
依次类推,T(N)=常数+N
×
\times
×logN,便知T(N)=O(NlogN)
O(NlogN)
int maxsumr(const vector<int>& a, int left, int right) //递归函数
{
if (left == right) //基准情形
if (a[left] < 0)
return 0;
else
return a[left];
int center = (left + right) / 2;
int maxleftsum = maxsumr(a, left, center); //递归左半部分
int maxrightsum = maxsumr(a, center + 1, right); //递归右半部分
//两个for循环分别计算跨越左半部分和右半部分
int maxleftbordersum = 0, leftbordersum = 0;
for (int i = center; i >= left; i--)
{
leftbordersum += a[i];
if (leftbordersum > maxleftbordersum)
maxleftbordersum = leftbordersum;
}
int maxrightbordersum = 0, rightbordersum = 0;
for (int j = center+1; j <= right; j++)
{
rightbordersum += a[j];
if (rightbordersum > maxrightbordersum)
maxrightbordersum = rightbordersum;
}
return max3(maxleftsum, maxleftsum, maxleftbordersum + maxrightbordersum);
}
int maxsubsum(const vector<int>& a)
{
return maxsumr(a, 0, a.size()-1);
}
int max3(int a, int b, int c)
{
return (a > b ? a : b) > c ? (a > b ? a : b) : c;
}
算法4:如果a[i]是负的,那么它不可能代表最有序列的起点;同理,任何负的子序列不可能是最优子序列的前缀。如果某一次循环,检测到 a[i] 到 a[j] 的子序列突然从非负变为负的,则我们不仅能把 i 推进到 i+1,实际上可以推进到 j+1。
附带优点:此算法只对数据进行一次扫描,一旦读入并被处理,它就不需要被记忆。如果数组存储在磁盘上,它就可以被顺序读入,在主存中不必存储数组的任何部分。而且任意时刻,算法能对它已经读入的数据给出子序列问题的正确答案。具有这种特性的算法也叫做联机算法(在线算法)。仅需要常量空间并以线性时间运行的在线算法几乎是完美的算法
O(N)
int maxsubsum(const vector<int> &a)
{
int maxsum = 0, thissum = 0;
for (int j = 0; j <= a.size() - 1; j++)
{
thissum += a[j];
if (thissum < 0) //子序列小于0,便抛弃前面序列,直接置0
thissum = 0;
else if (thissum > maxsum)
maxsum = thissum;
}
return maxsum;
}
1.4.4 运行时间中的对数
对数中最常出现的规律一般可概括为以下一般法则:
1、如果一个算法用常数时间(O(1))将问题的大小消减为其一部分(通常是1/2),那么该算法就是O(logN)。
2、如果使用常数时间只是把问题减少一个常数(如将问题减少1),那么这种算法就是O(N)的。
二分搜索
问题:给定一个整数X和整数A0,A1,…,AN-1,后者已经预先排序并在内存中,求下标 i 使得Ai=X,如果X不在数据中,返回 i=-1.
解法:比较X与居中元素。X小,则将相同策略应用到左边排好序的子序列。X大时同理
O(logN)
int binarysearch(const vector<int>& a, const int& x)
{
int left = 0, right = a.size() - 1;
while (left <= right) //left等于right时,可能刚好在这个点上,也需要进行循环判断下
{
int center = (left + right) / 2;
if (x > a[center])
left = center + 1; //由if的判断条件可知,center可以加 1
else if (x < a[center])
right = center - 1;
else
return center;
}
return -1;
}
欧几里得算法
问题:计算最大公因数
解法:欧几里得算法,通过连续计算余数直到余数为0为止,最后的非零余数就是最大公因数。
定理:如果M>N,则M mod N < N/2。
\quad\quad
可根据定理计算O()
O(logN)
long gcd(long m, long n)
{
while(n != 0)
{
long rem = m % n;
m = n;
n = rem;
}
return m;
}
幂运算
问题:处理整数的幂
解法:可以用递归,求解YN,N为偶数时YN =YN/2
×
\times
× YN/2,N为奇数时YN =YN/2
×
\times
× YN/2
×
\times
× Y
O(logN)
long pow(long x, int n)
{
if(n == 0)
return 1;
if(n == 1)
return x;
if(isEven(n))
return pow(x * x, n / 2);
else
return pow(x * x, n / 2) * x;
}
1.4.5 检验你的分析
- 实际编程,观察运行时间结果与分析预测出的运行时间是否匹配。如果N扩大一倍时,线性程序的运行时间乘以系数2,二次程序的运行时间乘以系数4,O(logN)的运行时间知识多加一个常数。
- 对N的某个范围(通常是2的倍数隔开)计算比值T(N)/f(N),其中T(N)是观察到的运行时间,f(N)则是理论推导出的运行时间。如果所算出的值收敛于一个正常数,则代表f(N)是运行时间的理想近似;如果收敛于0,则代表f(N)估计过大;如果结果发散,则代表f(N)估计过小。
2.1数据结构
数组:读取O(1),插入和删除O(n)
链表:读取O(n),插入和删除O(1)
栈:只有两种操作,压入和弹出。先进后出,不能随机访问队列中的元素
队列:只支持两种操作,入队和出队。先进先出,不能随机访问队列中的元素
散列表
图:模拟一组连接,由节点和边组成,可分为有向图和无向图。可以用散列图表示有向图的各节点的“邻居”。若A指向B,而B不指向A,则B是A的邻居,但A并不是B的邻居
树:
2.2查找
二分查找
二分查找是从中间位置开始入手,将中间位置元素与你要查找的元素比较,每次可以排除一半的元素,复杂度为O(logn)。输入为一个有序的元素列表,如果你要查找的元素包含在列表中,二分查找返回其位置;否则返回None
def binary_search(my_list, item):
#my_list为有序元素列表,item为你要查找的元素
low = 0
high = len(my_list)-1
while low <= high:
mid = (low + high)/2
mid = int(mid)
if item == my_list[mid]:
return mid
elif item > my_list[mid]:
low = mid + 1
else:
high = mid -1
return None```
2.3排序
选择排序
选择排序:循环遍历列表,每次都找出最大或最小的元素,并拿出,放入新的列表中。复杂度为O(n*n)
import numpy as np
def findsmaller(arr):
#找到列表中最小数的索引
arr_len = len(arr)
index = 1
small_index = 0
while index <= arr_len-1:
if arr[index] < arr[small_index]:
small_index = index
index += 1
return small_index
def selectionSort(my_list):
#选择排序
new_arr = []
while len(my_list) >= 1:
small_index = findsmaller(my_list)
new_arr.append(my_list.pop(small_index))
return new_arr
if __name__ == '__main__':
m = np.random.randint(10, size=10)
my_list = m.tolist()
print(selectionSort(my_list))
快速排序(分而治之、递归)
思想:
在一个数组中选择基准值,一次排序后,小于等于基准值的数在基准值的左边,大于等于基准值的数在基准值的右边,然后基准值左边的序列又进行排序,对基准值右边的序列进行排序,这样便是不断递归调用,最终可以得到排好序的序列。
步骤:
排序函数参数为序列数组 、排序的起点 、排序的终点
- 对于一次排序,我们通常选择起点元素作为我们的基准值,设置双指针,分别指向起点和终点。
- 让这两个指针分别向中间位置移动,先动右指针,当遇到小于基准值的数时停止;然后动左指针,当遇到大于基准值的数停止。
- 此时两个指针所对应的元素均已不满足要求了,需要交换。之后交换这两个指针对应的元素,然后继续移动。
- 当左指针等于右指针时,此时将指针所在位置的元素与起点元素交换(也就是基准值)。
关键点:
1、从始至终,由于我们判断条件是带有等于号,因此起点的对应的元素一直等于基准值,没有变过,因此最后当左右指针重合时才可以交换
2、一定要让右指针先走,这样才可以保证最后重合指向的元素是小于基准值的
剽图:
平均情况下算法复杂度为O(nlogn),最糟情况下为O(n*n),最糟情况下会导致调用栈非常长。
保存函数参数时,用调用栈进行保存,当进行递归时,需要不断保存函数参数,从而会导致调用栈非常长。
下面代码将每个列表第一个元素作为基准值。其实应该最好随机选择基准值元素,这样快速排序的运行时间不会太糟糕,一般就是平均运行时间。
public class QuickSort {
public static void main(String[] args) {
int[] arr = new int[]{2,5,1,6,2,9,3};
System.out.println(Arrays.toString(arr));
int n = arr.length;
quickSorta(arr, 0, n-1);
System.out.println(Arrays.toString(arr));
}
public static void quickSorta(int[] arr, int left, int right){
if(left >= right) return;
int i = left;
int j = right;
int base = arr[left];
while(i < j){
while(i < j && base <= arr[j]) j--;
while(i < j && base >= arr[i]) i++;
if(i < j){
int num = arr[j];
arr[j] = arr[i];
arr[i] = num;
}
}
arr[left] = arr[i];
arr[i] = base;
quickSorta(arr, left, i-1);
quickSorta(arr, i+1, right);
}
}
最糟情况:
好的情况:
2.4散列表
散列表内部机制:实现、冲突、散列函数
散列函数
散列函数:无论你给它什么数据,它都还你一个数字。专业讲,将输入映射到数字,且映射满足1、输入相同时,输出相同 2、不同的输入最好映射到不同的数字,均匀。
实现
散列表可以利用数组实现,输出对应数组的索引值。当输入过多,超过数组索引时,便会有多个输入对应同一个数组索引(发生冲突),此时可以用数组+链表实现,数组内存储链表的地址。在python中,散列表就是字典。
性能
1、 散列表的填装因子 = (包含的元素数)/(散列表位置总数),填装因子越低,发生冲突的可能性越小,散列表的性能越高。一个不错的经验规则是:一旦填装因子大于0.7 ,就调整散列表的长度。
2、良好的散列函数让数组中的值呈均匀分布,糟糕的散列函数让值扎堆,导致大量的冲突。
2.5广度优先搜索
广度优先搜索是一种用于非加权图的查找算法。可解决两类问题:1、从节点A出发,有没有到节点B的路径 2、从节点A出发,前往节点B的哪条路径最短(段数最少)。在广度优先搜索的执行过程中,搜索范围从起点开始逐渐向外延伸,先依次检查一度关系的节点,再检查二度关系,依次类推。可以用队列实现广度优先搜索。另外,对于搜索过的节点最好不要再去搜索了,以免无限循环。复杂度为O(节点+边数)。可以利用队列进行实现,先进先出
以下代码是找出,从自己出发,到元素的最后单词为’m‘的元素。寻找最短路径
from collections import deque
def search(graph, name):
s_queue = deque() #空队列
s_queue += graph[name] #为队列添加元素,即'yuan'的邻居
record = [name] #记录出现的元素,防止重复搜索
while s_queue: #判断队列是否为空
person = s_queue.popleft() #出队
if person not in record:
if test(person):
print(person, 'yes')
return True
else:
s_queue += graph[person]
record.append(person)
return False
def test(person):
#我们要找是否有元素最后单词为'm',自己随意设定
if person[-1] == 'm':
return True
else:
return False
if __name__ == '__main__':
#有向图的映射关系,起点设为'yuan'
my_dict = {}
my_dict['yuan'] = ['qi', 'bin']
my_dict['qi'] = ['yuan', 'shang', 'bin']
my_dict['bin'] = ['liu', 'ruo', 'ke']
my_dict['liu'] = ['ruo']
my_dict['shang'] = []
my_dict['ruo'] = []
my_dict['ke'] = []
search(my_dict, 'yuan')
2.6狄克斯特拉算法
之前的广度优先算法找到的是段数最少的路径(非加权图),狄克斯特拉算法可以找到最快到达的路径(加权图)。
要求:加权图、有向无环、权为非负
算法:
1.找出最便宜的节点,即可以在最短时间前往的节点(因此要求权为非负)
2. 对于该节点的邻居,检查是否有前往它们的更快路径。如果有就更新其开销
3. 重复以上过程,保证对图中每个节点都这样做了
4. 计算最终路径(可以用一个散列表记录每个节点的父节点)
原理:通过第一第二步处理的节点,都可以被保证是目前为止的到达此节点的最快路径。
def dikesitela(graph, cost, parents): #对节点进行处理,直到全都处理完
processed = []
node = find_min_node(cost, processed)
while node is not None:
for neighbor_node, neighbor_cost in graph[node].items():
if cost[node] + neighbor_cost < cost[neighbor_node]:
cost[neighbor_node] = cost[node] + neighbor_cost
parents[neighbor_node] = node
processed.append(node)
node = find_min_node(cost, processed)
return cost, parents
def find_min_node(costs, processed): #寻找cost最低,且未被处理过的节点
min_node = None
min_cost = infinity
for node, my_cost in costs.items():
if (my_cost < min_cost) and (node not in processed):
min_cost = my_cost
min_node = node
return min_node
if __name__ == '__main__':
infinity = float('inf')
graph = {} #代表有向图
graph["start"] = {}
graph["start"]["A"] = 6
graph["start"]["B"] = 2
graph["A"] = {}
graph["A"]["fin"] = 1
graph["B"] = {}
graph["B"]["A"] = 3
graph["B"]["fin"] = 5
graph["fin"] = {}
cost = {} #代表到不同的节点的最小cost,会不断更新
cost["A"] = 6
cost["B"] = 2
cost["fin"] = infinity
parents = {} #各个节点最小cost时的父节点,不断更新
parents["A"] = "start"
parents["B"] = "start"
parents["fin"] = None
new_cost, new_parents = dikesitela(graph, cost, parents)
print(new_cost['fin'])
2.7贪婪算法
贪婪算法:一种近似算法,每次都采取最优的做法。专业来讲,每步都选择局部最优解,最终得到的就是全局最优解。并非在任何情况下都行之有效,但是易于实现,并且得到的结果与正确结果相当接近或者相同。
判断近似算法的标准:1、速度 2、近似解与最优解的接近程度
集合覆盖问题
对于上面的问题,是一个NP完全问题。如果列出所有情况来解决的话,共有
2
50
2^{50}
250种广播台子集,时间复杂度O(
2
n
2^n
2n)。时间复杂度太高,此时没有任何算法可以足够快解决这个问题。那么我们就可以使用贪婪算法O(
n
2
n^2
n2)。
步骤:
1、选出一个广播台,它覆盖了最多的未覆盖州
2、重复第一步,直到所有州都被覆盖
代码实现:
states_needed = set(["mt", "wa", "or", "id", "nv", "ut", "ca", "az"]) #set集合内没有重复元素,set可以进行交&、并|、差-
stations = {} #广播台
stations["kone"] = set(["id", "nv", "ut"])
stations["ktwo"] = set(["wa", "id", "mt"])
stations["kthree"] = set(["or", "nv", "ca"])
stations["kfour"] = set(["nv", "ut"])
stations["kfive"] = set(["ca", "az"])
final_stations = set()
while states_needed: #空集时停止循环
best_stations = None
best_state = set()
for x_station, x_state in stations.items():
covered = states_needed & x_state
if len(covered) > len(best_state): #找出覆盖了最多未覆盖州的广播台
best_stations = x_station
best_state = x_state
final_stations.add(best_stations)
states_needed -= best_state
print(final_stations)
旅行商问题
有一位旅行商,需要前往5个城市,出发点和终点都没有限制,但要保证旅程最短。这是一个NP完全问题,复杂度为O(n!),即列出所有可能的路线,共有5!=120种。目前还没有找到更快的准确算法
NP完全问题
NP完全问题:以难解著称,需要计算所有的解,才能找到准确的方案。无法编写出可快速解决这些问题的方法。最佳的做法就是用近似算法
识别NP完全问题:1、随着元素数量增加,速度变得非常慢 2、涉及“所有组合”的问题 3、不能将问题分解,必须考虑所有可能的情况,可能是NP问题 4、涉及序列(如旅行商),或者涉及集合(如集合覆盖),并且难以解决的,可能时NP完全问题 5、旅行商、集合覆盖问题
2.8动态规划
背包问题
假设你是个小偷,背着一个可装4 磅东西的背包。你可盗窃的商品有如下3 件:
1、音响,3000美元,4磅
2、笔记本电脑,2000美元,3磅
3、吉他,1500美元,1磅
你需要装入最大价值的商品
动态规划可以将问题分为小问题,先着手解决小问题。每个动态规划算法都是从一个网格开始,背包问题的网格如下:
网格最初都是空的。需要依次填充每个网格,当网格填满后就找到问题的答案了。每个网格对应着一个小问题。比如cell[i][j]表示有前 i+1 个商品,背包容量为 j+1 的一个小问题。填充顺序为从第一行开始对各列进行填充,然后是第二行,第三行…下面为网格填充方法:
def bag(goods, weight):
num_good = len(goods)
cell = [[0 for j in range(weight)] for i in range(num_good)]
for j in range(weight): #第一行单独处理,可以放下与不可以放下第一件商品,两种情况
if goods[0][1] <= j + 1:
cell[0][j] = goods[0][0]
for i in range(1, num_good):
for j in range(weight):
rest_weight = j + 1 - goods[i][1]
if rest_weight > 0: #比较同样的容量,加上新的商品与不加,那种价值更高
cell[i][j] = max(cell[i-1][j], goods[i][0]+cell[i-1][rest_weight-1])
elif rest_weight == 0:
cell[i][j] = max(cell[i-1][j], goods[i][0])
else:
cell[i][j] = cell[i-1][j]
return cell[num_good-1][weight-1]
if __name__ == '__main__':
goods = [[1500,1],[3000,4],[2000,3]] #商品价值和重量
weight = 4
print(bag(goods,weight))
背包问题FAQ
1、可以加上新的商品,只需要改变网格的大小
2、行的顺序变化,不影响最终结果
3、也可以逐列填充,此问题不影响
4、当添加更小的商品,比如0.5磅的商品,那么需要考虑的粒度更细,需要调整网格。网格列变为0.5、1、1.5、2、2.5、3、3.5、4
5、动态规划无法处理仅偷商品一部分这种情况,一件商品要不全偷,要不不偷
更多推荐
算法图解与算法分析
发布评论