算法学习1

因为周五的时候收到了周日要笔试的消息,So, 上牛客刷了刷题,然后都不会~~(阿哲)~~ 。反正这两天就是疯狂刷题适应OJ, 我估计笔试五道变成应该能写出两道(泪目),所以被迫更新一下算法.(好的结果是AC 2.6/5, 太拉了,,)

背包问题

问题:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。

很明显这玩应绝壁不可能是贪心算法~~(要是贪心算法该有多好,呜呜呜)~~。 那么我作为一个最小白的普通人,肯定最先想到的就是递归遍历,找最大值

import copy
# 用例
bag_size = 5
put_value = [2,4,4,5]
put_size = [1,2,3,4]
put_size_with_loca = []
for i in range(len(put_size)):
    put_size_with_loca.append((put_value[i],put_size[i]))
    
# 对于0,1背包问题的遍历
def dfs(space_, put):
    value_all = []
    for size_xiaohao in put:
        if space_ < size_xiaohao[1]:
            value_all.append(0)
        else:
            put_new = copy.deepcopy(put)
            put_new.remove(size_xiaohao)
            value_all.append(size_xiaohao[0] + dfs(space_ - size_xiaohao[1], put_new))
    return max(value_all)
dfs(bag_size, put_size_with_loca)

这个时间复杂度怎么说呢, 假设背包size = m , 物品大小的均值 = n

时间复杂度就是: O((m / n)!) m/n 的阶乘,时间复杂度高的一批,但是我们使用的是枚举所以对0,1背包还是0,n 背包 亦或者是填充满背包都可以用的

一道例题:416. 分割等和子集

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        # 分割成两个自己换句话说就就是分割出和为1/2 的背包
        sum_ = sum(nums)
        if sum_ % 2 != 0:
            return False
        def dfs(value_sum,list_shengyu):
            for p in list_shengyu:
                if p + value_sum == (sum_ // 2):
                    return True
                elif p + value_sum < sum_ // 2:
                    list_new = copy.deepcopy(list_shengyu)
                    list_new.remove(p)
                    k = dfs(value_sum + p, list_new)
                    if k == True:
                        return True
            return False
        return dfs(0, nums)
# 直接运行会在第33个用例超时
# 继续优化,使用字典记录状态信息
# 拿字典记录状态
class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        # 分割成两个自己换句话说就就是分割出和为1/2 的背包
        sum_ = sum(nums)
        if sum_ % 2 != 0:
            return False
        # 拿字典记录状态, 放置重复的false多次继续遍历。True不需要记录,有True直接就返回了
        dict_ = dict()
        def dfs(value_sum,list_shengyu):
            if dict_.get(value_sum) == 1:
                return False
            for p in list_shengyu:
                if p + value_sum == (sum_ // 2):
                    return True
                elif p + value_sum < sum_ // 2:
                    list_new = copy.deepcopy(list_shengyu)
                    list_new.remove(p)
                    k = dfs(value_sum + p, list_new)
                    if k == True:
                        return True
            dict_[value_sum] = 1
            return False
        return dfs(0, nums)

然后在第37个用例超时,,,所以说这种方法时间复杂度上已经报废了。

OK我们言归正传,那么背包算法是什么样子的呢?https://www.cnblogs.com/jbelial/articles/2116074.html

首先是0,1 背包:

我们对于每种物品,都有选和不选两种选则。当然我们不可能遍历每一种选和不选,这样时间复杂度就是 O(2 ** len(object) ), 这个时间复杂度更可怕,我们选或者不选是根据选择此物品所能带来的贡献 的大小来决定的。换言之, 如何表示贡献,也就是我们列出动态规划解析式的关键.

动态规划么,子问题缩减 + 递归遍历, 我们可以定义出如下的方程 (怎么有种在做奥数的感觉)

f[i][s] 代表前i件物品放入容量为s的背包可以获得的最大价值 ,w[i]表示物品i的占用的容量, v[i] 表示物体i的价值。

状态转移方程就是:f[i][s] = max( f[i-1][s], f[i - 1][s - w[i]] + v[i] )

如果要是叫我们来列状态转移方程的话,我多半会变化s, 但是变化s之后的max又不好表示,换句话说要考虑所有的w[i] ,然后就陷入死循环了,,

说实话,看到这个方程的时候我是蒙蔽的,是真的蒙蔽的状态。而这玩应又不好用语言来表示,读者们加加油吧

bag_size = 20
put_value = [2,4,4,5,13,7,3,8,1,9,5,2,6,4]
put_size = [1,2,3,4,3,8,6,9,10,3,8,7,5,14]
count_ = 0
dict_ = {}
def dfs(i,s):
    global count_
    count_ += 1
    if dict_.get(str((i,s))):
        return dict_.get(str((i,s)))
    if i == 0 or s == 0:
        dict_[str((i,s))] = 0
        return 0
    else:
        k = dfs(i-1,s)
        if s > put_size[i]:
            dict_[str((i,s))] =  max(k,  dfs(i - 1, s - put_size[i]) + put_value[i])
            return dict_[str((i,s))]
        else:
            dict_[str((i,s))] =  k
            count_ += 1
            return dict_[str((i,s))]

对于物品多样甚至无限的情况下,我们新的状态方gai程就是: max( f[i-1][s], f[i - 1][s - w[i] * k] + v[i] * k )) , k的取值也很好找: min( 物品i数量, s // w[i] )这样我们的递归就能实现多重甚至无线的情况

至于恰好装满的情况:

我们在i == 0 and s != 0的情况下返回一个 -FFFFFFFFFFF 这样的界限值放置这种情况拿到最大值就可以了。

优先队列,大根堆,小根堆

堆是一种偏序的完全二叉树, 其中大根堆和小根堆是我们常用的两种数据类型, 分别是代表:父节点的值小于或等于子节点的值、父节点的值大于或等于子节点的值:

作用呢,就是是实现 O(logn) 时间复杂度的插入删除。由于是一颗完全二叉树,我们通常就不直接构造树了,而是直接使用list存储即可。根据完全二叉树的特性 + 数组从0开始, index_parent = (index_son - 1) // 2 (向下取整)

push, 直接在最后一位增加,从下到上调整

pop,将最后一位与第零位互换,然后从上到下调整

class heap_big:
    def __init__(self,list_):
        self.list_ = list_
        self.size = len(self.list_)
        
#   从下往上的swap, 初始化和push的时候用
    def swap_(self, now):
        if now == 0:
            return None
        if self.list_[(now-1)//2] <  self.list_[now]:
            a = self.list_[now]
            self.list_[now] = self.list_[(now-1)//2]
            self.list_[(now-1)//2] = a
            self.swap_((now-1)//2)

# 从上向下的swap, pop的时候用
    def swap_2(self, now):
        if (now*2 + 1) < self.size:
            if self.list_[now] < self.list_[(now*2 + 1)]:
                a = self.list_[now]
                self.list_[now] = self.list_[(now*2 + 1)]
                self.list_[(now*2 + 1)] = a
                self.swap_2(now*2 + 1)
        if (now*2 + 2) < self.size:
            if self.list_[now] < self.list_[(now*2 + 2)]:
                a = self.list_[now]
                self.list_[now] = self.list_[(now*2 + 2)]
                self.list_[(now*2 + 2)] = a
                self.swap_2(now*2 + 2)
                
# 初识堆顺序化,讲道理这个可以直接sort()没问题
    def init_heap(self):
        n = 0
        # 这个初始化,就我查到的算法都至少需要nlog(n)的时间,所以我也就nlog(n)
        for i in range(self.size - 1, 0,-1):
            self.swap_(i)
# 向堆中压入数据
    def push(self,val):
        self.size += 1
        self.list_.append(val)
        self.swap_(self.size - 1)
# 弹出堆首数据
    def pop(self):
        k = self.list_[0]
        self.list_[0] = self.list_[-1]
        self.list_ = self.list_[:-1]
        self.size -= 1
        self.swap_2(0)
        return k
# 这个是大根堆, 把所有的> 和 < 取反就是小根堆。

这种堆就是我们优先队列实现的原理。当然,我们可以将优先级自定义成数组或元组这样实现数据和优先级分离的优先排列方法

例题:973. 最接近原点的 K 个点

你可能感兴趣的:(算法)