目录
背包问题可参考动态规划-经典问题(0-1背包问题)分析及优化,进行理解
416.分割等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
- 每个数组中的元素不会超过 100
- 数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5] 输出: true 解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5] 输出: false 解释: 数组不能分割成两个元素和相等的子集.
分析:在n个物品中选出一定的物品,填满sum/2的背包
状态转移方程:F(n,C):考虑将n个物品填满容量为C的背包
F(i,c) = F(i-1,C)|| F(i-1,C-w(i)) 两种情况,任一满足即满足条件
判断用 i 个元素是否能填满C容量的背包,有两种情况,一种是用i-1个元素就能填满C容量的背包(不使用该物品),另一种是用i-1个元素就能填满C-w(i)容量的背包(使用该物品),时间复杂度O(n*sum/2)
/********* 递归 超出时间限制 *********/
class Solution
{
private $nums; //使nums成为成员变量
/**
* @param Integer[] $nums
* @return Boolean
*/
function canPartition($nums)
{
$sum = 0;
$len = count($nums); //计算数组长度
for ($i = 0; $i < $len; ++$i) { //初始化sum
$sum += $nums[$i];
}
if ($sum % 2 != 0) { //如果sum不能整除2,则不能分成两部分和相同的子集
return false;
}
$this->nums = $nums; //使nums成为成员变量
return $this->tryPartition($len - 1, $sum / 2);
}
/**
* [使用nums[0...index],是否可以完全填充一个容量为sum的背包]
* @param [type] $index [下标]
* @param [type] $sum [剩余的容量sum]
*/
private function tryPartition($index, $sum)
{
if ($sum == 0) { //如果已经填满容量,则满足条件,返回true
return true;
}
if ($sum < 0 || $index < 0) { //如果sum<0则已经装不下,index<0则已经没有可以用的数字
return false;
}
//两种情况,(不考虑当前物品,直接考虑从下一个元素开始能否填满该容量) || (将该元素装进背包,考虑从下一个元素开始能否填满该容量-当前元素占用位置)
return $this->tryPartition($index - 1, $sum) || $this->tryPartition($index - 1, $sum - $this->nums[$index]);
}
}
/********* 记忆化搜索 *********/
class Solution
{
private $nums; //使nums成为成员变量
private $memo; //初始化记忆数组
/**
* @param Integer[] $nums
* @return Boolean
*/
function canPartition($nums)
{
$sum = 0;
$len = count($nums); //计算数组长度
for ($i = 0; $i < $len; ++$i) { //初始化sum
$sum += $nums[$i];
}
if ($sum % 2 != 0) { //如果sum不能整除2,则不能分成两部分和相同的子集
return false;
}
$this->nums = $nums;
return $this->tryPartition($len - 1, $sum / 2);
}
//使用nums[0...index],是否可以完全填充一个容量为sum的背包
//memo[i][c] 表示使用索引为[0...i]的这些元素,是否可以完全填充一个容量为c的背包
//未收录表示为未计算,0表示不可以填充,1表示可以填充
private function tryPartition($index, $sum)
{
if ($sum == 0) { //如果已经填满容量,则满足条件,返回true
return true;
}
if ($sum < 0 || $index < 0) { //如果sum<0则已经装不下,index<0则已经没有可以用的数字
return false;
}
if (isset($this->memo[$index][$sum])) { //如果记忆数组有记录,则证明已经求过,判断是否满足条件,可以填充该数字即可
return $this->memo[$index][$sum] == 1;
}
//计算当前index和sum下的记忆数组的值
//两种情况,(不考虑当前物品,直接考虑从下一个元素开始能否填满该容量) || (将该元素装进背包,考虑从下一个元素开始能否填满该容量-当前元素占用位置)
$this->memo[$index][$sum] = ($this->tryPartition($index - 1, $sum) || $this->tryPartition($index - 1,
$sum - $this->nums[$index])) ? 1 : 0;
return $this->memo[$index][$sum] == 1; //返回是否可以填充
}
}
/********* 动态规划 *********/
class Solution
{
/**
* @param Integer[] $nums
* @return Boolean
*/
function canPartition($nums)
{
$sum = 0;
$len = count($nums); //计算数组长度
for ($i = 0; $i < $len; ++$i) { //初始化sum
$sum += $nums[$i];
}
if ($sum % 2 != 0) { //如果sum不能整除2,则不能分成两部分和相同的子集
return false;
}
$c = $sum / 2; //记录一下容量,方便后面引用
$dp = []; //初始化动态规划数组
for ($i = 0; $i <= $c; ++$i) { //如果只考虑第一个元素,判断是否能填满当前背包
$dp[$i] = ($nums[0] == $i); //否则则均是无法填满的
}
for ($i = 1; $i < $len; ++$i) { //从第二个元素开始,每一次多考虑一个数
for ($j = $c; $j >= $nums[$i]; --$j) { //从容量c开始倒着往前推,
$dp[$j] = $dp[$j] || $dp[$j - $nums[$i]]; //(之前dp是否为true,不使用第i个物品) ||(dp[$j - $nums[$i]]能否被填满,能则true,使用第i个物品)
}
}
return $dp[$c];
}
}
322.零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1
。
示例 1:
输入: coins = [1, 2, 5], amount = 11 输出: 3 解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3 输出: -1
说明:
你可以认为每种硬币的数量是无限的。
分析:该题属于0-1背包问题中的完全背包问题,每个物品可以用无限次
状态转移方程:F(n,C):考虑将n个物品填满容量为C的背包
F(i,c) =min( F(i+1,C), F(i,C-w(i))+1 )两种情况,任一满足即满足条件
判断用 i 个元素是否能填满C容量的背包,有两种情况,一种是不使用该物品:用i+1个后面的硬币就能填满amount容量的背包,另一种是使用该物品:用i后面的个元素就能填满C-w(i)容量的背包,使用硬币数+1,两种情况取用硬币数用的最少的情况
/**************** 普通递归:超出时间限制 ****************/
class Solution {
private $coins,$len; //使硬币和数组长度成为成员变量
/**
* @param Integer[] $coins
* @param Integer $amount
* @return Integer
*/
function coinChange($coins, $amount) {
$this->len = count($coins); //初始化硬币和数组长度
$this->coins = $coins;
$val = $this->getCoin(0,$amount); //进行递归求最终结果值
if($val == PHP_INT_MAX) //若最终结果值为整数最大值,证明没有任何一种组合满足条件
return -1;
return $val;
}
/**
* [使用coins[index...len],是否可以完全填充一个容量为amount的背包]
* @param [type] $index [下标]
* @param [type] $amount [剩余的容量amount]
*/
private function getCoin($index,$amount){
if($index >= $this->len || $amount < 0){ //amount<0则已经装不下,index>=len则已经没有可以用的硬币
return PHP_INT_MAX; //则返回整形最大值,因为下方要去最小值
}
if($amount == 0){ //如果已经填满容量,则满足条件,返回
return 0;
}
//两种情况,(将该硬币装进背包,使用硬币数+1,容量减去该硬币占用的金额)||(不考虑当前硬币,直接考虑从下一个硬币开始能否填满该容量)
return min($this->getCoin($index,$amount-$this->coins[$index])+1,$this->getCoin($index+1,$amount));
}
}
/**************** 记忆化搜索 ****************/
class Solution {
private $coins,$len; //使硬币和数组长度成为成员变量
private $memo = []; //初始化记忆数组
/**
* @param Integer[] $coins
* @param Integer $amount
* @return Integer
*/
function coinChange($coins, $amount) {
$this->len = count($coins); //初始化硬币和数组长度
$this->coins = $coins;
$val = $this->getCoin(0,$amount); //进行递归求最终结果值
if($val == PHP_INT_MAX) //若最终结果值为整数最大值,证明没有任何一种组合满足条件
return -1;
return $val;
}
/**
* [使用coins[index...len],是否可以完全填充一个容量为amount的背包]
* @param [type] $index [下标]
* @param [type] $amount [剩余的容量amount]
*/
private function getCoin($index,$amount){
if($index >= $this->len || $amount < 0){//amount<0则已经装不下,index>=len则已经没有可以用的硬币
return PHP_INT_MAX; //则返回整形最大值,因为下方要去最小值
}
if($amount == 0){ //如果已经填满容量,则满足条件,返回
return 0;
}
if(isset($this->memo[$index][$amount]))//记忆数组存在对应该下标后的硬币能填满amount的最小数量,则直接返回
return $this->memo[$index][$amount];
//计算对应记忆数组
//两种情况,(将该硬币装进背包,使用硬币数+1,容量减去该硬币占用的金额)||(不考虑当前硬币,直接考虑从下一个硬币开始能否填满该容量)
$this->memo[$index][$amount] = min($this->getCoin($index,$amount-$this->coins[$index])+1,$this->getCoin($index+1,$amount));
return $this->memo[$index][$amount];
}
}
/**************** 动态规划 ****************/
class Solution {
/**
* @param Integer[] $coins
* @param Integer $amount
* @return Integer
*/
function coinChange($coins, $amount) {
$len = count($coins); //初始化硬币和数组长度
if($len == 0 || $amount == 0) return 0; //初始化判断
sort($coins); //排序数组
//计算每一个容量下,最小的硬币组合
for($i = 1;$i <= $amount; ++$i){
$dp[$i] = PHP_INT_MAX; //初始化当前dp的值为最大值
for($j = 0;$j < $len && $coins[$j]<=$i;++$j){ //遍历到coin数组完 或者 硬币超出了金额
$dp[$i] = min($dp[$i],$dp[$i-$coins[$j]]+1);//考虑将硬币放入背包和不放入背包的两种情况
}
}
if($dp[$amount] == PHP_INT_MAX) return -1;
return $dp[$amount];
}
}
377.组合总和
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
nums = [1, 2, 3] target = 4 所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) 请注意,顺序不同的序列被视作不同的组合。 因此输出为 7。
分析:该题跟之前的组合总和一样,可以使用递归回溯的算法进行计算,但会超出时间限制。因此该题可以采用动态规划或者记忆化搜索的方法来解决。该题属于0-1背包问题中的完全背包问题,每个物品可以用无限次,且有顺序性。
状态转移方程:F(target):考虑填满容量为target的背包的组合总数
F(target) += F(target - nums[i])
判断填满amount容量的背包组合总和数,可以自底向上,从1 到amount,依次求出填满amount的背包有多少种可能,下一个dp将会继续尝试所有的数,$dp[$i] += $dp[$i - $nums[$j]],考虑使用每一个数的可能性
/**************** 普通递归:超出时间限制 ****************/
class Solution {
private $res = 0; //初始化最终结果
private $nums,$len; //使其变成成员变量
/**
* @param Integer[] $nums
* @param Integer $target
* @return Integer
*/
function combinationSum4($nums, $target) {
sort($nums); //排序数组
$this->nums = $nums; //初始化nums和数组长度
$this->len = count($nums);
if($this->len == 0) return $this->res; //初始化判断
$this->findSum($target); //递归寻找最终结果
return $this->res;
}
/**
* [递归寻找组合个数]
* @param [type] $target [当前剩余的目标值]
*/
private function findSum($target){
if($target == 0){ //当目标值为0时,满足条件,最终结果+1
$this->res++;
return ;
}
if($target < $this->nums[0]){ //当目标值为小于排序后nums的第一个元素时,已经无法再继续排列组合
return;
}
//因为每个组合都可以无限次使用,因此每次都是从0开始遍历,知道长度超过,或者超过目标值为止
for($i = 0;$i<$this->len && $this->nums[$i] <= $target;++$i){
$this->findSum($target-$this->nums[$i]);
}
}
}
/**************** 记忆化搜索:20ms ****************/
class Solution {
private $memo = []; //初始化记忆数组
private $nums,$len; //使其变成成员变量
/**
* @param Integer[] $nums
* @param Integer $target
* @return Integer
*/
function combinationSum4($nums, $target) {
sort($nums); //排序数组
$this->nums = $nums; //初始化nums和数组长度
$this->len = count($nums);
if($this->len == 0) return 0; //初始化判断
return $this->findSum($target); //递归寻找最终结果
}
private function findSum($target){
if($target == 0){ //当目标值为0时,满足条件,返回 1
return 1;
}
if($target < $this->nums[0]){ //当目标值为小于排序后nums的第一个元素时,已经无法再继续排列组合
return 0; //返回0
}
if(isset($this->memo[$target])){ //记忆数组若已记录了当前目标值下的所有排列组合的可能,则直接返回
return $this->memo[$target];
}
$this->memo[$target] = 0; //否则则需继续遍历,记录记忆数组
for($i = 0;$i<$this->len && $this->nums[$i] <= $target;++$i){
$this->memo[$target] += $this->findSum($target-$this->nums[$i]);
}
return $this->memo[$target]; //返回目标值
}
}
/**************** 动态规划:16ms ****************/
class Solution {
/**
* @param Integer[] $nums
* @param Integer $target
* @return Integer
*/
function combinationSum4($nums, $target) {
sort($nums); //排序数组
$dp = [];
$dp[0] = 1; //初始化起始元素,若元素一开始就=目标值,则组合结果就是1
for($i = 1;$i <= $target;++$i){ //动态规划,记录每一个目标值的所有组合可能性总和
$dp[$i] = 0; //初始化当前dp的值为0
foreach ($nums as $num) { //从头开始遍历排序好的nums,直到遍历完毕或者数字大于目标值为止
if($i >= $num){
$dp[$i] += $dp[$i - $num];//考虑每一个数字的可能性
}
}
}
return $dp[$target];
}
}
474.一和零
在计算机界中,我们总是追求用有限的资源获取最大的收益。
现在,假设你分别支配着 m 个 0
和 n 个 1
。另外,还有一个仅包含 0
和 1
字符串的数组。
你的任务是使用给定的 m 个 0
和 n 个 1
,找到能拼出存在于数组中的字符串的最大数量。每个 0
和 1
至多被使用一次。
注意:
- 给定
0
和1
的数量都不会超过100
。 - 给定字符串数组的长度不会超过
600
。
示例 1:
输入: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3 输出: 4 解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 "10","0001","1","0" 。
示例 2:
输入: Array = {"10", "0", "1"}, m = 1, n = 1 输出: 2 解释: 你可以拼出 "10",但之后就没有剩余数字了。更好的选择是拼出 "0" 和 "1" 。
分析:该题属于0-1背包问题中的多维费用背包问题,需要考虑两个维度,1 和 0
状态转移方程:F(zero,one):考虑0和1能拼出字符串数组中最多的字符
dp[m][n] = MAX( dp[m][n], dp[m-count0][n-count1] + 1 )
两种情况取大值,不使用该字符串,使用该字符串(拼接成的字符串数量+1 再加上 左边 对应的dp[m-count0][n-count1] 的能拼接成的最大字符串数量)
遍历字符串数组,每多一个字符串,计算加进来这个字符串之后,所能拼接成的最大字符串数量。
因为更新时要取的是左边元素的数,因此可以从后往前进行遍历,使得要用到的上一次的值不会受到影响
class Solution {
/**多维费用背包问题
* @param String[] $strs
* @param Integer $m
* @param Integer $n
* @return Integer
*/
function findMaxForm($strs, $m, $n) {
$len = count($strs);
if($len == 0) return 0; //初始化判断数组是否为空
$dp = [];
for($i = $m;$i >= 0;--$i){ //初始化dp,每个数都是 0
for($j = $n;$j >= 0;--$j){
$dp[$i][$j] = 0;
}
}
foreach ($strs as $str) { //遍历每一个字符串
$lens = strlen($str); //计算每一个字符串中的0和1各有多少
$zero = $one = 0;
for($i = 0;$i<$lens;++$i){
if($str[$i] == '1') $one++;
else $zero++;
}
if($zero > $m || $one > $n) continue;//当超出了背包的容纳极限,则直接跳过
for($i = $m;$i >= $zero;--$i){ //从后往前遍历,直到超过背包0,1的容量
for($j = $n;$j >= $one;--$j){
//两种情况取大值,不使用该字符串,使用该字符串(拼接成的字符串数量+1 再加上 左边 对应的dp[m-count0][n-count1] 的能拼接成的最大字符串数量)
$dp[$i][$j] = max($dp[$i][$j],$dp[$i-$zero][$j-$one]+1);
}
}
}
return $dp[$m][$n];
}
}
139.单词拆分
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
- 拆分时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"] 输出: true 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"] 输出: true 解释: 返回 true 因为 "applepenapple"可以被拆分成 "apple pen apple"。 注意你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] 输出: false
分析:该题属于0-1背包问题中的完全背包问题,背包为 s ,物品为wordDict
(1)递归:判断是否有物品可以放入定义的背包中,一直递归判断,直到当前下标达到字符串长度,定义preIndex为上一次插入空格的位置,index为当前位置。截取字符串substr($s, $preIndex, $index-$preIndex+1)
- 如果截取的字符串在数组中存在,即数组中的物品能填满截取的字符串背包,则考虑两种情况,数组中元素是否能放入 (index+1往后的字符串背包中 || preIndex不变截取字符串增加1位的字符串背包)
- 不存在则只判断preIndex不变截取字符串增加1位的字符串背包
(2)记忆化搜索:如下图,递归中存在大量定义的重叠子问题
定义记忆化数组,memo[preIndex]记录数组物品 能否放入 定义的 preIndex 到 len-1 (即字符串末尾)的字符串背包中
(3)动态规划
状态的转移方程:F(wordDict,s):考虑物品数组中是否有元素可以填满 s 背包中
F(wordDict,s) = F(wordDict,s[0..i]).. && ... F(wordDict,s[i..len-1])
用dp[i]表示0到i的子字符串是否可以拆分成满足条件的单词,在计算dp[i]的时候,我们已经知道dp[0],dp[1],…,dp[i-1],
如果dp[j]已经定义了,且为true(意为0..j的子串存在数组中) && s[j..i]的字符串也存在于数组中
则dp[i]就是true
/**************** 递归:超出时间限制 ****************/
class Solution {
private $s,$wordDict,$len;
/**
* @param String $s
* @param String[] $wordDict
* @return Boolean
*/
function wordBreak($s, $wordDict) {
$this->s = $s; //初始化成员变量
$this->len = strlen($s);
$this->wordDict = $wordDict;
return $this->breakWord(0, 0); //递归判断是否能找到该元素
}
/**
* [分割的字符串是否存在wordDict背包中]
* @param [type] $index [当前遍历到的下标]
* @param [type] $preIndex [前一个下标]
*/
private function breakWord($index, $preIndex){
//截取字符串
$tmp = substr($this->s, $preIndex, $index-$preIndex+1);
if($index == $this->len){ //递归终止,当前下标到达数组长度,已经不能继续截取
return in_array($tmp, $this->wordDict); //判断当前分割的字符串是否在数组中
}
if(in_array($tmp, $this->wordDict)){ //如果物品数组有可以放入字符串背包中的
//则继续判断两种情况:定义从新的下标往后的字符串背包 || 定义前一个下标位置不变时截取字符串增加1位的字符串背包
return $this->breakWord($index+1, $index+1) || $this->breakWord($index+1, $preIndex);
}
//否则则只判断数组元素能否放入定义的前一个下标位置不变时截取字符串增加1位的字符串背包
return $this->breakWord($index+1, $preIndex);
}
}
/**************** 记忆化搜索 ****************/
class Solution {
private $s,$wordDict,$len;
private $memo = []; //初始化记忆数组
/**
* @param String $s
* @param String[] $wordDict
* @return Boolean
*/
function wordBreak($s, $wordDict) {
$this->s = $s;
$this->len = strlen($s);
$this->wordDict = $wordDict;
return $this->breakWord(0, 0); //递归判断是否能找到该元素
}
/**
* [分割的字符串是否存在wordDict背包中]
* @param [type] $index [当前遍历到的下标]
* @param [type] $preIndex [前一个下标]
*/
private function breakWord($index, $preIndex){
//当前一个下标对应的记忆数组已经被定义,则直接返回结果
if(isset($this->memo[$preIndex])) return $this->memo[$preIndex];
$tmp = substr($this->s, $preIndex, $index-$preIndex+1);
if($index == $this->len){ //递归终止,当前下标到达数组长度,已经不能继续截取
return in_array($tmp, $this->wordDict); //判断当前分割的字符串是否在数组中
}
if(in_array($tmp, $this->wordDict)){ //如果物品数组有可以放入字符串背包中的
//则继续判断两种情况
$this->memo[$index+1] = $this->breakWord($index+1, $index+1); //定义从新的下标往后的字符串背包
$this->memo[$preIndex] = $this->breakWord($index+1, $preIndex);//定义前一个下标位置不变时截取字符串增加1位的字符串背包
return $this->memo[$index+1] || $this->memo[$preIndex];
}
//否则则只判断数组元素能否放入定义的前一个下标位置不变时截取字符串增加1位的字符串背包
$this->memo[$preIndex] = $this->breakWord($index+1, $preIndex);
return $this->memo[$preIndex];
}
}
/**************** 递归 ****************/
class Solution {
/**
* @param String $s
* @param String[] $wordDict
* @return Boolean
*/
function wordBreak($s, $wordDict) {
$len = strlen($s);
if($len == 0 || $s == '') return false; //初始化判断,数组为空 或 字符串为空 都无法检索
$dp = []; //初始化动态规划数组
for($i = 0;$i < $len; ++$i){ //从第一个子串动态规划到整个字符串
$str = substr($s, 0, $i+1); //判断直接截取是否能在背包中找到
if(in_array($str, $wordDict)){
$dp[$i] = true; //可以则直接定义当前dp为true,继续下一层遍历
continue;
}
for($j = 0;$j < $i; ++$j){ //j相当于递归中的preIndex前一下标
$str = substr($str, 1); //截取 j(不包含 j )到 i的字符串
//判断 0到 j 的字符串是否可以在背包中找到 并且 j(不包含 j )到 i的字符串也可以在背包中找到
if(isset($dp[$j]) && in_array($str, $wordDict)){
$dp[$i] = true; //同时满足两个条件,即 0 - i的字符串是可以在背包中找到的
break; //跳出循环
}
}
}
return $dp[$len-1]; //最后一个值即为结果
}
}
494.目标和
给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 +
和 -
。对于数组中的任意一个整数,你都可以从 +
或 -
中选择一个符号添加在前面。返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例 1:
输入: nums: [1, 1, 1, 1, 1], S: 3 输出: 5 解释: -1+1+1+1+1 = 3 +1-1+1+1+1 = 3 +1+1-1+1+1 = 3 +1+1+1-1+1 = 3 +1+1+1+1-1 = 3 一共有5种方法让最终目标和为3。注意:
- 数组的长度不会超过20,并且数组中的值全为正数。
- 初始的数组的和不会超过1000。
- 保证返回的最终结果为32位整数。
分析:该题目答案参考评论区的大哥们,不算是一个明显的背包问题
原问题等同于: 找到nums的一个正子集和一个负子集,使得总和等于 S
假设P是正子集,N是负子集 例如: 假设nums = [1, 2, 3, 4, 5],target = 3,一个可能的解决方案是+1-2+3-4+5 = 3 这里正子集P = [1, 3, 5]和负子集N = [2, 4]
则可以得出下面公式
- sum(P) - sum(N) = S
- 则 sum(P) + sum(N) + sum(P) - sum(N) = sum(nums) + S
- 则可得 sum(P) = ( sum(nums) + S ) / 2
问题
因此,原来的问题已转化为一个求子集的和问题: 找到nums的一个子集 P,使得sum(P) = target = ( sum(nums) + S ) / 2
注意: 公式证明sum(nums) + S必须是偶数,否则输出为0
因此可以将该题用动态规划的方式求解,dp[ j ] :j 为nums数组中sum(P)结果为 j 的子集总数
class Solution {
/**
* @param Integer[] $nums
* @param Integer $S
* @return Integer
*/
function findTargetSumWays($nums, $S) {
$sum = 0; //计算nums的总和
foreach ($nums as $num) {
$sum += $num;
}
if($sum < $S || ($sum + $S) % 2 != 0) return 0; //初始化判断 全部取正是否可以求解 , target是否为偶数
$target = ($sum + $S) / 2 ; //初始化target
$dp = []; //初始化dp动态规划数组 为 0
for($i = 0;$i <= $target; ++$i){
$dp[$i] = 0;
}
$dp[0] = 1; //记录dp[0] 为1 :意为遍历到的数刚好可以填充 j 目标值
foreach ($nums as $num) { //遍历 nums 数组
for($j = $target;$j >= $num;--$j){ //从target 遍历 直到 记录的目标值小于填充值
//记录可以填充当前 j 目标值的所有子集数
$dp[$j] += $dp[$j - $num]; //当前目标的子集数 可以 加上 小的目标值(当前目标 - 遍历到的数字)的实现子集数
}
}
return $dp[$target];
}
}