经典回溯算法:集合划分问题 :: labuladong的算法小抄 #1043
Replies: 51 comments 20 replies
-
@luckywords 我感觉你的解法和东哥那个遍历数字解法本质是一样的。你尝试对nums排序了吗? |
Beta Was this translation helpful? Give feedback.
-
我是发现了,我脑袋不够用 |
Beta Was this translation helpful? Give feedback.
-
看着是感觉自己好像懂了,实际上这类型题还是找不到头绪 |
Beta Was this translation helpful? Give feedback.
-
@luckywords @0xlightyear |
Beta Was this translation helpful? Give feedback.
-
@luckywords @0xlightyear @Qiqiqiqiqishuo |
Beta Was this translation helpful? Give feedback.
-
for (int i = 0; i < bucket.length; i++) {
// 剪枝,桶装装满了
if (bucket[i] + nums[index] > target) {
continue;
}
// 将 nums[index] 装入 bucket[i]
bucket[i] += nums[index];
// 递归穷举下一个数字的选择
if (backtrack(nums, index + 1, bucket, target)) {
return true;
}
// 撤销选择
bucket[i] -= nums[index];
//剪枝
if(bucket[i] == 0) break;
} 添加最后一个剪枝,就能不超时了 |
Beta Was this translation helpful? Give feedback.
-
@zhao2019010704 想问一下是为什么啊 ? |
Beta Was this translation helpful? Give feedback.
-
@zhao2019010704 大佬能解释一下为啥最后一个桶如果等于0就跳出就等于剪枝呢,很巧妙 |
Beta Was this translation helpful? Give feedback.
-
if(bucket[i] == 0) break;这个剪枝我感觉像是int[] nums里面的某一个数循环了一圈都没有找到合适的数去配队凑成target,所以后面的都不用在看了,必定凑不成题目说的那种。 |
Beta Was this translation helpful? Give feedback.
-
// 用@zhao2019010704大佬的方式加工的
public boolean helper(int[] bucket, int target, int[] nums, int index) {
// base case,index来记录当前循环到第几个数据了
if(index == nums.length) {
// nums[]遍历完了,这时候说明所有数都找到了桶,如果没找到的话会在bucket[i] == 0被剪掉
return true;
}
// 穷举 nums[index] 可能装入的桶
for(int i = 0; i < bucket.length; i++) {
if(bucket[i] + nums[index] > target) {
continue;
}
// 装进去
bucket[i] += nums[index];
if(help(bucket, target, nums, index + 1)) {
return true;
}
// 到这里说明上面没有return,修建
bucket[i] -= nums[index];
// 这里可能index还没到nums.length,但是出现了无法凑成target的数,所以直接返回break,然后fasle就行
if(bucket[i] == 0) {
// nums[index]找不到可以凑成target的数
break;
}
}
return false;
} |
Beta Was this translation helpful? Give feedback.
-
boolean backtrack(int k, int bucket,
...
/*
if (k == 0) {
return true;
}*/
/*用 k==1 来进行判断也是可以的,因为除了最后一个桶没有确定外,其他的桶都确定了其中的数字(桶中数字的和等于target),那么还没有被添加到桶中的数字必定会被添加到最后一个桶中,因此就不需要在额外判断
*/
if(k==1) return true;
... |
Beta Was this translation helpful? Give feedback.
-
根据大佬们的剪枝和解释,再+一个剪枝 |
Beta Was this translation helpful? Give feedback.
-
位运算不熟练的同学,可以用bitset 嘿嘿 class Solution {
public:
bool canPartitionKSubsets(vector<int> &nums, int k) {
typedef bitset<20> bs20;
if (k > nums.size())return false;
int sum = 0;
for (int i: nums)sum += i;
if (sum % k)return false;
int target = sum / k;
//最大的数比target大
if(nums[0]>target)return false;
unordered_set<bs20> memo;
sort(nums.begin(), nums.end(), greater<int>());
function<bool(int, int, bs20)> backtrack = [&](int at, int sum, bs20 used) {
if (memo.count(used))return false;
if (at == k) {
return true;
}
if (sum == target) {
return backtrack(at + 1, 0, used);
} else {
for (int i = 0; i < nums.size(); i++) {
if (used[i])continue;
if (sum + nums[i] > target)continue;
sum += nums[i];
used[i] = 1;
if (backtrack(at, sum, used)) {
return true;
} else {
//记录失败情况
memo.insert(used);
}
sum -= nums[i];
used[i] = 0;
//没有能够凑成target
if(sum==0)break;
}
}
return false;
};
return backtrack(0, 0, 0);
}
};
|
Beta Was this translation helpful? Give feedback.
-
if(bucket[i] == 0) break; 个人理解是:当前的桶经过上面的试探后无法满足条件,桶还是空的,所以后面的桶也不用试了,不可能满足条件,所以直接返回false。不知道这样理解对不对呢? |
Beta Was this translation helpful? Give feedback.
-
贴一个解法,增加两种剪枝,第二种剪枝不怎么用,只有在数组中具有大量和target相等的数时才有用。 class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
total_sum = sum(nums)
if total_sum % k != 0:
return False
self.target = total_sum // k
nums = sorted(nums, reverse=True)
if nums[0] > self.target:
return False
# start, end = -1, -1
# for i,v in enumerate(nums):
# if v == self.target:
# if start == -1:
# start = i
# end = i
# else:
# end = i
# elif v < self.target:
# break
# if start != -1:
# nums = nums[:start]+nums[end+1:]
# k = k-(end-start+1)
self.nums = nums
self.k = k
self.used = 0
self.mem = dict() # 备忘录,避免不同子集的相同组合数(因为这题关注的每个子集组合数的总和,不需要关心子集的顺序)
return self.DFS(0, 0, 0)
def DFS(self, k, cur, start):
if k == self.k:
return True
if self.target == cur:
res = self.DFS(k+1, 0, 0)
self.mem[self.used] = res
return res
if self.used in self.mem:
return self.mem[self.used]
for i in range(len(self.nums)):
if cur + self.nums[i] > self.target:
continue
if ((self.used>>i) & 1) == 1:
continue
self.used |= 1<<i
cur += self.nums[i]
if self.DFS(k, cur, i+1):
return True
cur -= self.nums[i]
self.used ^= 1<<i
return False |
Beta Was this translation helpful? Give feedback.
-
感觉这个medium好难啊,这么多层优化 |
Beta Was this translation helpful? Give feedback.
-
看了其他答案和评论,改了改东哥的第一种数字视角的解法,加了些注释进行解释: class Solution {
public:
bool res = false;
void backtrack(vector<int>& nums, int index, vector<int>& buckets, int target) {
// 已经得出答案,直接返回。
if(res) return;
if(index >= nums.size()) {
// index 递归到 nums.size() 时,nums 中的每个元素都放入了某个桶,同时每个桶都满足其元素和 <= target,
// 即 sum(buckets) = sum(nums),相当于 k * target = k * buckets[i],又 bucket[i] <= target,
// 所以必须要是 bucket[i] = target,找到了一个组合满足要求。
res = true;
return;
}
for(int i = 0; i < buckets.size(); i++) {
if(buckets[i] + nums[index] > target)
continue;
// 对于 nums[index],bucket[i-1] 装入后没得到答案,bucket[i] 与 bucket[i-1] 相等,
// 每个桶的和都要满足等于 target,这两个桶装入 nums[index] 的情况应该相同,即都是无法满足要求。
if(i > 0 && buckets[i] == buckets[i-1])
continue;
buckets[i] += nums[index];
backtrack(nums, index + 1, buckets, target);
buckets[i] -= nums[index];
}
}
bool canPartitionKSubsets(vector<int>& nums, int k) {
sort(nums.rbegin(), nums.rend());
int sum = accumulate(nums.begin(), nums.end(), 0);
if(sum % k != 0)
return false;
vector<int> buckets(k, 0); // k 个桶,初始容量为 0
backtrack(nums, 0, buckets, sum/k);
return res;
}
}; |
Beta Was this translation helpful? Give feedback.
-
一点看法:
public boolean canPartitionKSubsets(int[] nums, int k) {
int sum=0;
for (int i = 0; i < nums.length; i++) {
sum+=nums[i];
}
if (sum%k!=0)return false;
Arrays.sort(nums);
//每个子集的和
int targetSum=sum/k;
//抽象桶
int[] bucket=new int[k];
Arrays.fill(bucket,targetSum);
// for (int i = 0; i < bucket.length; i++) {
// LinkedList<Integer> bucketList=new LinkedList<>();
// bucketLists.add(bucketList);
// }
boolean result = dfs(nums, nums.length-1, bucket);
return result;
}
//List<LinkedList<Integer>> bucketLists=new ArrayList<>();
//元素是否可以放入桶
public boolean dfs(int[] nums,int index,int[] bucket){
//base case
if (index<0){
return true;
}
int cur = nums[index];
//记录前面选过的桶的状态
Set<Integer> memo=new HashSet<>();
//选择桶
for (int i=0;i<bucket.length;i++){
//剪枝
if (memo.contains(bucket[i])||bucket[i]-cur<0){
continue;
}
//选择
bucket[i]-=cur;
// LinkedList<Integer> bucketList = bucketLists.get(i);
//bucketList.push(cur);
//下一个数
if (dfs(nums,index-1,bucket)){
return true;
}
//撤销选择
bucket[i]+=cur;
// bucketList.poll();
//记录上次的入桶失败的桶状态
memo.add(bucket[i]);
}
return false;
} |
Beta Was this translation helpful? Give feedback.
-
说一下自己看的时候遇到的问题,自己也是想了一会才发现的,以桶的视角去实现代码的时候之所以每次 for 都是从 start 开始的原因是,以桶的视角来选择数字,本质上来说就是一个数字的组合问题,组合的问题是不需要考虑排列的,所以自然不需要从使 i 每次从 0 开始,虽然从 0 开始也可以解决问题,但是在递归的时候会走出多余的很多的递归,提交的时候就会超时。 |
Beta Was this translation helpful? Give feedback.
-
对东哥的以数组视角算法进行的优化
|
Beta Was this translation helpful? Give feedback.
-
javascript版本
|
Beta Was this translation helpful? Give feedback.
-
思路拆解写的太好了👍🏻 |
Beta Was this translation helpful? Give feedback.
-
if(bucket[i] == 0) break; // 如果nums[index]装入当前桶bucket[i]是失败的,那么继续尝试装入其它的桶也一定失败,所以不用继续试了 |
Beta Was this translation helpful? Give feedback.
-
class Solution {
public:
vector<int> bucket;
vector<bool> visited;
bool backtrace(int k, vector<int>& nums, int target, int start){
if(bucket.size() == k) return true;
if(bucket[k]==target)
return backtrace(k+1, nums, target, 0);
for(int i=start; i<nums.size(); i++){ // i=start 是关键的一步
if(visited[i]) continue;
// int num = nums[i];
if(bucket[k]+nums[i] > target) continue;
visited[i] = true;
bucket[k]+=nums[i];
if(backtrace(k, nums, target,i+1)) return true;
visited[i] = false;
bucket[k]-=nums[i];
}
return false;
}
bool canPartitionKSubsets(vector<int>& nums, int k) {
bucket.resize(k,0);
visited.resize(nums.size(),false);
// sort(nums.begin(), nums.end(),greater<>());
int sum = accumulate(nums.begin(), nums.end(), 0);
if(sum%k!=0) return false;
int target = sum/k;
// if(nums[0] > target) return false;
return backtrace(0,nums,target,0);;
}
}; 哥几个,桶视角,我这个代码为啥后面几个测试点过不去啊?感觉和东哥的差不了多少啊。 |
Beta Was this translation helpful? Give feedback.
-
看这题的解题真是神游,仔仔细细看了两遍才搞懂。2轮优化太厉害,尤其是位图的操作开眼界了,时间从11%->73%。 |
Beta Was this translation helpful? Give feedback.
-
时间 3 ms 击败 75.16% 内存 39.4 MB 击败 44.49% public boolean canPartitionKSubsets(int[] nums, int k) {
// k 前置的两个条件
if (k > nums.length) return false;
int sum = Arrays.stream(nums).sum();
if (sum % k != 0) return false;
int target = sum / k;
// 划分为 k 个子集
int[] bucket = new int[k];
// 优化点1:数组倒序
Arrays.sort(nums);
// 双指针反转数组
for (int i = 0, j = nums.length - 1; i < j; i++, j--) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
return backtrace(nums, 0, bucket, target);
}
private boolean backtrace(int[] nums, int index, int[] bucket, int target) {
if (index == nums.length) {
// 优化点2:这个地方可以不需要再每个都判断,所有球已经按要求放入每个桶,一定满足条件 target
return true;
}
for (int i = 0; i < bucket.length; i++) {
// 优化点3:类似组合可重不可复选问题的剪枝
// 当桶的和相同时,选择当前桶和前面的桶结果是一样的
if (i > 0 && bucket[i] == bucket[i - 1]) continue;
// 剪枝
if (bucket[i] + nums[index] > target) {
continue;
}
bucket[i] += nums[index];
if (backtrace(nums, index + 1, bucket, target)) return true;
bucket[i] -= nums[index];
}
return false;
} |
Beta Was this translation helpful? Give feedback.
-
为什么正向遍历,从0号遍历到k号桶时return true却会报错呢 |
Beta Was this translation helpful? Give feedback.
-
通俗来说,我们应该尽量「少量多次」,就是说宁可多做几次选择(乘法关系),也不要给太大的选择空间(指数关系);做 n 次「k 选一」仅重复一次(O(k^n)),比 n 次「二选一」重复 k 次(O(k*2^n))效率低很多。 我有一點疑惑,在不考慮其它優化的情況下,只考慮桶視角/數字視角,兩種方式不應該是一樣的時間嗎?雖然從上界BIG O 分析看來,桶數角比較小,但如果都需要遍歷所有情況,實際需時不應該是一樣的嗎? 覺得桶視角的最大優勢,在於桶是同質的,因此可以減少很多搜㝷空間(但這點是沒有在BIG O分析下是看不出來的)。 |
Beta Was this translation helpful? Give feedback.
-
对位运算不熟的朋友可以用 使用 class Solution {
public boolean canPartitionKSubsets(int[] nums, int k) {
// 排除一些基本情况
if (k > nums.length) return false;
int sum = 0;
for (int v : nums) sum += v;
if (sum % k != 0) return false;
int target = sum / k;
// k 号桶初始什么都没装,从 nums[0] 开始做选择
boolean[] used = new boolean[nums.length];
return backtrack(k, 0, nums, 0, used, target);
}
HashMap<Integer, Boolean> memo = new HashMap<>();
boolean backtrack(int k, int bucket, int[] nums, int start, boolean[] used, int target) {
// base case
if (k == 0) {
return true;
}
// 将 used 的状态转化成hashCode
// 便于存入 HashMap
int state = Arrays.hashCode(used);
if (bucket == target) {
// 装满了当前桶,递归穷举下一个桶的选择
boolean res = backtrack(k - 1, 0, nums, 0, used, target);
// 将当前状态和结果存入备忘录
memo.put(state, res);
return res;
}
if (memo.containsKey(state)) {
// 如果当前状态曾今计算过,就直接返回,不要再递归穷举了
return memo.get(state);
}
// 从 start 开始向后探查有效的 nums[i] 装入当前桶
for (int i = start; i < nums.length; i++) {
// 剪枝
if (used[i]) {
// nums[i] 已经被装入别的桶中
continue;
}
if (nums[i] + bucket > target) {
// 当前桶装不下 nums[i]
continue;
}
// 做选择,将 nums[i] 装入当前桶中
used[i] = true;
bucket += nums[i];
// 递归穷举下一个数字是否装入当前桶
if (backtrack(k, bucket, nums, i + 1, used, target)) {
return true;
}
// 撤销选择
used[i] = false;
bucket -= nums[i];
}
// 穷举了所有数字,都无法装满当前桶
return false;
}
} |
Beta Was this translation helpful? Give feedback.
-
把回溯和递归重新外出了新高度!!!赞一个 |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
文章链接点这里:经典回溯算法:集合划分问题
评论礼仪 见这里,违者直接拉黑。
Beta Was this translation helpful? Give feedback.
All reactions