破解大厂算法面试最难动态规划题:将数组分割成元素和相等的两部分

我们继续研究算法面试题型中最复杂的动态规划类型。题目如下:给定一个含有正整数的数组,请给出算法将其分成两个子数组,使得他们的元素和相等。例如给定数组[2,3, 5, 6],它可以分割成两个数组[2,6],[3,5],两个字数组的和都是8.

在面试中遇到问题时,我们首先需要对其中一些模糊之处进行澄清,这样我们才能给面试官思维周全和谨慎的好印象。对于这个问题,我们需要搞清楚的问题又,数组是否为空?数组最多包含多少元素等。清除掉这些模糊点再下手,这样成功率才会更大。

根据前面说过的动态规划解决套路,问题的思考模式为:首先缩小问题规模,看看解决更小规模的问题后,能不能通过其结果来解决更大规模的问题。例如给定的问题中输入数组包含n个元素,那么我们先思考能不能解决n-1个元素情况下的问题,如果能解决,如何利用其结果来解决n个元素的问题。其次一定要利用缓存来存储中间问题的答案,在递归的解决更小规模问题时,先从缓存中查找,如果没有结果在进行下一步的处理。

我们把题目里面的元素增加一些有利于讨论,假设数组为[14, 6, 7, 2, 3, 5, 7],我们将其分成两部分,使得两部分元素之和相等。首先从肉眼上不难看出两部分子数组为:
part1 : 14, 3, 5
part2: 6, 7, 2, 7

现在我们看看如何将问题的规模进行缩小。假设我们去掉数组最后一个元素7,那么两部分数组变成:
part1: 14, 3, 5
part2: 6, 7, 2,

不难发现此时两个数组的元素和的差值为7,这意味着当数组元素为n时,我们要找到两个子数组使得他们元素和的差值为0,如果拿掉最后一个元素,我们用last_element表示,那么问题变成从包含n-1个元素的数组中找到两子数组,使得他们的差值为last_element,如果我们从包含n-1个元素的数值中找到给定分组,那么我们把拿掉的元素放入到元素和较小的那个分组中,这样我们就得到在n个元素下的两个子数组,使得他们元素和相等。

我们再往下看,如果我们再上面问题基础上再进行规模缩小,拿掉最后一个元素5,那么分组变成:
part1: 14, 3
part2: 6,7,2
于是问题变成当数组包含n-2个元素时,我们需要找到两个分组,使得他们元素和的差值为|5 - 7| = 2。我们再看一种情况,那就是元素2排在5的后面,那么在规模为n-2时,拿掉最后一个元素也就是2,于是分组变成:
part: 14,3,5
part2: 6,7
于是问题变成当问题规模为n-2时,我们需要找到两个子数组,使得两个数组元素和的差值为7+2=9。这样我们可以看到一个规律,如果连续两次递归中拿掉的元素属于同一个子数组,那么分组的差值要变成所拿掉元素的和,如果前后两次递归中,拿掉的元素分属与不同子数组,那么差值要变成元素的差。这样一来问题就出现了,我们怎么知道拿掉的元素属于哪个子数组呢。

实际情况是我们不知道,因此两种情况都得考虑,我们分别假设本次递归和上次递归,两次拿走的元素属于同一集合和不同集合,看看这两种情况哪一种能返回有效结果,由此我们给出实现代码如下:

class ArrayPartition:
    def __init__(self):
        self.element_sum = 0
        self.elements = []

    def add(self, elem):
        self.element_sum += elem
        self.elements.append(elem)

    def copy(self):
        array_partition = ArrayPartition()
        array_partition.element_sum = self.element_sum
        array_partition.elements = self.elements.copy()
        return array_partition

    def __str__(self):
        return f"partition:{self.elements} with sum:{self.element_sum}"


class PartitionByTarget:
    def __init__(self, elements, target):
        self.elements = elements
        self.target = target
        self.hash_table = {}

    def __append_element_to_partition(self, array_partition, last_element, target):
        '''
       看看将最后一个元素放到哪一个分组能使得其值等于target
        '''
        if abs(array_partition[0].element_sum + last_element - array_partition[1].element_sum) == abs(target):
            array_partition[0].add(last_element)
        else:
            array_partition[1].add(last_element)

    def __partition_elements(self, index, target):
        if index == 0:
            return None
        if len(self.elements[0: index + 1]) == 2:
            if abs(self.elements[0] - self.elements[1]) == abs(target):
                partition1 = ArrayPartition()
                partition2 = ArrayPartition()
                partition1.add(self.elements[0])
                partition2.add(self.elements[1])
                self.hash_table[(index, target)] = (partition1, partition2)
                return self.hash_table[(index, target)]

        if (index, target) in self.hash_table: # 先查表看看结果是否已经存在
            return  self.hash_table[(index, target)]

        '''
        将问题进行递归处理,要看当前数值[0:index]是否能分成两部分,使得他们和的差值等于target,假设数组能够分成两部分,使得他们的差值为target,
        
        '''
        last_element = self.elements[index]
        next_target1 = last_element + target
        next_target2 = last_element - target
        array_partition1 = self.__partition_elements(index - 1, next_target1)
        array_partition2 = self.__partition_elements(index - 1, next_target2)
        if array_partition1 is None and array_partition2 is None:
            self.hash_table[(index, target)] = None
            return None

        if array_partition1 is not None:
            '''
            这里的copy()很重要,在python中所有对象都以引用的方式存在。由于array_partition1和array_partition2
            有可能是从哈希表中获取,由于__append_element_to_partion函数会修改他们的内容,因此如果我们不传入他们的拷贝,
            那么这里一旦修改其内容,原来哈希表指向的对象也会被修改,这样就会引入错误
            '''
            copy_partition = (array_partition1[0].copy(), array_partition1[1].copy())
            self.__append_element_to_partition(copy_partition, last_element, target)
            self.hash_table[(index, target)] = copy_partition
        else:
            copy_partition = (array_partition2[0].copy(), array_partition2[1].copy())
            self.__append_element_to_partition(copy_partition, last_element, target)
            self.hash_table[(index, target)] = copy_partition

        return self.hash_table[(index, target)]

    def find_partition(self):
        return self.__partition_elements(len(self.elements) -1 , self.target)


partion_target = PartitionByTarget([14, 6, 7, 2, 3, 5, 7, 15, 7, 8, 47, 30, 17, 12, 16, 6, 6, 8, 8], 0)
partions = partion_target.find_partition()
if partions is not None:
    print(f"first part:{partions[0]}")
    print(f"second part:{partions[1]}")

上面代码运行后所得结果如下:

first part:partition:[14, 7, 2, 5, 7, 15, 7, 8, 47] with sum:112
second part:partition:[6, 3, 30, 17, 12, 16, 6, 6, 8, 8] with sum:112

在代码中有值得注意的地方。我把会把ArrayPartition类存储在哈希表中,当递归时会进行查询,一旦条件满足,对应的ArrayPartion类会返回给上层用于组合出新的结果。但由于python所有对象都以引用的方式存在,因此为了防止缓存的对象其内容被更改,我们从哈希表中获得ArrayPartition的实例时,需要返回它的拷贝,这样上层进行操作时才不会影响原有实例内容。

我们注意到,哈希表的键值形式为(index, target),因此哈希表存在的对象数量为index的取值范围乘以target的可能取值范围,由于index最大值为n, target最大值为所有元素之和,我们用s来表示,因此缓存中可能存储的对象数量为n * s, 由此代码的时间和空间复杂度都是O(n * s)。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值