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; } };
我們也可以對上面的解法進行一些優化,比如先給數組按從大到小的順序排個序,然后在遞歸函數中,我們可以直接判斷,如果curSum大於target了,直接返回false,因為題目中限定了都是正數,並且我們也給數組排序了,后面的數字只能更大,這個剪枝操作大大的提高了運行速度,感謝熱心網友 hellow_world00 提供,參見代碼如下:
解法二:
class Solution { public: bool canPartitionKSubsets(vector<int>& nums, int k) { int sum = accumulate(nums.begin(), nums.end(), 0); if (sum % k != 0) return false; sort(nums.begin(), nums.end(), greater<int>()); 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 false; 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; } };
類似題目:
參考資料:
https://leetcode.com/problems/partition-to-k-equal-sum-subsets/