算法竞赛(Python)-“暴力”美学-从穷举算法到贪心算法


前言

  穷举算法就是把所有可能的答案遍历一遍,逐一检查,找到正确的答案。穷举算法看起来简单,它的优点是容易实现,不需要太多的代码就可以解决问题,缺点是时间复杂度。
   穷举算法通过遍历所有的可行解来“试”出最优解,但在很多情况下,可行解的范围非常大,计算所需的时间超出了可接受的范围,这时候可以通过剪枝方法来缩小穷举范围,但如果此时的可行解范围依然非常大怎么办?可以通过贪心算法来解决。
   贪心算法的核心思想就是每一步都选择在当前看来的最优解,也许最终不会得到最优结果,但会接近最优结果。
   穷举算法和贪心算法针对的问题都是可以列出所有可能答案的那类问题。

买卖股票的最佳时机问题

  给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
  注意你不能在买入股票前卖出股票。
  示例 1:
  输入 : [7, 1, 5, 3, 6, 4]
  输出 : 5

  解释 : 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,
  最大利润 = 6 - 1 = 5 。
  注意利润不能是 7 - 1 = 6, 因为卖出价格需要大于买入价格。
  示例 2 :
  输入 : [7, 6, 4, 3, 1]
  输出 : 0

穷举解法

  对所有可能的买入和卖出时机进行遍历,并记录最大差值。

prices = [7, 1, 5, 3, 6, 4]
def find_max(prices):
    if len(prices) <2:
        return "长度不够"
    max_profit = 0
    for i in range(len(prices)):
        for j in range(i+1,len(prices)):
            if (prices[j]-prices[i])>max_profit:
                max_profit=prices[j]-prices[i]
    return max_profit
print(find_max(prices))

贪心解法

  最贪心的投资方案显然是在低价时买人,高价时卖出。是找到一个波峰和一个波谷,且波峰在波谷之后出现,使波峰和波谷在竖功方向上的差别最大,这样一来,在波谷处买人,在波峰处卖出,赚取的差价最大。
  假设我们要在第i天卖出,那么就要在前-1天中的某一天买人,贪心地考虑,当我们在前i-1天中以最低价买人时,赚的钱最多,这正是当前最好的方案。所以可以只遍历卖出的时间,买入的价格就是卖出前的最低价。与“暴力的穷举算法相比,这样的贪心算法把时间复杂度从0(n*2)了降低到了0(n)。

prices = [7, 1, 5, 3, 6, 4]
def find_max(prices):
    if len(prices) <2:
        return "长度不够"
    max_profit = 0
    min_price=prices[0]
    for i in range(len(prices)):
        if prices[i]-min_price>max_profit:
            max_profit=prices[i]-min_price
        if prices[i]<min_price:
            min_price=prices[i]#维护一个最小值
    return max_profit
print(find_max(prices))

物流站的选址(一)

  小余的家乡共有n个地方可以建设物流站。每一个物流站都只能对附近直线距离为ai的区域(包括边界)中的居民点进行配送,另外,小余的家乡有m个居民点需要提供物流配送服务。
  小余作为一个关心家乡的老板,既要满足所有居民的需求(即每个居民点至少有一个可以提供服务的物流站),又要保障公司的利益(即建立较少的物流站),小余必须担任物流站选址规划的重任,要计算最小需要建设多少物流站才能服务到每一个居民点。
  为了简化问题,物流站和居民点都可以看作二维平面上的点,物流站和居民点之间的距离就是两点之间的直线距离。

数据 n=4,表示物流站选址的个数。m=9附近居民点的个数。
接下来表示每个物流站选址的坐标和配送范围
2,1,2;-3,0,3;-1,0,2;2,-1,3。
每个居民点的坐标
1,2;-4,1;-1,1;1,1;-2,0;2,0;3,0;-1,-1;-3,-2。

穷举算法

  一共有4个地址,每个地址要么被选择作为物流站,要么否。一共只有16种方案,可穷举。(用二进制表示方案)

import numpy as np
import math
n=4#物流站地址个数
m=9#居民点个数
X=[2,-3,-1,2] #物流站地址横坐标
Y=[1,0,0,-1]#物流站地址纵坐标
a=[2,3,2,3]#物流站地址配送距离
u=[1,-4,-1,1,-2,2,3,-1,-3]#居民点横坐标
v=[2,1,1,1,0,0,0,-1,-2]#居民点纵坐标

flag=[False for i in range(m)] #标记每个居民点是否被覆盖
#判断物流站i能否服务居民点j
def can_serve(i,j):
    return (X[i]-u[j])*((X[i]-u[j]))+(Y[i]-v[j])*(Y[i]-v[j])<=a[i]*a[i]

max_s=np.power(2,n)#总方案数 ,2的n次方  用二进制表示方案[0,0,0,0]=0 [0,0,0,1]=1,1表示可选

def ten_two(max_s,num):
    """
    将num转换为二进制
    :param max_s: 方案总数
    :param num: 当前方案代码 且num<max_s
    :return:
    """
    result = [0 for i in range(int(math.log(max_s,2)))] #二进制初始化
    num_two = bin(num)  # 将num转换为二进制 如5结果为0b101
    num_two = str(num_two[2:])  # 去除0b 格式为字符
    geshu = int(len(result)) - int(len(num_two))
    result1 = [0 for i in range(geshu)]
    result1.extend([int(i) for i in num_two])
    return result1

def solve():
    ans=n#初始化结果为n
    bests=0 #最优方案
    for s in range(max_s):#遍历每个方案
        station=0 #当前方案的物流站个数
        for j in range(m):#遍历每个居民点
            flag[j]=False
        temp=ten_two(max_s,s) #当前方案二进制
        for i in range(len(temp)):  # 遍历每个方案选址
            if temp[i]==1:
                station += 1
                # 将物流站i服务范围内的每个居民点打上标志
                for j1 in range(m):
                    if can_serve(i,j1):
                        flag[j1]=True

        # 判断当前方案能否覆盖每个居名点,并更新答案
        num = 0
        for j2 in range(m):
            if flag[j2]:
                num += 1
        if num == m:
            if station<ans:
                ans=station
                bests=s
    return ans,ten_two(max_s,bests) # 最优选址个数,最优选址方案

ans,best=solve()
print("最优选址个数:",ans)
print("最优选址方案:",best)

贪心算法

  既然要覆盖每一个居民点,那么每次都要尽可能多地覆盖几个居民点,这正是贪心算法的核心思想。
步骤如下:
  (1)选一个包含最多未覆盖的居民点的物流站站址,在此处新建物流站。
  (2)重复步骤(1),直到所有居民点都被覆盖。
  贪心算法在这个样例中恰好得到了最优解,但正如前面提到的,根本不存在解决这个问题的快速解法,贪心算法计算出的结果不一定是最优解,但这个解不会太差。

n=4#物流站地址个数
m=9#居民点个数
X=[2,-3,-1,2] #物流站地址横坐标
Y=[1,0,0,-1]#物流站地址纵坐标
a=[2,3,2,3]#物流站地址配送距离
u=[1,-4,-1,1,-2,2,3,-1,-3]#居民点横坐标
v=[2,1,1,1,0,0,0,-1,-2]#居民点纵坐标

flag=[False for i in range(m)] #标记每个居民点是否被覆盖
#判断物流站i能否服务居民点j
def can_serve(i,j):
    return (X[i]-u[j])*((X[i]-u[j]))+(Y[i]-v[j])*(Y[i]-v[j])<=a[i]*a[i]


new_num=[0 for i in range(n)]#每个选址覆盖的居民点数初始化为0

best=[0 for i in range(n)] #初始化最优选址
def solve():
    num=0 #当前覆盖的居民点个数
    station=0#需要建造的物流站个数
    while(num<m):#遍历每一个居民
        #选一个包含最多未被覆盖的居名点的物流站,将它作为一个修建点
        new_station=0
        for i in range(n):#遍历每一个选址
            new_num[i]=0#i选址覆盖的居民点数初始化为0
            for j in range(m):#遍历每一个居民点
                if flag[j]==False and can_serve(i,j):
                    new_num[i]+=1
            if new_num[i]>new_num[new_station]:
                new_station=i

            best[new_station]=1 #当前的选址索引
        #在该处新建物流站,更新相关变量
        for j in range(m):##遍历每一个居民点
            if flag[j]==False and can_serve(new_station,j):
                flag[j]==True
                num+=1
        station+=1
    return station,best #个数 、方案
station,best=solve()
print("最优选址个数:",station)
print("最优方案:",best)

物流站的选址(二)

  小算为了扩大物流运输服务的范围,打算开辟一条新的物流运输线路,这条线路可以认为是数轴上长度为L的线段,在坐标0,1,工上有L+1个居民点,需要在其中若干个居民点建设物流站,每一个物流站都只能对线路上直线距离a以内的区域(含边界)提供服务。这条物流运输线路很长,并非每个居民点都可以建设物流站,但为了满足长距离运输的需求,线路上每一个居民点都必须在服务范围内,现在小算需要重新考虑物流站选址的问题,如图所示。

数据格式
n=4,表示物流站选址的个数
L=8表示物流运输线路的长度
接下来的n行,每行有2个整数Xi,ai。表示第i个物流站选址的坐标和服务距离
4,3;
1,1;
6,2;
2,2;

  以样例为例,如果按照前一个问题中的贪心的思路,每次尽可能增加服务范围,结果如图。

  但是,最优解显然不需要三个物流站,两个就够了。

  这个例子再次说明了用贪心算法的思想解决问题时只考虑眼前利益并不一定能得到最优解。现在换一种贪心算法的思路,从最左侧开始,每次在左侧尽可能增加服务范围,这样的思路恰好得到了最优解。同样是贪心算法,为什么这个思路就可以得到最优解呢?下面来分析原因。
  首先要注意的是,物流站的选址规划与顺序无关,所以从左侧开始和从右侧
开始建设物流站都可以。
  考虑最左侧的居民点0,能覆盖到这个点的物流站有两个,分别位于位置1和位置2,这两个物流站至少要建一个,当前看来,位于位置2的物流站更优,因为它可以覆盖更多居民点。对于整个方案来说,如果选择在位置1建设物流站,那么还需要覆盖{3,4,5,6,7,8}居民点,如果选择在位置2建设物流站,那么还需要覆盖{5,6,7,8}居民点,是前者的子集。所以选择在位置2建设物流站对于整个方案来说是最优的选择。
  未被覆盖的居民点5,也用类似的思路分析,在位置4和位置6中选择,左侧增加的服务范围越大,剩余需要覆盖的居民点越少,这种贪心的思路可以保证每一步决策都是全局最优的。

n=4#物流站地址个数
L=8#物流运输线路的长度
X=[4,1,6,2]#Xi表示第i个物流站选址的坐标
a=[3,1,2,2]#ai。表示第i个物流站选址的服务距离
best=[]#存放最优物流站选址的坐标索引
def Solve():
    r=0#当前未覆盖的最左侧的居民点
    num=0#物流站数量
    while(r<=L):
        # 寻找下一个物流站选址
        nex=-1
        for i in range(n):
            if (X[i]-a[i]<=r) and (r<=X[i]+a[i]):
                if nex==-1 or X[nex]+a[nex]<X[i]+a[i]: #居民点i比nex更优
                    nex=i
        best.append(nex)
        #如果没有任何物流站可以覆盖坐标r的居民点,返回--1
        if nex==-1:
            return -1
        #更新r和num
        r=X[nex]+a[nex]+1
        num+=1
    return num,best

print(Solve())

  既然这种贪心算法的思路可以得到全局最优解,那么为什么在前面的二维平面建物流站时,不能用贪心算法得到最优解呢?因为二维比一维要复杂得多,比较两个物流站选址的优劣时,关键是要比较决策后仍需解决的问题,这在二维平面中是很困难的。
  例如在图中,要比较物流站选址1和物流站选址3,在物流站选址1及建立物流站后,还需要再覆盖居民点{(1,2),(1,1),(2,0),(3,0)},在物流站选址3处建立物流站后,还需要再覆盖居民点{(1,2),(-4,1),(-1,1),(-2,0),(-3,-2)},这两个集之间没有包含或被包含的关系,也就没办法直接判断两种决策的优劣。

回合制游戏

  小余回到家里,打开计算机,开始玩一个回合制游戏。在该回合制游戏中,小余扮演勇者,在前往拯救公主的路上,魔王派出了n只怪物阻挡勇者的前进,每个怪物都有一定的血量(游戏人物的生命值)hi和攻击力ai。
  每个回合中,首先所有未被打败的怪物会一哄而上攻击小余扮演的勇者,第i只怪物会造成ai点的伤害,勇者受到的伤害等于每个怪物造成的伤害总和。
当然,勇者也会攻击,勇者一次只能选择其中一只怪物进行攻击,第i个怪物需要攻击hi次才能被打败。
  勇者不能逃避,必须选择攻击,现在用算法采取最优决策,规划攻击每个怪物的顺序,计算出勇者打败所有怪物时受到的最小伤害。

  数据格式
  n=5表示5个怪物。
  a=[1,3,2,6,3],ai表示第i个怪物每回合可造成ai的伤害。
  h=[9,4,2,3,3],hi表示第i个怪物需要hi回合才能被打败。

思路:
如果只有一只怪物,打败它需要h0回合,每回合受到a0的伤害,总共受到的伤害就是a0*h0。
如果有两只怪物,其中一只a0=2,h0=2,另一只a1=3,h1=4。

  • 从怪物攻击力的角度考虑,第2只怪物的攻击力更大,每回合对勇者的伤害更大,所以要优先打败第2只怪物。
  • 从回合数的角度考虑,打败第2只怪物需要的回合数更多,这意味着,如果先打败第2只怪物,则第1只怪物对勇者造成伤害的回合数会更多,所以优先打败第1只怪物。

两只怪物判断函数如下

n=5 #怪物数量
a=[1,3,2,6,3]#ai表示第i个怪物每回合可造成ai的伤害。
h=[9,4,2,3,3]#hi表示第i个怪物需要hi回合才能被打败。

def compare(num1,num2):
    """
    判断怪物num1,num2应该优先打败谁
    :param num1: 怪物1位置索引
    :param num2: 怪物2位置索引
    :return:
    """
    #计算先打败怪物num1时受到的伤害
    damage1=(a[num1]+a[num2])*h[num1]+a[num2]*h[num2]
    # 计算先打败怪物num2时受到的伤害
    damage2 = (a[num1] + a[num2]) * h[num2] + a[num1] * h[num1]
    #比较两个伤害,决定先打败那一只怪物
    if damage1<damage2:
        print("damage1:",damage1)
        print("damage2:",damage2)
        return True
    else:
        print("damage1:", damage1)
        print("damage2:", damage2)
        return  False
print(compare(0,1))

  有两只怪物时应该如何攻击的问题解决了,那么有n只怪物呢?此时需要一个排序算法。本题的解法本质就是一个自定义排序(只是传统的直接比较数字大小变成了用compare(num1,num2)比较)

n=5 #怪物数量
a=[1,3,2,6,3]#ai表示第i个怪物每回合可造成ai的伤害。
h=[9,4,2,3,3]#hi表示第i个怪物需要hi回合才能被打败。

def compare(num1,num2):
    """
    判断怪物num1,num2应该优先打败谁
    :param num1: 怪物1位置索引
    :param num2: 怪物2位置索引
    :return:
    """
    #计算先打败怪物num1时受到的伤害
    damage1=(a[num1]+a[num2])*h[num1]+a[num2]*h[num2]
    # 计算先打败怪物num2时受到的伤害
    damage2 = (a[num1] + a[num2]) * h[num2] + a[num1] * h[num1]
    #比较两个伤害,决定先打败那一只怪物
    if damage1<damage2:
        return True
    else:
        return  False
#print(compare(1,2)) #False

#====排序===
def sortArray(n,a,h):
    """
    快速排序
    :param n:n=5 #怪物数量
    :param a: a=[1,3,2,6,3]#ai表示第i个怪物每回合可造成ai的伤害。
    :param h: h=[9,4,2,3,3]#hi表示第i个怪物需要hi回合才能被打败。
    :return:
    """
    # selection sort
    nums=[i for i in range(n)] #怪物位置索引
    for i in range(n):
        for j in range(i,n):
            if compare(i,j):#如果应该优先打败怪物i
                pass
            else:#如果应该优先打败怪物j。则j和i位置变换
                nums[i],nums[j] = nums[j],nums[i]
                a[i], a[j] = a[j], a[i]#攻击交换
                h[i], h[j] = h[j], h[i]  # 回合交换
    return nums

nums=sortArray(n,a,h)
print("排序结果:",nums)#[3, 4, 2, 1, 0]
#计算勇者受到的总伤害
damage=0
round=0
for i in range(n):
    round+=nums[i]*h[i]#回合数增加
    damage+=round*a[i]

print("总伤害:",damage)

快速包装

  小余在物流站建立一套自动化快递打包系统。只需要把快递摆放在传送带上,传送带就会自动将货物运输到打包机械臂下方,打包机械臂会根据货物调整到合适的大小,只需要1min就可以完成一件快递的打包工作。等机械臂打包完成后,传送带才会慢慢移动,送来下一件货物。
每件快递大小不一,并已知每件快递的高度和长度。打包台会依次对传送带上的每件快递进行打包,特别地,如果后一件快递的高度和长度分别都不大于当前快递的高度和长度,那么机械臂打包完当前的快递后不需要调整即可立即对后一件快递进行打包,否则需要1min来做调整,此外,第1件快递打包时也需要花时间调整。小余想要尽可能提高打包效率,请你计算最少需要多久才能完成打包。

输入格式
n=6 表示快递的数量
L=[8,5,7,4,5,3] 。Li表示第 i件快递的高度。
W=[8,7,6,4,7,7]。Wi表示第i件快递的长度。

  思路:每件快递打包需要的时间都是1min,所以需要缩短打包做调整的时间,可以再进一步,把这些快递分到若干个队列中,再对每个队列中的快递打包时,打包只需在队首做一次调整,需要用最少的队列容纳所有的快递。
  1 在每个队列中,队首的高度和长度最大,后续每个货物都比前一个相等或者小。
  2 用最少的队列。

方案如下
  先把货物排序,然后进行如下
  (1)如果没有任何队列可以容纳某快递,那么就新建一个队列容纳它。
  (2)如果有队列都能容纳该快递,那么一定不要新建队列。
  (3)如果有多个队列可以容纳这件快递,那么把它放在最早出现的队列末尾。


#快递结构体
class kuaidi:
    def __init__(self,Li,Wi):
        self.L=Li #快递高度
        self.W=Wi#快递长度

n=6 #表示快递的数量
L=[8,5,7,4,5,3] #Li表示第 i件快递的高度。
W=[8,7,6,4,7,7]#Wi表示第i件快递的长度
lis=[] #快递结构题数组
for i in range(n):
    lis.append(kuaidi(L[i],W[i]))

def compare_parkage(a,b):
    """
    比较两个快递 。先比较高度再比较长度
    :param a:快递1
    :param b:快递2
    :return:
    """
    if a.L!=b.L:#快递1高度是否大于快递2
        return a.L>b.L
    else: #否则比较长度
        return a.W > b.W

    # ====排序===


def sortArray(n, lis):
    """
    根据问题自定义快速排序
    :param n: 表示快递的数量
    :param lis: #快递结构题数组
    :return:
    """
    for i in range(n):
        for j in range(i, n):
            if compare_parkage(lis[i], lis[j]):  # 如果快递i 快递j 比较
                pass
            else:# 则j和i位置变换
                lis[i], lis[j]=lis[j], lis[i]
    return lis
lis1=sortArray(n,lis) #排序后的快递组
for i in range(n):
    print("第%d个快递,高度为%d,长度为%d"%(i,lis[i].L,lis[i].W))

#==============构建队列=============
queue_num=0 #队列数量初始化为0
tail=[] #初始化每个队列的队尾,最多n个队尾
for i in range(n):
    tail.append(kuaidi(Li=0,Wi=0))

for  i in range(n):#遍历每个快递
    flag=True
    for j in range(queue_num):#遍历每个队列
        #找到最早出现的能容纳这件快递的队列
        if (tail[j].L>=lis1[i].L) & (tail[j].W>=lis1[i].W):
            tail[j]=lis1[i]
            flag=False
            break
    #如果没有找到能容纳这件快递的队列,则新建一个队列
    if flag:
        tail[queue_num]=lis1[i]
        queue_num+=1
print("--------------结果-----------------")
print("队列数量:%d,快递数量:%d"%(queue_num,n))

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

总裁余(余登武)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值