日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 >

力扣--目标和

發(fā)布時間:2024/4/11 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 力扣--目标和 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

力扣–目標(biāo)和

文章目錄

  • 力扣--目標(biāo)和
    • 一、題目描述
    • 二、分析
      • 方法一:枚舉
      • 方法二:回溯【超時】
      • 方法三:消除重疊子問題【超時】
      • 方法四:動態(tài)規(guī)劃

一、題目描述


class Solution { public:int findTargetSumWays(vector<int>& nums, int S) {} };

二、分析

方法一:枚舉

class Solution { public://保存結(jié)果int count = 0;//start代表當(dāng)前在nums的下標(biāo)索引//sum代表當(dāng)前+/-運算之后的結(jié)果//target代表目標(biāo)結(jié)果void dp(vector<int>& nums, int start, int sum, int target){//遍歷完數(shù)組并且sum和target相等才++countif(start == nums.size()){if(target == sum){count++;}return ;}//+dp(nums,start + 1,sum + nums[start],target);//-dp(nums,start + 1,sum - nums[start],target);}int findTargetSumWays(vector<int>& nums, int S) {if(nums.empty()){return 0;}dp(nums, 0, 0, S);return count;} };

方法二:回溯【超時】

  • 任何算法的核心都是窮舉,回溯算法就是一個暴力窮舉算法,回溯算法框架:
def backtrack(路徑, 選擇列表):if 滿足結(jié)束條件:result.add(路徑)returnfor 選擇 in 選擇列表:做選擇backtrack(路徑, 選擇列表)撤銷選擇
  • 關(guān)鍵就是搞清楚什么是「選擇」,而對于這道題,「選擇」不是明擺著的嗎?
  • 對于每個數(shù)字 nums[i],我們可以選擇給一個正號 + 或者一個負(fù)號 -,然后利用回溯模板窮舉出來所有可能的結(jié)果,數(shù)一數(shù)到底有幾種組合能夠湊出 target 不就行了嘛?
  • 偽碼思路如下:
def backtrack(nums, i):if i == len(nums):if 達(dá)到 target:result += 1returnfor op in { +1, -1 }:選擇 op * nums[i]# 窮舉 nums[i + 1] 的選擇backtrack(nums, i + 1)撤銷選擇
  • 代碼
int result = 0;/* 主函數(shù) */ int findTargetSumWays(int[] nums, int target) {if (nums.length == 0) return 0;backtrack(nums, 0, target);return result; }/* 回溯算法模板 */ void backtrack(int[] nums, int i, int rest) {// base caseif (i == nums.length) {if (rest == 0) {// 說明恰好湊出 targetresult++;}return;}// 給 nums[i] 選擇 - 號rest += nums[i];// 窮舉 nums[i + 1]backtrack(nums, i + 1, rest);// 撤銷選擇rest -= nums[i]; // 給 nums[i] 選擇 + 號rest -= nums[i];// 窮舉 nums[i + 1]backtrack(nums, i + 1, rest);// 撤銷選擇rest += nums[i]; }
  • 有的讀者可能問,選擇 - 的時候,為什么是 rest += nums[i],選擇 + 的時候,為什么是 rest -= nums[i] 呢,是不是寫反了?
  • 不是的,「如何湊出 target」和「如何把 target 減到 0」其實是一樣的。我們這里選擇后者,因為前者必須給 backtrack 函數(shù)多加一個參數(shù),我覺得不美觀:
void backtrack(int[] nums, int i, int sum, int target) {// base caseif (i == nums.length) {if (sum == target) {result++;}return;}// ... }
  • 因此,如果我們給 nums[i] 選擇 + 號,就要讓 rest - nums[i],反之亦然。
  • 以上回溯算法可以解決這個問題,時間復(fù)雜度為O(2N)O(2^N)O(2N),N 為 nums 的大小。
  • 進一步觀察發(fā)現(xiàn)這個回溯算法就是個二叉樹的遍歷問題:
void backtrack(int[] nums, int i, int rest) {if (i == nums.length) {return;}backtrack(nums, i + 1, rest - nums[i]);backtrack(nums, i + 1, rest + nums[i]); }
  • 樹的高度就是 nums 的長度嘛,所以說時間復(fù)雜度就是這棵二叉樹的節(jié)點數(shù),為 O(2N)O(2^N)O(2N),其實是非常低效的。

方法三:消除重疊子問題【超時】

  • 動態(tài)規(guī)劃之所以比暴力算法快,是因為動態(tài)規(guī)劃技巧消除了重疊子問題。
  • 如何發(fā)現(xiàn)重疊子問題?看是否可能出現(xiàn)重復(fù)的「狀態(tài)」。對于遞歸函數(shù)來說,函數(shù)參數(shù)中會變的參數(shù)就是「狀態(tài)」,對于 backtrack 函數(shù)來說,會變的參數(shù)為 i 和 rest。
  • 先抽象出遞歸框架:
void backtrack(int i, int rest) {backtrack(i + 1, rest - nums[i]);backtrack(i + 1, rest + nums[i]); }
  • 舉個簡單的例子,如果 nums[i] = 0,會發(fā)生什么?
void backtrack(int i, int rest) {backtrack(i + 1, rest);backtrack(i + 1, rest); }
  • 你看,這樣就出現(xiàn)了兩個「狀態(tài)」完全相同的遞歸函數(shù),無疑這樣的遞歸計算就是重復(fù)的。
  • 這就是重疊子問題,而且只要我們能夠找到一個重疊子問題,那一定還存在很多的重疊子問題。
  • 因此,狀態(tài) (i, rest) 是可以用備忘錄技巧進行優(yōu)化的:
int findTargetSumWays(int[] nums, int target) {if (nums.length == 0) return 0;return dp(nums, 0, target); }// 備忘錄 HashMap<String, Integer> memo = new HashMap<>(); int dp(int[] nums, int i, int rest) {// base caseif (i == nums.length) {if (rest == 0) return 1;return 0;}// 把它倆轉(zhuǎn)成字符串才能作為哈希表的鍵String key = i + "," + rest;// 避免重復(fù)計算if (memo.containsKey(key)) {return memo.get(key);}// 還是窮舉int result = dp(nums, i + 1, rest - nums[i]) + dp(nums, i + 1, rest + nums[i]);// 記入備忘錄memo.put(key, result);return result; }
  • 這個解法通過備忘錄消除了很多重疊子問題,效率有一定的提升,但是這就結(jié)束了嗎?

方法四:動態(tài)規(guī)劃

  • 事情沒有這么簡單,先來算一算,消除重疊子問題之后,算法的時間復(fù)雜度是多少?其實最壞情況下依然是 O(2N)O(2^N)O(2N)
  • 為什么呢?因為我們只不過恰好發(fā)現(xiàn)了重疊子問題,順手用備忘錄技巧給優(yōu)化了,但是底層思路沒有變,依然是暴力窮舉的回溯算法,依然在遍歷一棵二叉樹。
  • 這只能叫對回溯算法進行了「剪枝」,提升了算法在某些情況下的效率,但算不上質(zhì)的飛躍。
  • 其實,這個問題可以轉(zhuǎn)化為一個子集劃分問題,而子集劃分問題又是一個典型的背包問題。動態(tài)規(guī)劃總是這么玄學(xué),讓人摸不著頭腦……
  • 首先,如果我們把 nums 劃分成兩個子集 A 和 B,分別代表分配 + 的數(shù)和分配 - 的數(shù),那么他們和 target 存在如下關(guān)系:
sum(A) - sum(B) = target sum(A) = target + sum(B) sum(A) + sum(A) = target + sum(B) + sum(A) 2 * sum(A) = target + sum(nums)
  • 綜上,可以推出 sum(A)=(target+sum(nums))/2sum(A) = (target + sum(nums)) / 2sum(A)=(target+sum(nums))/2 ,也就是把原問題轉(zhuǎn)化成:nums 中存在幾個子集A,使得 A 中元素的和為(target+sum(nums))/2(target + sum(nums)) / 2(target+sum(nums))/2
  • 類似的子集劃分問題我們前文 經(jīng)典背包問題:子集劃分 講過,現(xiàn)在實現(xiàn)這么一個函數(shù):
/* 計算 nums 中有幾個子集的和為 sum */ int subsets(int[] nums, int sum) {}//然后,可以這樣調(diào)用這個函數(shù): int findTargetSumWays(int[] nums, int target) {int sum = 0;for (int n : nums) sum += n;// 這兩種情況,不可能存在合法的子集劃分if (sum < target || (sum + target) % 2 == 1) {return 0;}return subsets(nums, (sum + target) / 2); }
  • 好的,變成背包問題的標(biāo)準(zhǔn)形式:
  • 有一個背包,容量為 sum,現(xiàn)在給你 N 個物品,第 i 個物品的重量為 nums[i - 1](注意 1 <= i <= N),每個物品只有一個,請問你有幾種不同的方法能夠恰好裝滿這個背包?
  • 現(xiàn)在,這就是一個正宗的動態(tài)規(guī)劃問題了,下面按照我們一直強調(diào)的動態(tài)規(guī)劃套路走流程:
  • 第一步要明確兩點,「狀態(tài)」和「選擇」
  • 對于背包問題,這個都是一樣的,狀態(tài)就是「背包的容量」和「可選擇的物品」,選擇就是「裝進背包」或者「不裝進背包」。
  • 第二步要明確 dp 數(shù)組的定義。
  • 按照背包問題的套路,可以給出如下定義:
  • dp[i][j] = x 表示,若只在前 i 個物品中選擇,若當(dāng)前背包的容量為 j,則最多有 x 種方法可以恰好裝滿背包。
  • 翻譯成我們探討的子集問題就是,若只在 nums 的前 i 個元素中選擇,若目標(biāo)和為 j,則最多有 x 種方法劃分子集。
  • 根據(jù)這個定義,顯然 dp[0][..] = 0,因為沒有物品的話,根本沒辦法裝背包;dp[..][0] = 1,因為如果背包的最大載重為 0,「什么都不裝」就是唯一的一種裝法。
  • 我們所求的答案就是 dp[N][sum],即使用所有 N 個物品,有幾種方法可以裝滿容量為 sum 的背包。
  • 第三步,根據(jù)「選擇」,思考狀態(tài)轉(zhuǎn)移的邏輯。
  • 回想剛才的 dp 數(shù)組含義,可以根據(jù)「選擇」對 dp[i][j] 得到以下狀態(tài)轉(zhuǎn)移:
  • 如果不把 nums[i] 算入子集,或者說你不把這第 i 個物品裝入背包,那么恰好裝滿背包的方法數(shù)就取決于上一個狀態(tài)dp[i-1][j],繼承之前的結(jié)果。
  • 如果把 nums[i] 算入子集,或者說你把這第 i 個物品裝入了背包,那么只要看前 i - 1 個物品有幾種方法可以裝滿 j -nums[i-1] 的重量就行了,所以取決于狀態(tài) dp[i-1][j-nums[i-1]]。
  • PS:注意我們說的 i 是從 1 開始算的,而數(shù)組 nums 的索引時從 0 開始算的,所以 nums[i-1] 代表的是第 i個物品的重量,j - nums[i-1] 就是背包裝入物品 i 之后還剩下的容量。
  • 由于 dp[i][j] 為裝滿背包的總方法數(shù),所以應(yīng)該以上兩種選擇的結(jié)果求和,得到狀態(tài)轉(zhuǎn)移方程:
  • dp[i][j]=dp[i?1][j]+dp[i?1][j?nums[i?1]]dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]]dp[i][j]=dp[i?1][j]+dp[i?1][j?nums[i?1]];
  • 然后,根據(jù)狀態(tài)轉(zhuǎn)移方程寫出動態(tài)規(guī)劃算法:
/* 計算 nums 中有幾個子集的和為 sum */ int subsets(int[] nums, int sum) {int n = nums.length;int[][] dp = new int[n + 1][sum + 1];// base casefor (int i = 0; i <= n; i++) {dp[i][0] = 1;}for (int i = 1; i <= n; i++) {for (int j = 0; j <= sum; j++) {if (j >= nums[i-1]) {// 兩種選擇的結(jié)果之和dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];} else {// 背包的空間不足,只能選擇不裝物品 idp[i][j] = dp[i-1][j];}}}return dp[n][sum]; }
  • 然后,發(fā)現(xiàn)這個 dp[i][j] 只和前一行 dp[i-1][…] 有關(guān),那么肯定可以優(yōu)化成一維 dp:
/* 計算 nums 中有幾個子集的和為 sum */ int subsets(int[] nums, int sum) {int n = nums.length;int[] dp = new int[sum + 1];// base casedp[0] = 1;for (int i = 1; i <= n; i++) {// j 要從后往前遍歷for (int j = sum; j >= 0; j--) {// 狀態(tài)轉(zhuǎn)移方程if (j >= nums[i-1]) {dp[j] = dp[j] + dp[j-nums[i-1]];} else {dp[j] = dp[j];}}}return dp[sum]; }
  • 對照二維 dp,只要把 dp 數(shù)組的第一個維度全都去掉就行了,唯一的區(qū)別就是這里的 j 要從后往前遍歷,原因如下:
  • 因為二維壓縮到一維的根本原理是,dp[j]和dp[j?nums[i?1]]dp[j] 和 dp[j-nums[i-1]]dp[j]dp[j?nums[i?1]] 還沒被新結(jié)果覆蓋的時候,相當(dāng)于二維 dp 中的dp[i?1][j]和dp[i?1][j?nums[i?1]]dp[i-1][j] 和 dp[i-1][j-nums[i-1]]dp[i?1][j]dp[i?1][j?nums[i?1]]
  • 那么,我們就要做到:在計算新的 dp[j] 的時候,dp[j] 和 dp[j-nums[i-1]] 還是上一輪外層 for循環(huán)的結(jié)果。
  • 如果你從前往后遍歷一維 dp 數(shù)組,dp[j] 顯然是沒問題的,但是 dp[j-nums[i-1]] 已經(jīng)不是上一輪外層 for循環(huán)的結(jié)果了,這里就會使用錯誤的狀態(tài),當(dāng)然得不到正確的答案。
  • 完整C++代碼
class Solution { public:/* 計算 nums 中有幾個子集的和為 sum */int subsets(vector<int>& nums, int sum) {int n = nums.size();vector<int> dp (sum + 1,0);// base casedp[0] = 1;for (int i = 1; i <= n; i++) {// j 要從后往前遍歷for (int j = sum; j >= 0; j--) {// 狀態(tài)轉(zhuǎn)移方程if (j >= nums[i-1]) {dp[j] = dp[j] + dp[j-nums[i-1]];} else {dp[j] = dp[j];}}}return dp[sum];}int findTargetSumWays(vector<int>& nums, int S) {if(nums.empty()){return 0;}int sum = 0;for(auto e : nums){sum += e;}if (sum < S || (sum + S) % 2 == 1) {return 0;}int target = (sum + S) / 2;return subsets(nums, target);} };
  • 現(xiàn)在,這道題算是徹底解決了。
  • 總結(jié)一下,回溯算法雖好,但是復(fù)雜度高,即便消除一些冗余計算,也只是「剪枝」,沒有本質(zhì)的改進
  • 而動態(tài)規(guī)劃就比較玄學(xué)了,經(jīng)過各種改造,從一個加減法問題變成子集問題,又變成背包問題,經(jīng)過各種套路寫出解法,又搞出狀態(tài)壓縮,還得反向遍歷。

總結(jié)

以上是生活随笔為你收集整理的力扣--目标和的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。