LeetCode/划分k个相等的子集

给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等

一. 回溯法

1. 对每个数,回溯放入子集(球视角)

只关心每个球是否成功放入,球在k个容器中做选择,做出选择后进入下一个状态

class Solution {
public:
    bool canPartitionKSubsets(vector<int>& nums, int k) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if (sum % k) return false;
        sum /= k;
        int n = nums.size();
        vector<int> cur(k);
        sort(nums.begin(), nums.end(), greater<int>());
        return backtrack(nums,cur,0,sum);
    }
    //回溯为真,表示下标i及以后的球成功放入容器
    bool backtrack(vector<int>&nums,vector<int>&cur,int i,int target){
            if (i == nums.size())  return true;//边界条件,表示所有球放完
            for (int j = 0; j < cur.size(); ++j) {//遍历所有容器
                if (j && cur[j] == cur[j - 1]) //剪枝,跳过等价容器
                    continue;
                cur[j] += nums[i];//选择把该球放入当前容器
                if (cur[j] <= target && backtrack(nums,cur,i+1,target))//递归判断
                    return true;
                cur[j] -= nums[i];//撤销选择
            }
            return false;
    }
};

2. 对每个子集,回溯放入数(桶视角)

关心每一个容器是否装满,装满后递归判断下一个容器,每个容器从数组中遍历选数,并记录已经选过的数
这里用位图记录选过的数,同时用作记忆化搜索,辅助剪枝

class Solution {
public:
    unordered_map<int,bool> m;//位图记录所有状态
    bool canPartitionKSubsets(vector<int>& nums, int k) {
        int sum = accumulate(nums.begin(),nums.end(),0);
        if(sum%k) return 0;
        sum/=k;
        int state = 0;
        vector<int> cur(k+1);
        return backtrack(nums,0,cur,k,sum,state);//前k个桶能用start起始的数装满
    }
    bool backtrack(vector<int>&nums,int start,vector<int>&cur,int k,int target,int state){
        if(k==0) return true;//边界条件,k个容器皆装满
        if(cur[k]==target){//当前桶已装满
            bool res = backtrack(nums,0,cur,k-1,target,state);//转移桶的状态
            m[state] = res;
            return res;
        }
        if(m.count(state)) return m[state];//剪枝
        for(int i=start;i<nums.size();i++){
            if((state >> i) & 1) continue;//i位数字已被使用,跳过
            if(cur[k]+nums[i]>target) continue;//桶溢出,跳过
            cur[k]+=nums[i];//做选择
            state|= 1<<i;//对第i个数做标记,表示已使用
            if(backtrack(nums,i+1,cur,k,target,state)) return true;//递归判断,转移数的状态
            //撤销选择
            cur[k]-=nums[i];
            state^=1<<i;//异或操作将第i位的1置为0
        }
        return false;
    }
};

二. 动态规划

回溯法记忆化搜索的自底向上

class Solution {
public:
    bool canPartitionKSubsets(vector<int>& nums, int k) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if (sum % k ) return false;
        int per = sum / k; 
        sort(nums.begin(), nums.end());
        if (nums.back() > per) return false;
        int n = nums.size();
        vector<bool> dp(1 << n, false);//记录所有状态的动态规划表
        vector<int> curSum(1 << n, 0);
        dp[0] = true;
        for (int i = 0; i < 1 << n; i++) {
            if (!dp[i]) continue;
            for (int j = 0; j < n; j++) {
                if (curSum[i] + nums[j] > per) 
                    break;
                if (((i >> j) & 1) == 0) {
                    int next = i | (1 << j);
                    if (!dp[next]) {
                        curSum[next] = (curSum[i] + nums[j]) % per;
                        dp[next] = true;
                    }
                }
            }
        }
        return dp[(1 << n) - 1];
    }
};
posted @ 2022-09-20 10:06  失控D大白兔  阅读(30)  评论(0编辑  收藏  举报