LIS模型之变形题 Python实现

一、拦截导弹 —— LIS + 贪心(Dilworth定理)

1. 题目描述

拦截导弹

有一个导弹拦截系统,一开始可以调节高度,确定高度之后就只能往下移动(以后每一发都不能高于前一发)。给定敌方飞来的一系列导弹高度,问:

  1. 只有一台拦截系统时,最多能拦截多少导弹?
  2. 最少要多少拦截系统能拦下所有导弹?

2. 题目思路

根据题意,第一问问的是最长的不上升子序列的长度是多少;第二问问的是最少有多少个不上升子序列可以覆盖整个数组

  1. 针对第一问,就是经典的LIS模型,只需用模板题做就可以了

  2. 针对第二问,采用的是贪心法

    从前往后扫描每个数,对于每个数:

    情况1:如果现有的子序列的皆为都小于当前数,则创建新子序列

    情况2:将该数放到大于等于它本身中最小的子序列结尾后面(让每个序列结尾都尽可能大)

  • 贪心的证明:如何证明贪心的结果和正确结果相等?

    • A表示贪心算法的结果,B表示正确结果,需证明A >= B, B >=A

    • B <=A:很容易证明成立,因为B是最优解,找到的子序列个数一定是最小的

    • A <= B:调整法

      • 假设A和B方案不同,找到第一个不同的数x

        • 由于贪心法是将x放到比它大的最小的数后面,假设该数为a,即这个子序列可以表示成:...ax。。。

        • 而最优解是将x放到一个数b后面,即这个子序列为:...bx...

      • 由前面可得b>=a成立,则最优解的这个子序列后半部分x...接到a后面,变成...ax...,即可以将最优解的所有子序列都调整成和贪心解的子序列。因此,A<=B得证。

  • 如何实现该贪心法?

    • 用一个数组g来存储每个子序列的结尾

      • 这样数组的长度就是子序列的个数
      • 每进来一个数,都能在数组里找到要接到哪个序列后面;如果找不到就直接将该数放进数组里后面,相当于一个新的序列 —— 该数组能全部满足贪心法的全部过程
      • 该数组会是一个递增数组(因为一个数进来时只有数组g中没有能够比它大的数的时候,数组g才会把它放进来,所以进来的数都是比数组中前面的数大的)
    • 一开始数组g为空,然后扫描a数组每一个数a[i]:

      • g数组从前往后扫描,如果有遇到大于等于a[i]的第一个数b,就将a[i]替换b(表示a[i]接到以b结尾的序列的后面)
      • 如果扫描完都没有比a[i]大的,就将a[i]放到数组g中,数组g长度+1

      以上过程和LIS贪心解法的过程是一样的

  • 因此,该题的结果就是:LIS的长度

💡 总结

第一问:求最长不上升子序列的长度

第二问:(性质) 一个序列用最少不上升子序列覆盖的个数 = 该序列的LIS长度

3. 代码实现

a = list(map(int, input().split(" ")))
n = len(a)
dp = [1] * n
g = []
for i in range(n):
    # 问题一
    for j in range(i):
        if a[j] >= a[i]: # 不上升子序列的条件
            dp[i] = max(dp[i], dp[j] + 1)
    # 问题二
    flag = 0
    for j in range(len(g)):
        if g[j] >= a[i]:
            g[j] = a[i]
            flag = 1
            break
    if not flag:
        g.append(a[i])
print(max(dp))
print(len(g))    

二、导弹拦截系统 —— LIS + DFS

1. 题目描述

导弹拦截系统

在前面一道题的基础上改了一个条件:导弹拦截系统可以拦截严格单调上升或者严格单调下降的(不再是只拦截不上升子序列)

问给出一系列导弹高度,问最少需要多少套拦截系统可以全部拦截。

输入样例:

5
3 5 2 4 1
0 

输出样例:

2

样例解释:

对于给出样例,最少需要两套防御系统。

一套击落高度为3,4的导弹,另一套击落高度为5,2,1的导弹。

2. 题目思路

  • 回顾上一道题的做法,通过从前往后遍历数,对每个数都是同一个决策——贪心流程,所以对于每个数,放到哪个序列后面是唯一确定的

  • 但是对于这道题,由于有多一种选择,所以在进入贪心流程之前还要决策这个数是放在上升子序列的贪心策略中还是下降子序列的贪心策略中,因此:

    • 对于扫描的每一个数,搜索如果放在上升子序列中会怎么样,放在下降子序列会怎么样,都去看一下——暴搜DFS
  • DFS求最小步数的做法一般有两种:

    • 记一个全局最小值,然后不断更新
    • 迭代加深
  • 整体思路:

    • 设一个存放上升子序列结尾的序列,一个存放下降子序列结尾的序列
    • 设全局最小值ans,初始化为n,表示最坏的结果是每一个数都是单独一个序列
    • dfs函数里面需要三个参数,一个是当前遍历的数,一个是上升子序列的个数,一个是下降子序列的个数
      • 边界:
        • 上升子序列的个数+下降子序列的个数>= ans(说明ans没办法再往下变小了)
        • 遍历到最后一个位置,说明已经有一个方案出来了,更新ans(直接更新:ans = 上升子序列的个数+下降子序列的个数)
      • 对于每一个当前遍历的数,有两个选择(类似八皇后问题):放到上升子序列中/放到下降子序列中
        • 放到上升子序列中:在up序列中找大于等于这个数的最小数并替换它,然后继续搜下一个,注意搜索完成要恢复现场
        • 放到下降子序列中:同上

3. 代码实现

def dfs(u, nu, nd):
    global ans,up,down,a,n
    # 边界1
    if nu + nd >= ans:
        return
    # 边界2
    if u == n: # 不是n-1,因为最后一个数也要处理,应该是到n,即所有数都处理好了
        ans = nu + nd
        return
    
    # 情况1:u放入上升子序列中
    k = 0
    while k < nu and up[k] >= a[u]: # 找到第一个比a[u]小的序列结尾
        k += 1
    temp = up[k] # 备份,为了恢复现场用
    up[k] = a[u] # 替换up[k]
    if k >= nu: # 新增了上升子序列个数
        dfs(u+1, nu + 1, nd) # 找下一个
    else:
        dfs(u+1, nu, nd)
    up[k] = temp # 恢复现场
    
    # 情况2:u放入下降子序列中
    k = 0
    while k < nd and down[k] <= a[u]: # 找到第一个比a[u]大的序列结尾
        k += 1
    temp = down[k] # 备份,为了恢复现场用
    down[k] = a[u] # 替换down[k]
    if k >= nd: # 新增了下降子序列个数
        dfs(u+1, nu, nd+1) # 找下一个
    else:
        dfs(u+1, nu, nd)
    down[k] = temp # 恢复现场
    
while True:
    n = int(input())
    if not n:
        break
    a = list(map(int, input().split()))
    ans = n # 全局最小值
    up = [0] * n
    down = [0] * n
    dfs(0,0,0)
    print(ans)
posted @ 2022-06-12 20:29  要兵长还是里维  阅读(77)  评论(0编辑  收藏  举报