题目 划分为k个相等的子集
给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。
示例 1:
输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。
解
根据题意,可以先求出数组的和,然后除以集合数k,可以得到每个子集的和n是多少。再分别采用不同的方法求解本体
递归回溯方法1
设置一个访问数组visit,全部元素的和sum。
- sum取n余数,获取剩余取值的上限t
- 循环遍历数组,如果元素未访问且小于等于t,访问设置为true,则sum减去该值,递归继续处理。
- 成功返回true;否则元素访问还原为false,继续查看下一个元素。
- 数组没有满足条件的元素,则返回false
class Solution {
public:
bool canPartitionKSubsets(vector<int>& nums, int k) {
//k=1.则返回true
if(1 == k) return true;
//求全部元素的和
int sum = accumulate(nums.begin() , nums.end() , 0);
//若sum不整除k,则返回false
if(sum%k != 0) return false;
//访问数组
vector<int> visit(nums.size() , false);
return getSolution(nums , visit , sum/k , sum);
}
bool getSolution(vector<int>& nums , vector<int> visit , int n , int sum)
{
//若和减为0,则存在数组,返回真
if(sum == 0) return true;
//t为sum区域n,由于防止取余0,则先减1取余再加1
int t = (sum-1)%n+1;
//循环遍历数组
for(int i=0 ; i<nums.size() ; i++)
{
//数字<=t 且 访问值为false
if(nums[i] <= t && visit[i] == false)
{
//访问设为true
visit[i] = true;
//递归处理,sum-num[i]
if(getSolution(nums , visit , n , sum-nums[i]))
return true;
//不成功还原visited
visit[i] = false;
}
}
return false;
}
};
其中递归回溯法可以进行优化。
优化1: 可以通过一个值的used位表示是否访问,不需要额外设置访问数组
class Solution {
public:
bool canPartitionKSubsets(vector<int>& nums, int k) {
if(1 == k) return true;
int sum = accumulate(nums.begin() , nums.end() , 0);
if(sum%k != 0) return false;
return getSolution(nums , sum/k , 0 , sum);
}
bool getSolution(vector<int>& nums, int n , int used , int sum)
{
if(sum == 0) return true;
int t = (sum-1)%n + 1;
for(int i=0 ; i<nums.size() ; i++)
{
//used对应的第i位是否为0(未访问),且值小于t
if(((used>>i)&1) == 0 && nums[i] <= t)
{
//used对应位设置为1
if(getSolution(nums, n , used|(1<<i) , sum-nums[i]))
return true;
}
}
return false;
}
};
优化2: 通过传入访问的坐标值index
如果sum减去元素恰好满足了一个n,则下一次遍历从0开始遍历;
如果sum减去元素未满足n,则下一次遍历从i+1开始遍历;
class Solution {
public:
bool canPartitionKSubsets(vector<int>& nums, int k) {
if(1 == k) return true;
int sum = accumulate(nums.begin() , nums.end() , 0);
if(sum%k != 0) return false;
vector<int> visit(nums.size() , false);
return getSolution(nums , visit , sum/k , sum , 0);
}
bool getSolution(vector<int>& nums , vector<int> visit , int n , int sum , int index)
{
int i=0;
if(sum == 0) return true;
int t = (sum-1)%n+1;
for(int i= index ; i<nums.size() ; i++)
{
if(nums[i] <= t && visit[i] == false)
{
visit[i] = true;
if(((sum-nums[i])%n != 0 &&getSolution(nums , visit , n , sum-nums[i] , i+1)) ||
((sum-nums[i])%n == 0 &&getSolution(nums , visit , n , sum-nums[i] , 0)))
return true;
visit[i] = false;
}
}
return false;
}
};
递归回溯方法2
额外设置一个数组groups,排序nums数组,并从后向前递归处理原数组的每个元素,遍历groups,如果加上递归处理的nums元素小于n,继续处理nums下一个元素,直到头则返回true。
class Solution {
public:
bool canPartitionKSubsets(vector<int>& nums, int k) {
//k=1 返回真
if(1 == k) return true;
//求和
int sum = accumulate(nums.begin() , nums.end() , 0);
//不能整除,返回false
if(sum % k != 0) return false;
//获取子集和
int n = sum/k;
//从小到大排序
sort(nums.begin() , nums.end());
//row取最后一个元素
int row = nums.size()-1;
//首先从后向前和 n 进行比较
//元素大于等于n
while(nums[row] >= n)
{
//如果大于n,则返回false
if(nums[row] > n) return false;
//如果等于n,row向前移动,且分配的子集数也减1
row--;
k--;
}
//元素小于n,则递归开始处理
//groups数组,存放每个子集的和
vector<int> groups(k , 0);
return getSolution(groups , nums , row , n , k);
}
bool getSolution(vector<int> groups , vector<int> nums , int row , int n , int k)
{
//遍历到头,返回真
if(row < 0)
return true;
//取出值,并且row减1
int val = nums[row--];
//遍历groups数组
for(int i=0 ; i<groups.size() ; i++)
{
//如果group[i]还未达到n,则将val加和到group[i]上
if(groups[i] + val <= n)
{
groups[i] += val;
//并递归处理下一个元素,成功返回真,失败还原
if(getSolution(groups , nums , row , n , k))
return true;
//还原
groups[i] -= val;
}
//还原后为0的话,返回false
if(groups[i] == 0) break;
}
return false;
}
};