经典回溯算法:集合划分问题 :: 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.
-
文章链接点这里:经典回溯算法:集合划分问题
评论礼仪 见这里,违者直接拉黑。
Beta Was this translation helpful? Give feedback.
All reactions