主要记录一下官方的题解的理解
这道题官方题解的方法一使用的是记忆化搜索的方法,也称为自顶向下的动态规划,虽然我感觉这个解法主要是回溯算法还使用了状态压缩
首先解释一下什么叫做状态压缩
就是类似于掩码MASK的方式,这道题的数组为
对于这个数组中的每一个数字都有一种状态这个状态位的意思是这个数有没有被选择
我们不妨初始化s为
[1,1,1,1,1,1,1]
也就是一个状态数组
根据排列组合,我们可以知道其实每一个数字都有可能被选择也有可能不被选择所以一共就有2^n个状态,每一个状态都要用一个数组对于存储空间的消耗就会很大,所以我们不妨使用一个二进制数字来表示这个状态数组
即
1111111
我们使用这个二进制数字来表示每一个数字的选取状态,在这里我们设1表示没有被选取过,0表示已经被选取了
你可能要问为什么我们要记录状态,在我的理解里面这是一种优化手段,通过记录某个状态是否遍历过以及计算的结果来防止重复计算,达到减少计算量的目的,说白了可以称为另类的“剪枝”。所以这就是记忆化搜索的记忆的来源,通过一个数组记录状态和是否被遍历过,来达到记忆并且减少计算量的目的。
这也是为什么这个回溯的过程中有dp数组,因为这个问题使用记忆化数组 dp
来保存中间状态的计算结果,避免对相同状态进行重复计算。这种方式提高了递归的效率,也符合动态规划的核心理念——通过分治与重叠子问题的思想来减少重复计算。
所以现在我们知道使用2^n个二进制数字s来表示各个状态,表示各个状态的什么属性呢,就是有没有出现过,有没有被计算过了已经,所以这个dp[s] s指的是各个状态的掩码,dp数组的值表示的是这个状态是否被计算过
有了这些认识,我们来看这道题是怎么解决的吧
首先进行的就是比较明显的情况的处理,首先处理总和不能被k整除的情况,因为要是不能整除就永远分不成k个相等的子集
然后对nums数组排序,看这个数组的最大值,要是这个最大值>sum/k也要返回false,因为这个也没法分
排除掉上面这两种情况后,开始初始化dp数组,一共有2^n个状态,所以我们使用1<<n这个数字初始化了数组的大小,dp数组的含义表示的是s这种情况有没有被处理过,所以使用bool值类型的数据存储最为省空间,我们不妨初始化为true,表示的是这个情况都没有被处理过(这个地方初始化为false行不行,也行但是后面写的要麻烦一些,后面会讲)
OK,下面就开始递归暴搜
官方的题解使用了lamda表达式,目的是能够在一个函数里面解决这个问题,并且使用lamda表达式能够捕捉变量,从而在匿名函数中直接使用局部变量,节省了变量传递的功夫
然后这个匿名函数也就是lamda表达式使用std::function来接受这个东西表示的是通用的可调用对象,相当于存储函数的容器,这样做的目的是让整个代码更加紧凑。
下面开始递归逻辑
递归三部曲
1.确定返回值以及参数
返回值是bool值,也就是总问题我们要解决的是什么——》题目中问的是这个数组能否划分k个子集,所以我们返回这个问题的结果,也就是bool类型
参数:我们要使用什么东西来判断能不能划分?
我们是用状态和现在数组中的和 也就是这个s和p来判断
2.递归的终止条件
当状态变成0000000的时候也就是说每一个数字都被考虑过,表明现在可以终止这个函数了
3.确定递归每一层应该做什么
首先判断这个状态有咩有被处理过,处理过以后我们直接return false来表示这个状态已经处理过了
要是没有被处理过我们也得把这个状态的dp值赋成false,表示这个状态现在被处理过了
处理完状态我们开始看核心的部分
核心的逻辑是
我们遍历nums数组然后拿到其中的元素,判断这个元素有没有被处理过,通过状态表示s就可以判断,如果这个状态已经被处理过了我们就什么也不做,要是没有被处理过,我们递归处理这个子问题---也就是说我选择了这个元素i并且加到了现在的数组的和中,这种情况下能否划分,要是这个子问题返回可以的话 父问题也就能够返回true
要是遍历完nums都不能找到解就返回false
这就是这个单层逻辑
注意这里用了一个异或,作用是去反,因为我们相当于先判断这个i有没有被选择过,然后这个就构成了一个选择,后面处理子问题的时候这个i就是选过了