动态规划の公共子序列问题~

Past mistakes can't be fixed,  but the future is full of potential.

悟已往之不谏,知来者之可追

序言

书接上回,讲述公共子序列的问题,这次我们要迎来的是——公共最长子序列http://noi.openjudge.cn/ch0206/1808/icon-default.png?t=N7T8http://noi.openjudge.cn/ch0206/1808/此乃上回c子序列的C(3, 2)种形式!(动态规划,偷偷加了一点其他算法的私货)-CSDN博客

这次就不回顾动态规划的四部曲,想看的可以从上面的内容进行回顾(我个人的方法论)

最长公共子序列(两种方法)

        0.题目理解

                公共子序列的题目其实都是非常好理解的内容啊,直接理解方面确实是很简单,但是想要实现我们的想法还是存在一点难度的,特别是在优化算法上,更是难度飙升,将我们的最长公共子序列转换成最长上升子序列的思想(这一点等一下再讲嘞)

下面是初始化输入的代码

total = []  # 储存所有case的地方
while True:  # 利用终止程序报错打断while循环,使其进入程序
    try:
        list1 = input().split()
        total.append(list1)
    except:
        break

                那么首先,我们先来做做常规的做法

A: 常规dp(复杂度还是O(n ** 2))

        1. 下角标dp[i][j]的含义

                我们首先先来判断用几维的dp数组,不难发现,两个数组的对应元素,其实刚好组成一个链接在一起的表格,一个一个元素根据索引对应的列表嘞,那么此时我们的dp[i][j]其实就代表了list1第i个和list2第j个的对应关系,也就是——在这个位置时的最长子序列长度,所以dp[i][j] 对应了在这个位置下的最大公共子序列长度

                那么这个长度何时能增加呢?那当然是公共的时候啦

        2. dp数组初始化

                因为对于初始情况都是未选择的状态,如果要判断相不相等的话需要循环才能判断(因为初始状态是不知道两个具体内容的相同情况的)所以初始的时候所有内容都是为0,那我们初始化就先设置长度为list1和维度为list2,如果有情况再回来改(事实证明这里是要改的,但是不着急嘞,等会再过来)

                根据4,这里长度为list1 + 1和维度为list2 + 1

        3. 递推公式的推导

                好,啊,这里是我们最重点核心的部分了

                首先我们很明确的可以分成两种情况

                1. 当list1[i] != list2[j]

                此时没办法啊,两个人八字不合凑不到一起,那么这个时候就是老老实实的把之前找过的最好的对象找出来,那么此时最好的对象在哪里呢,那就在之前找的位置啦,横行的dp[i - 1][j] 则代表了在上一横行,也就是list1[i - 1]情况下的对象,dp[i][j - 1]则代表了在相同list2[j - 1]状态下的对象(这里补充理解一下我的看法,此时代表了上一个最优解的继承,而上一个最优解有两种可能

        在同一个位置不同列表, 也就是选择了list1相同位置子序列的情况,对应dp[i - 1][j]

        在不同位置同一个列表, 也就是选择了list2上个位置子序列的情况,对应dp[i][j - 1]

        但是呢,我们其实是不知道这两种情况哪个是最优解,于是乎,max就噔噔瞪得登场啦)

                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

               

                2.当list1[i] == list2[j]

                这个时候包加上去的,为什么?天生一对啊,这个时候我们就要让他们在一起,那我们就需要继承到上一个该加上去的地方

               我们已知用到了i - 1,j - 1的情况,所以这里理应也合起来,得到dp[i - 1][j - 1] + 1

                什么歪门邪道哇,直接开猜了,不靠谱,我们还是来正式分析一下吧

                首先呢,我们已经从第一种情况发现,dp[i][j]的公式,是不包括自己的最优解的继承,并且此时的dp[i - 1][j] 或者是dp[i][j - 1]都代表的是选择过的子序列情况

                因为想要最长的子序列,所以此时我们想要把我们相同的list1[i] 加上去呢,就要找对于dp[i][j] 来说还没加上的子序列,于是我们选择了dp[i - 1][j - 1]代表了不同位置的不同列表,也就是上一个还未经过改变的子序列, 此时把自己的那个1加上,得到递推公式

                dp[i][j] = dp[i - 1][j - 1] + 1

                综上就得到所有递推公式啦

                dp[i][j] = \left\{\begin{matrix} max(dp[i - 1][j], dp[i][j - 1]), &list1[i - 1] != list2[j - 1] \\dp[i - 1][j - 1] + 1, & list1[i - 1] == list2[j - 1] \end{matrix}\right.

                怎么突然冒出一个i - 1,j - 1啊,别急别急,下面就解释        

        4.遍历顺序

                我们从递推公式发现,需要用到i - 1或者j - 1的值,很明显i,j都必须正向遍历,同时我们发现,i,j必须从1开始,否则其对应的dp[i - 1][j - 1]就会到-1去,所以说dp数组的长度都必须在原基础上+1,回到2更改(红笔)遍历的时候也不准忘记从1到length + 1,然后既可以美美写ac代码啦        

for list1, list2 in total:  # 代表了一个case
    length1 = len(list1)
    length2 = len(list2)
    memo = [[0] * (length2 + 1) for _ in range(length1 + 1)]  # dp数组初始化,见2
    for i in range(1, length1 + 1):
        this = list1[i - 1]  # 与递推公式相对应
        for j in range(1, length2 + 1):
            that = list2[j - 1]  # 与递推公式相对应
            if this == that:
                memo[i][j] = memo[i - 1][j - 1] + 1
            else:
                memo[i][j] = max(memo[i - 1][j], memo[i][j - 1])

B.空间复杂度的优化

        这一部分通常是同理的(根据需要优化,如果ac不了发现是mle再用,反而是更好的选择)

        由于特殊也只用到i或者i - 1,所以可以优化,下面给出代码,解释也在图中给出了嘞

                

for list1, list2 in total:  # 代表了一个case
    length1 = len(list1)
    length2 = len(list2)
    memo = [0] * (length2 + 1)  # dp数组初始化,一维dp当然对应的是一维的初始化,根据长度设定为list2 + 1
    for i in range(length1):  # 由于y用不上,此时的i直接对应的是list1的元素,直接改成for i in list1都可以的
        this = list1[i]
        for j in range(1, length2 + 1):
            temp = memo[j]  # 用于记录未变化的memo[j]
            that = list2[j - 1]
            if that == this:
# 如果第一个元素就相同,因为原来的dp[i - 1][j - 1]此时被压缩成last,而last无法存dp[0][j - 1],就会报错,于是用try
                try:
                    memo[j] = last + 1
                except:
                    memo[j] = 1
            else:
                memo[j] = max(memo[j - 1], memo[j])  
# memo[j - 1]就是dp[i][j - 1]的情况,与前面的解释相对应
            last = temp  # 保存,相当于dp[i - 1][j - 1]
    print(memo[-1])

C: 贪心+二分,时间复杂度优化,最大子序列问题!

        不是哥们,莫名奇妙的怎么回到最大子序列问题了呢

       思路转换

                因为我们发现,求最大子序列的过程中,最重要的还可以是我们的索引啊,并且我们发现,list1的字母,会在list2能找到一一的对应,找不到对应那就是寄了,也不用管这个字母了

                那如果能转换的话,是不是意味着对应的位置就找到了,而且如果将这些位置按照一个递增的顺序排序的话,是不是就能满足子序列的性质

                例如:        

                        acicoop

                        dacoppo

                        这里统一以list1元素为参照,list2元素来对应

                        小tip,之后所说明的对应是指list1中的元素,在list2中的所有索引组成的临时数组        

                        a对应的位置是1,c的位置是2,i滚蛋惹,c同位,o的位置是3,6,p的位置是4,5

                                那么我们就可以一一对应起来组成一个新的数组

        细节推敲

                诶,这个数组得怎么排序啊,只知道必然是要按照list1参照的顺序排序,那么参照的对应应该怎么排序,如果是顺序排放的话

                12 2363645,那么此时我们一一对应写下对应的数组,其实从最后一位就能发现端倪,如果顺序,那么p所对应的位置则为4和5,那么根据我们的贪心思想,此时肯定这两个位置都取得最好但是!!!!一个字母只能对应一个元素,所以其实只能选一个加入,那么我们应该怎么办呢,选择我们没有依据,那我们直接对数组排序的时候处理让我们一次只能选一个字母不就好了嘛

                12 2636354,那么此时我们也是按照对应序列的反向写出了一个数组,我们发现,这样子,就一定只能在对应中取一个,并且这个列表本质上就是按照list1的元素顺序排序!!!此时我们只要能求出最大上升子序列,这个子序列的上升属性就代表了list2的元素按顺序排序!!!因为索引代表了相同元素的位置,所以此时找出的这个最大上升子序列一定会同时是list1与list2的子序列,于是题目求解   

for list1, list2 in total:
    length1 = len(list1)
    length2 = len(list2)
    
    # 对应的字典构建,列出对应所有元素的索引位置
    reflect = {}  
    for i in range(length2):
        this = list2[i]
        if this not in reflect:
            reflect[this] = [i]
        else:
            reflect[this].append(i)
    
    # 做出参照转换为对应的数组,在对应寻找自己的位置,如果没有自己的位置就滚蛋
    will_deal = []  # 代表处理完的数组
    for i in list1:
        if i in reflect:
            will_deal.extend(list(reversed(reflect[i])))
    
    # 初始化数组,因为最大子序列一开始至少能包括自己
    try:
        res = [will_deal[0]]
    except IndexError:  # 什么都没有,根本没有重复的,所以直接进入下一个case,continue
        print(0)
        continue
    # 以下内容为上个blog所讲的最大子序列贪心求法,这里不再赘婿    
    def search(n):
        """左闭右开, 优先向小的输出"""
        left = 0
        right = len(res)
        while left < right:
            middle = (left + right) // 2
            reflect = res[middle]
            if reflect > n:
                right = middle
            else:
                left = middle + 1
        left = min(left, len(res) - 1)
        if res[left] > n:
            return left
        else:
            return min(left + 1, len(res) - 1)

    length = len(will_deal)
    for i in range(1, length):
        this = will_deal[i]
        if this > res[-1]:
            res.append(this)
        else:
            loc = search(this)
            res[loc] = this
    print(len(res))

          剩下的还在敲呜呜呜(偷偷摸摸写在最后,因为还有一种情况没讲)

          厦大也要期末考了,不想背政治经济学QAQ

  • 25
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值