[LeetCode] Partition to K Equal Sum Subsets 分割K個等和的子集
Given an array of integers nums
and a positive integer k
, find whether it's possible to divide this array into k
non-empty subsets whose sums are all equal.
Example 1:
Input: nums = [4, 3, 2, 3, 5, 2, 1], k = 4 Output: True Explanation: It's possible to divide it into 4 subsets (5), (1, 4), (2,3), (2,3) with equal sums.
Note:
1 <= k <= len(nums) <= 16
.0 < nums[i] < 10000
.
這道題給了我們一個數組nums和一個數字k,問我們該數字能不能分成k個非空子集合,使得每個子集合的和相同。給了k的範圍是[1,16],而且陣列中的數字都是正數。這跟之前那道Partition Equal Subset Sum很類似,但是那道題只讓分成兩個子集合,所以問題可以轉換為是否存在和為整個陣列和的一半的子集合,可以用dp來做。但是這道題讓求k個和相同的,感覺無法用dp來做,因為就算找出了一個,其餘的也需要驗證。這道題我們可以用遞迴來做,首先我們還是求出陣列的所有數字之和sum,首先判斷sum是否能整除k,不能整除的話直接返回false。然後需要一個visited陣列來記錄哪些陣列已經被選中了,然後呼叫遞迴函式,我們的目標是組k個子集合,是的每個子集合之和為target = sum/k。我們還需要變數start,表示從陣列的某個位置開始查詢,curSum為當前子集合之和,在遞迴函式中,如果k=1,說明此時只需要組一個子集合,那麼當前的就是了,直接返回true。如果curSum等於target了,那麼我們再次呼叫遞迴,此時傳入k-1,start和curSum都重置為0,因為我們當前又找到了一個和為target的子集合,要開始繼續找下一個。否則的話就從start開始遍歷陣列,如果當前數字已經訪問過了則直接跳過,否則標記為已訪問。然後呼叫遞迴函式,k保持不變,因為還在累加當前的子集合,start傳入i+1,curSum傳入curSum+nums[i],因為要累加當前的數字,如果遞迴函式返回true了,則直接返回true。否則就將當前數字重置為未訪問的狀態繼續遍歷,參見程式碼如下:
解法一:
class Solution { public: bool canPartitionKSubsets(vector<int>& nums, int k) { int sum = accumulate(nums.begin(), nums.end(), 0); if (sum % k != 0) return false; vector<bool> visited(nums.size(), false); return helper(nums, k, sum / k, 0, 0, visited); } bool helper(vector<int>& nums, int k, int target, int start, int curSum, vector<bool>& visited) { if (k == 1) return true; if (curSum == target) return helper(nums, k - 1, target, 0, 0, visited); for (int i = start; i < nums.size(); ++i) { if (visited[i]) continue; visited[i] = true; if (helper(nums, k, target, i + 1, curSum + nums[i], visited)) return true; visited[i] = false; } return false; } };
下面這種方法也挺巧妙的,思路是建立長度為k的陣列v,只有當v裡面所有的數字都是target的時候,才能返回true。我們還需要給陣列排個序,由於題目中限制了全是正數,所以數字累加只會增大不會減小,一旦累加超過了target,這個子集合是無法再變小的,所以就不能加入這個數。實際上相當於貪婪演算法,由於題目中陣列數字為正的限制,有解的話就可以用貪婪演算法得到。我們用一個變數idx表示當前遍歷的數字,排序後,我們從末尾大的數字開始累加,我們遍歷陣列v,當前位置加上nums[idx],如果超過了target,我們掉過繼續到下一個位置,否則就呼叫遞迴,此時的idx為idx-1,表示之前那個數字已經成功加入陣列v了,我們嘗試著加下一個數字。如果遞迴返回false了,我們就將nums[idx]從陣列v中對應的位置減去,還原狀態,然後繼續下一個位置。如果某個遞迴中idx等於-1了,表明所有的數字已經遍歷完了,此時我們檢查陣列v中k個數字是否都為target,是的話返回true,否則返回false,參見程式碼如下:
解法二:
class Solution { public: bool canPartitionKSubsets(vector<int>& nums, int k) { int sum = accumulate(nums.begin(), nums.end(), 0); if (sum % k != 0) return false; vector<int> v(k, 0); sort(nums.begin(), nums.end()); return helper(nums, sum / k, v, (int)nums.size() - 1); } bool helper(vector<int>& nums, int target, vector<int>& v, int idx) { if (idx == -1) { for (int t : v) { if (t != target) return false; } return true; } int num = nums[idx]; for (int i = 0; i < v.size(); ++i) { if (v[i] + num > target) continue; v[i] += num; if (helper(nums, target, v, idx - 1)) return true; v[i] -= num; } return false; } };
類似題目:
參考資料: