首页 > 编程语言 >[算法]分割等和子集算法 & bitset容器应用

[算法]分割等和子集算法 & bitset容器应用

时间:2024-03-26 10:26:37浏览次数:13  
标签:return tar nums sum vis 算法 num 子集 bitset

LeetCode 416. 分割等和子集

1 题目描述

给你一个只包含正整数非空数组nums。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

1.1 输入测试

示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

1.2 数据范围

1 <= nums.length <= 200
1 <= nums[i] <= 100


2 解题思路

将数组分割成两个元素和相等的子集,意味着这两个子集的元素和都是总元素和sum的一半。
所以,道题的目标就是判断是否存在元素和为sum/2的子集。

其中包含的一个特殊情况是,当sum为奇数时,结果明显是不存在的。


2.1 DFS枚举

初看数据量比较小,于是就尝试DFS暴力枚举,最后果不其然超时了。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int tar = 0;

        for (auto &num : nums)
        {
            tar += num;
        }

        if (tar & 1)
        {
            return false;
        }
        return dfs(nums, 0, 0, tar >> 1);
    }

    bool dfs(vector<int>& nums, int i, int sum, int tar)
    {
        if (sum == tar)
        {
            return true;
        }
        if (sum > tar)
        {
            return false;
        }

        return i == nums.size() ? false : dfs(nums, i + 1, sum + nums[i], tar) || dfs(nums, i + 1, sum, tar);
    }
};

2.2 记忆化搜索

既然暴力枚举不行,那就加上记忆化搜索,通过记录相同sum和i时的结果,达到减少重复计算的目的。

最终成功通过,运行时间只有55ms,但其实还有更优的解法

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int tar = 0;

        for (auto &num : nums)
        {
            tar += num;
        }

        if (tar & 1)
        {
            return false;
        }
        return dfs(nums, 0, 0, tar >> 1);
    }

    bool vis[10001][201]{};
    bool mem[10001][201]{};

    bool dfs(vector<int>& nums, int i, int sum, int tar)
    {
        if (sum == tar)
        {
            return true;
        }
        if (sum > tar)
        {
            return false;
        }

        if (vis[sum][i])
        {
            return mem[sum][i];
        }

        vis[sum][i] = true;

        return mem[sum][i] = i == nums.size() ? false : dfs(nums, i + 1, sum + nums[i], tar) || dfs(nums, i + 1, sum, tar);
    }
};

2.3 DP

重新观察上面的计算过程就能发现,实际上我们所做的就是在枚举所有可能的子集和,这其实就是典型的0/1背包问题。
因此,我们可以采用动态规划的思想,构造一个初始只包含一个数字0的子集和数组v,接着依次从nums集合中取出一个数,与数组v中的子集和逐个相加得到一批新的子集和,并添加进子集和数组v。直到求得目标结果,或是所有数字都被取出。

这种写法的运行时间仅为15ms,但接下来我们还要介绍另一种DP的写法,为后面的bitset优化作铺垫。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int tar = 0;
        vector<int> v{0};
        bool vis[10001]{};

        for (auto &num : nums)
        {
            tar += num;
        }

        if (tar & 1)
        {
            return false;
        }
        tar >>= 1;
        
        for (auto &num : nums)
        {
            for (int i = v.size() - 1; i >= 0; i--)
            {
                int n = num + v[i];
                
                if (n == tar)
                {
                    return true;
                }
                if (n > tar || vis[n])
                {
                    continue;
                }
                vis[n] = true;
                v.push_back(n);
            }
        }
        
        return false;
    }
};

2.4 DP + bitset

在上面的DP写法中,我们使用了数组v记录已知的子集和,但也可以看出vis数组同时也记录了对应下标值的子集和是否存在。
因此,我们可以不使用v数组,而是通过遍历vis数组的方式来判断子集和是否存在。当然,根据0/1背包的特性,遍历的过程需要从后往前。

最终运行时间为23ms,比第一个DP方法稍慢,毕竟我们是遍历范围内所有可能的下标,而不像之前有额外的记录可以快速访问,但这么做也能相应地减少内存消耗。

不过这并不是我们的最终目标。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int tar = 0;
        bool vis[10001]{1};

        for (auto &num : nums)
        {
            tar += num;
        }

        if (tar & 1)
        {
            return false;
        }
        tar >>= 1;
        
        for (auto &num : nums)
        {
            for (int i = tar; i >= num; i--)
            {
                // 等价于 vis[i] |= vis[i - num];
                // if (vis[i - num])
                // {
                //     vis[i] = true;
                // }
                vis[i] |= vis[i - num];
            }
        }
        
        return vis[tar];
    }
};

其实从上面的内层for循环中可以看出,我们每次所做的操作都是将第i位元素与第i偏移num位的元素进行或运算。所以,我们可以把内层循环的结果等价为,对整个vis数组做移位或运算,也就是vis |= vis << num

但即使我们知道这样的等价操作,也没办法直接对整个bool数组进行移位,这时候就需要用到支持<<>>位移操作的bitset容器来实现了。

其使用方法也很简单,只需要将原来的bool vis[N]替换成bitset<N> vis即可。

于是,我们的运行时间成功缩短到了5ms。对比第一种DP算法,这种方法在减少内存占用的同时也缩短了运行时间。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int tar = 0;
        bitset<10001> vis{1};

        for (auto &num : nums)
        {
            tar += num;
        }

        if (tar & 1)
        {
            return false;
        }
        tar >>= 1;
        
        for (auto &num : nums)
        {
            vis |= vis << num;
        }
        
        return vis[tar];
    }
};

其实到了这一步,我们还能将代码逻辑进行进一步的合并。将tar的计算合并到vis的for循环中,并在结尾统一对tar和vis的结果进行判断。

最终版本如下所示

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int tar = 0;
        bitset<10001> vis{1};

        for (auto &num : nums)
        {
            tar += num;
            vis |= vis << num;
        }
        
        return !(tar & 1) && vis[tar >> 1];
    }
};

本文发布于2024年3月26日

最后编辑于2024年3月26日

标签:return,tar,nums,sum,vis,算法,num,子集,bitset
From: https://www.cnblogs.com/ThousandPine/p/18095402

相关文章

  • Manacher 算法
    开个坑,后续补解析classSolution{public:intcountSubstrings(strings){intn=s.size();stringt="$#";for(constchar&c:s){t+=c;t+='#';}n=t.size();......
  • 嵌入式算法开发系列之pid算法
    文章目录概要一、PID算法原理二、PID算法模型三、PID算法实现方法四、PID算法C语言实现示例五、PID算法在嵌入式系统中的应用小结概要PID(Proportional-Integral-Derivative)控制算法是一种常用的闭环控制算法,广泛应用于嵌入式系统中的控制器设计。今天张工将深入探讨......
  • 编辑距离算法
    1.题目给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数  。你可以对一个单词进行如下三种操作:删除一个字符替换一个字符插入一个字符示例:输入:word1="horse",word2="ros"输出:3解释:horse->rorse(将'h'替换为'r')ror......
  • Python数据结构实验 递归算法设计
    一、实验目的1.掌握递归程序设计的基本原理和方法;2.熟悉数据结构中顺序表和单链表下的递归算法设计思想;3.掌握并灵活运用递归算法解决一些较复杂的应用问题。二、实验环境1.Windows操作系统的计算机2.Python3.7环境平台和PyCharm编辑器三、实验说明 1.实现递归算法的程序......
  • 算法训练day50卡玛70. 爬楼梯(进阶版)Leetcode322. 零钱兑换279. 完全平方数
    57.爬楼梯(第八期模拟笔试)题目分析我们可以使用动态规划。动态规划的思想是用一个数组dp来保存到达每一级台阶的方法数。对于每一级台阶i,你可以从i-1,i-2,...,i-m级台阶爬上来(只要这些台阶的索引大于等于0),因此到达第i级台阶的方法数就是这些dp[j](i-m<=j<i)的总和。acm......
  • 最小生成树:Kruskal算法和Prim算法
    首先区别一下图跟树:树不会包含环,图可以包含环。图的生成树其实就是在图中找一棵包含图中的所有节点的树。专业点说,生成树是含有图中所有顶点的无环连通子图。最小生成树就是再所有可能的生成树中,权重和最小的那棵生成树就叫最小生成树(注意:最小生成树有n-1条边)。Kruskal算法......
  • 蓝桥杯算法基础(29)字符串匹配(RabinKarp)(KMP)(前缀树,字典树,trie,后缀数组,高度数组)
     RabinKarpRabinKarpS:ABABABm个P:ABBn个1.朴素算法,挨个匹配2.哈希法hash->滚动哈希c0*31^2+c1*31^1+c2类似于进制的求法求hash值(c0*31+c1)*31+c2hash(p)=o(n)hash(s)=o(m*n)privatestaticvoidmatch(Stringp,Strings){longhash_p=hash(p);......
  • 加密算法概述:分类与常见算法
    码到三十五:个人主页心中有诗画,指尖舞代码,目光览世界,步履越千山,人间尽值得!在信息安全领域,加密技术是保护数据不被未授权访问的关键手段。Java作为一种广泛使用的编程语言,提供了丰富的加密API,支持多种加密算法。本文将介绍Java中加密算法的分类以及常见的......
  • 蓝桥杯算法集训 - Week 4:BFS、并查集、Flood Fill、哈希、单调栈/队列
    蓝桥杯算法集训-Week4本系列随笔用于整理AcWing题单——《蓝桥杯集训·每日一题2024》的系列题型及其对应的算法模板。一、BFSBFS算法复习参考:BFS(Java)广度优先搜索简单介绍、模板、案例(一)Ⅰ、代码模板staticvoidbfs(Troot){//双端队列,用来存储元素D......
  • 6种负载均衡算法
    6种负载均衡算法1、轮询法此算法将请求按顺序轮流的分配到后端服务器,他均衡的对待后台每一台服务器,而不关心服务器实际的连接数和当前的系统负载publicclassRoundRobin{privatestaticMap<String,Integer>serverWeightMap=newHashMap<String,Integer>();......