核心思想

状压dp 设计一个整型可变参数status,利用status的位信息,来表示: 某个样本是否还能使用,然后利用这个信息进行尝试 写出尝试的递归函数 -> 记忆化搜索 -> 严格位置依赖的动态规划 -> 空间压缩等优化

如果有k个样本,那么表示这些样本的状态,数量是2^k 所以可变参数status的范围: 0 ~ (2^k)-1

样本每增加一个,状态的数量是指数级增长的,所以状压dp能解决的问题往往样本数据量都不大 一般样本数量在20个以内 (10^6),如果超过这个数量,计算量 (指令条数) 会超过 10^7 ~ 10^8 讲解043 - 根据数据量猜解法的技巧,天字第一号重要技巧

如果样本数量大到状压dp解决不了,或者任何动态规划都不可行,那么双向广搜是一个备选思路

上一节讲述了状压dp的原理和一些经典题目

本节继续状压dp问题,以及重要技巧:如何在位状态上,枚举所有子集的状态(题目3)

例题

1、每个人戴不同帽子的方案数

题目描述: 总共有 n 个人和 40 种不同的帽子,帽子编号从 1 到 40 。

给你一个整数列表的列表 hats ,其中 hats[i] 是第 i 个人所有喜欢帽子的列表。

请你给每个人安排一顶他喜欢的帽子,确保每个人戴的帽子跟别人都不一样,并返回方案数。

由于答案可能很大,请返回它对 10^9 + 7 取余后的结果。

示例 1:

输入:hats = [[3,4],[4,5],[5]] 输出:1 解释:给定条件下只有一种方法选择帽子。 第一个人选择帽子 3,第二个人选择帽子 4,最后一个人选择帽子 5。

示例 2:

输入:hats = [[3,5,1],[3,5]] 输出:4 解释:总共有 4 种安排帽子的方法: (3,5),(5,3),(1,3) 和 (1,5)

示例 3:

输入:hats = [[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4]] 输出:24 解释:每个人都可以从编号为 1 到 4 的帽子中选。 (1,2,3,4) 4 个帽子的排列方案数为 24 。

提示:

  • n == hats.length

  • 1 <= n <= 10

  • 1 <= hats[i].length <= 40

  • 1 <= hats[i][j] <= 40

  • hats[i] 包含一个数字互不相同的整数列表。

解题思路:

  • 状压dp,仔细观察这个题,人的数量相比帽子数量更低,且帽子数量大于20,因此更适合以人的状态来做状压dp

  • 这个题的尝试与以往的01背包很类似,对于一个帽子使用或者不使用,使用时给那个人使用

  • 这个题有一段值得记住的代码,求解位运算的最右位的1--> cur=num & (-num), 原数去除最右位的1 num^cur

  • 本题我在第一次写的时候出错

    • 一是使用的是一维dp,忽略了帽子数与人的状态是不相互依赖的关系,应该用二维dp

    • 而是,在每一次对于每顶帽子可以给那些人使用的时候,我直接拿的帽子与人的转换数组进行操作,导致进入后续的递归中这个数组很多元素为0,应该用一个局部变量进行尝试的

项目名称

内容填写(每道题一行填写)

📘 题目编号 / 标题

LeetCode 1434 + 每个人戴不同帽子的方案数

🧠 题目关键词(原文关键词)

n 个人和 40 种不同的帽子,给每个人安排一顶他喜欢的帽子,方案数

🧩 抽象问题类型(题目本质)

方案数的排列模型

🔍 数据规模 / 限制

- n == hats.length - 1 <= n <= 10 - 1 <= hats[i].length <= 40 - 1 <= hats[i][j] <= 40 - hats[i] 包含一个数字互不相同的整数列表。

🧭 我的初步思路

状压dp

✅ 正确解法类型

状压dp

❗ 没想到的原因

识别出来了

📦 归入的题型分类

状压dp

🧠 触发词(以后遇到就联想)

方案数,遍历的数量小于20->状压dp

🧪 解法一句话总结

用位运算来替代每个人是否戴帽子的状态

代码如下:

 package main.java.class081;  
   
 import java.util.ArrayList;  
 import java.util.Arrays;  
 import java.util.List;  
   
 /**  
  * @program: ZuoChengxunAlgorithmClass  
  * @ClassName Code01_NumberOfWaysWearDifferentHats  
  * @description: 每个人戴不同帽子的方案数  
  * // 总共有 n 个人和 40 种不同的帽子,帽子编号从 1 到 40  
  * // 给你一个整数列表的列表 hats ,其中 hats[i] 是第 i 个人所有喜欢帽子的列表  
  * // 请你给每个人安排一顶他喜欢的帽子,确保每个人戴的帽子跟别人都不一样,并返回方案数  
  * // 由于答案可能很大,答案对 1000000007 取模  
  * // 测试链接 : https://leetcode.cn/problems/number-of-ways-to-wear-different-hats-to-each-other  
  * @author: zs宝  
  * @create: 2025-09-30 09:08  
  * @Version 1.0  
  **/public class Code01_NumberOfWaysWearDifferentHats {  
     class Solution {  
         public static int MOD=1000000007;  
         public static int numberWays(List<List<Integer>> hats) {  
             return numberWays1(hats);  
         }  
   
         public static int numberWays1(List<List<Integer>> arr) {  
             int n=arr.size();  
             int m=0;  
             //开始计算帽子的最大序号  
             for(List<Integer>hats:arr){  
                 for(int hat:hats){  
                     m=Math.max(m,hat);  
                 }  
             }  
             //计算每顶帽子有哪些人可带,下标为帽子  
             int []hats=new int[m+1];  
             for(int i=0;i<n;i++){  
                 for(int hat:arr.get(i)){  
                     hats[hat]|=(1<<i);  
                 }  
             }  
             int[][]dp=new int[m+1][1<<n];  
             for(int i=0;i<=m;i++){  
                 Arrays.fill(dp[i],-1);  
             }  
             return f1(m,n,1,0,dp,hats);  
         }  
   
         //i:第几顶帽子  
         //status:当前人戴帽子的状况,0未戴帽子,1戴过帽子  
         //hats_people:每顶帽子对应那些人  
         public static int f1(int m,int n,int i,int status,int[][]dp,int[]hats_people){  
             if(status==(1<<n)-1){  
                 return 1;  
             }  
             //还有人没满足,但是帽子已经用光  
             if(i==m+1){  
                 return 0;  
             }  
             if(dp[i][status]!=-1){  
                 return dp[i][status];  
             }  
             //不用第i顶帽子  
             int ans=f1(m,n,i+1,status,dp,hats_people);  
             //接下来考虑用第i顶帽子的情况  
             int cur;  
             int hats=hats_people[i];  
             while(hats!=0){  
                 //得到最右的1:拿到如 00001000                cur=hats &(-hats);  
                 if((status & cur)==0){  
                     ans=(ans+f1(m,n,i+1,status|cur,dp,hats_people))%MOD;  
                 }  
                 //去掉最右侧的1  
                 hats^=cur;  
             }  
             dp[i][status]=ans;  
             return ans;  
         }  
     }  
 }

2、好子集的数目

题目描述: 给你一个整数数组 nums 。如果 nums 的一个子集中,所有元素的乘积可以表示为一个或多个 互不相同的质数 的乘积,那么我们称它为 好子集 。

  • 比方说,如果 nums = [1, 2, 3, 4] :

    • [2, 3] ,[1, 2, 3] 和 [1, 3] 是  子集,乘积分别为 6 = 2*3 ,6 = 2*3 和 3 = 3 。

    • [1, 4] 和 [4] 不是  子集,因为乘积分别为 4 = 2*2 和 4 = 2*2 。

请你返回 nums 中不同的  子集的数目对 109 + 7 取余 的结果。

nums 中的 子集 是通过删除 nums 中一些(可能一个都不删除,也可能全部都删除)元素后剩余元素组成的数组。如果两个子集删除的下标不同,那么它们被视为不同的子集。

示例 1:

输入:nums = [1,2,3,4] 输出:6 解释:好子集为:

  • [1,2]:乘积为 2 ,可以表示为质数 2 的乘积。

  • [1,2,3]:乘积为 6 ,可以表示为互不相同的质数 2 和 3 的乘积。

  • [1,3]:乘积为 3 ,可以表示为质数 3 的乘积。

  • [2]:乘积为 2 ,可以表示为质数 2 的乘积。

  • [2,3]:乘积为 6 ,可以表示为互不相同的质数 2 和 3 的乘积。

  • [3]:乘积为 3 ,可以表示为质数 3 的乘积。

示例 2:

输入:nums = [4,2,3,15] 输出:5 解释:好子集为:

  • [2]:乘积为 2 ,可以表示为质数 2 的乘积。

  • [2,3]:乘积为 6 ,可以表示为互不相同质数 2 和 3 的乘积。

  • [2,15]:乘积为 30 ,可以表示为互不相同质数 2,3 和 5 的乘积。

  • [3]:乘积为 3 ,可以表示为质数 3 的乘积。

  • [15]:乘积为 15 ,可以表示为互不相同质数 3 和 5 的乘积。

提示:

  • 1 <= nums.length <= 105

  • 1 <= nums[i] <= 30

解题思路:

  • 换个想法,既然要求子数组累乘积可以质因数分解,且每个质因数只能出现一次

  • 而题目给的的数据范围子啊1-30以内,其中的质因数只有10个,

  • 那么要质因数分解,每个质因数只出现那一次,那么30以内的质因数只出现一次的组合状况有多少种是可以算出来的,1<<10

  • 因此这里求符合条件的子数组个数,可以转为求解每种质因数只出现一次的组合状况的数组个数

  • 每种质因数只出现一次的组合status的数组如何判定呢?

    • 数组也只能含有质因数,且其中的质因数要包含在status中,即status中的位运算1包含质因数的位运算1

项目名称

内容填写(每道题一行填写)

📘 题目编号 / 标题

LeetCode 1994 + 好子集数目

🧠 题目关键词(原文关键词)

整数数组 nums 。如果 nums 的一个子集中,所有元素的乘积可以表示为一个或多个 互不相同的质数 的乘积,那么我们称它为 好子集

🧩 抽象问题类型(题目本质)

子集模型

🔍 数据规模 / 限制

- 1 <= nums.length <= 105 - 1 <= nums[i] <= 30

🧭 我的初步思路

无思路

✅ 正确解法类型

状压dp

❗ 没想到的原因

不知道怎么转化

📦 归入的题型分类

状压dp

🧠 触发词(以后遇到就联想)

好子集,质因数->状压dp

🧪 解法一句话总结

转为求解质因数只出现一次的组合状况的子数组个数

代码如下:

 package main.java.class081;  
   
 /**  
  * @program: ZuoChengxunAlgorithmClass  
  * @ClassName Code03_TheNumberOfGoodSubsets  
  * @description: 好子集的数目  
  * // 给你一个整数数组 nums,好子集的定义如下:  
  * // nums的某个子集,所有元素的乘积可以表示为一个或多个互不相同质数的乘积  
  * // 比如nums = [1, 2, 3, 4]  
  * // [2, 3],[1, 2, 3],[1, 3] 是好子集  
  * // 乘积分别为6=2*3,6=2*3,3=3  
  * // [1, 4]和[4]不是好子集,因为乘积分别为4=2*2和4=2*2  
  * // 返回nums中不同的好子集的数目,答案对 1000000007 取模  
  * // 如果两个子集删除的下标不同,那么它们被视为不同的子集  
  * // 测试链接 : https://leetcode.cn/problems/the-number-of-good-subsets/  
  * @author: zs宝  
  * @create: 2025-09-30 10:17  
  * @Version 1.0  
  **/public class Code03_TheNumberOfGoodSubsets {  
     class Solution {  
         //由于数字的范围是1-30,质数因子只有10个,那么纯粹有质数因子组成的子数组和的状态有Limit个  
         public static int Limit=(1<<10);  
         public static int MAXV=30;  
         public static int MOD=1000000007;  
         // 打个表来加速判断  
         // 如果一个数字拥有某一种质数因子不只1个  
         // 那么认为这个数字无效,状态全是0,0b0000000000  
         // 如果一个数字拥有任何一种质数因子都不超过1个  
         // 那么认为这个数字有效,用位信息表示这个数字拥有质数因子的状态  
         // 比如12,拥有2这种质数因子不只1个,所以无效,用0b0000000000表示  
         // 比如14,拥有2这种质数因子不超过1个,拥有7这种质数因子不超过1个,有效  
         // 从高位到低位依次表示:...13 11 7 5 3 2  
         // 所以用0b0000001001表示14拥有质数因子的状态  
         // 质数: 29 23 19 17 13 11 7 5 3 2  
         // 位置: 9 8 7 6 5 4 3 2 1 0  
         public static int[] own = { 0b0000000000, // 0  
                 0b0000000000, // 1  
                 0b0000000001, // 2  
                 0b0000000010, // 3  
                 0b0000000000, // 4  
                 0b0000000100, // 5  
                 0b0000000011, // 6  
                 0b0000001000, // 7  
                 0b0000000000, // 8  
                 0b0000000000, // 9  
                 0b0000000101, // 10  
                 0b0000010000, // 11  
                 0b0000000000, // 12  
                 0b0000100000, // 13  
                 0b0000001001, // 14  
                 0b0000000110, // 15  
                 0b0000000000, // 16  
                 0b0001000000, // 17  
                 0b0000000000, // 18  
                 0b0010000000, // 19  
                 0b0000000000, // 20  
                 0b0000001010, // 21  
                 0b0000010001, // 22  
                 0b0100000000, // 23  
                 0b0000000000, // 24  
                 0b0000000000, // 25  
                 0b0000100001, // 26  
                 0b0000000000, // 27  
                 0b0000000000, // 28  
                 0b1000000000, // 29  
                 0b0000000111 // 30  
         };  
         public int numberOfGoodSubsets(int[] nums) {  
             //1-30的数字的个数  
             int[]cnt=new int[MAXV+1];  
             for(int num:nums){  
                 cnt[num]++;  
             }  
             int [][]dp=new int[MAXV+1][Limit];  
             for(int i=0;i<=MAXV;i++){  
                 Arrays.fill(dp[i],-1);  
             }  
             //这里我们转化一下  
             //原题目是想要求nums 中不同的 好 子集的数目  
             //我们根据好子集的定义,将其转化为  
             //求解所有质数状态组合的集合数量  
             int ans=0;  
             for(int s=1;s<Limit;s++){  
                 ans=(ans+f(MAXV,s,cnt,dp))%MOD;  
             }  
             return ans;  
         }  
   
         /**  
          在1-i的数字范围内要求能够达到status状态的数组有哪些  
          就是1-i数字范围组成的数组的乘积,这个乘积的质数分解状态为status中的质数  
          */  
         public static int f(int i,int status,int[]cnt,int[][]dp){  
             if(dp[i][status]!=-1){  
                 return dp[i][status];  
             }  
             int ans=0;  
             //所有数字已经用完  
             if(i==1){  
                 if(status==0){  
                     ans=1;  
                     for(int j=0;j<cnt[1];j++){  
                         ans=(ans<<1)%MOD;  
                     }  
                 }  
             }else{  
                 //数字还未用完  
                 //不用当前数字  
                 ans=f(i-1,status,cnt,dp);  
                 //用当前数字  
                 int cur=own[i];  
                 int times=cnt[i];  
                 //判断当前数字是否可用  
                 //现实cur是质数且给的数组中有这个数,并且这个数有status中要的质数状态  
                 if(cur!=0 && times!=0 && (status & cur)==cur){  
                     ans=(int)((ans+(long)f(i-1,status^cur,cnt,dp)*times)%MOD);  
                 }  
             }  
             dp[i][status]=ans;  
             return ans;  
         }  
     }  
 }

3、分配重复整数

题目描述: 给你一个长度为 n 的整数数组 nums ,这个数组中至多有 50 个不同的值。同时你有 m 个顾客的订单 quantity ,其中,整数 quantity[i] 是第 i 位顾客订单的数目。请你判断是否能将 nums 中的整数分配给这些顾客,且满足:

  • 第 i 位顾客 恰好 有 quantity[i] 个整数。

  • 第 i 位顾客拿到的整数都是 相同的 。

  • 每位顾客都满足上述两个要求。

如果你可以分配 nums 中的整数满足上面的要求,那么请返回 true ,否则返回 false 。

示例 1:

输入:nums = [1,2,3,4], quantity = [2] 输出:false 解释:第 0 位顾客没办法得到两个相同的整数。

示例 2:

输入:nums = [1,2,3,3], quantity = [2] 输出:true 解释:第 0 位顾客得到 [3,3] 。整数 [1,2] 都没有被使用。

示例 3:

输入:nums = [1,1,2,2], quantity = [2,2] 输出:true 解释:第 0 位顾客得到 [1,1] ,第 1 位顾客得到 [2,2] 。

提示:

  • n == nums.length

  • 1 <= n <= 105

  • 1 <= nums[i] <= 1000

  • m == quantity.length

  • 1 <= m <= 10

  • 1 <= quantity[i] <= 105

  • nums 中至多有 50 个不同的数字。

解题思路

  • 这道题容易用贪心的思路去解决,但是贪心的思路就很容易一个数值的个数解决一个人的需求,忽略一种数值的个数可以解决多个人的需求(自己写的最后几个测试用例就错在这里)

  • 那么这里就考虑一个数值解决多种需求的可能

  • 就有一个问题,多个人的需求组合起来要多少个数的相同数值,如下代码

 for(int i=0,v,h;i<m;i++){  
     v=quantity[i];  
     h=1<<i;  
     //所有比h小的状态和h状态组合  
     for(int k=0;k<h;k++){  
         sum[h|k]=sum[k]+v;  
     }  
 }  
  • 而后在后续递归调用的时候,如何判定当前数值的个数可以解决那些人的雪球组合,这里就需要我们依次遍历当前状态的所有子集,如1111100的子集1100100这种,代码如下,需牢牢记住

 for(int j=status;j>0;j=(status & (j-1))){  
     if(sum[j]<=k && f(cnts,sum,i+1,status^j,dp)){  
         ans=true;  
         break;  
     }  
 }

项目名称

内容填写(每道题一行填写)

📘 题目编号 / 标题

LeetCode 1655 + 分配重复整数

🧠 题目关键词(原文关键词)

第 i 位顾客拿到的整数都是 相同的,整数分配给这些顾客

🧩 抽象问题类型(题目本质)

分配问题

🔍 数据规模 / 限制

- n == nums.length - 1 <= n <= 105 - 1 <= nums[i] <= 1000 - m == quantity.length - 1 <= m <= 10 - 1 <= quantity[i] <= 105 - nums 中至多有 50 个不同的数字。

🧭 我的初步思路

状压dp+贪心

✅ 正确解法类型

状压dp

❗ 没想到的原因

忽略了一个数值的个数满足多个人的需求

📦 归入的题型分类

状压dp

🧠 触发词(以后遇到就联想)

分配问题,需求人数少于20->状压dp

🧪 解法一句话总结

每次判定某个数值满足需求是,对当前状态的所有子集都进行尝试

代码如下:

 package main.java.class081;  
   
 import java.util.Arrays;  
   
 /**  
  * @program: ZuoChengxunAlgorithmClass  
  * @ClassName Code04_DistributeRepeatingIntegers  
  * @description: 分配重复整数  
  * // 给你一个长度为n的整数数组nums,这个数组中至多有50个不同的值  
  * // 同时你有m个顾客的订单quantity,其中整数quantity[i]是第i位顾客订单的数目  
  * // 请你判断是否能将nums中的整数分配给这些顾客,且满足:  
  * // 第i位顾客恰好有quantity[i]个整数、第i位顾客拿到的整数都是相同的  
  * // 每位顾客都要满足上述两个要求,返回是否能都满足  
  * // 测试链接 : https://leetcode.cn/problems/distribute-repeating-integers/  
  * @author: zs宝  
  * @create: 2025-09-30 13:44  
  * @Version 1.0  
  **/public class Code04_DistributeRepeatingIntegers {  
     class Solution {  
         public boolean canDistribute(int[] nums, int[] quantity) {  
             Arrays.sort(nums);  
             //统计有多少种数值  
             int n=1;  
             for(int i=1;i<nums.length;i++){  
                 if(nums[i]!=nums[i-1]){  
                     n++;  
                 }  
             }  
             //统计每种数值的个数  
             int[]cnts=new int[n];  
             int indx=0;  
             int j=0;  
             while(j<nums.length){  
                 int cur=nums[j];  
                 int len=0;  
                 while(j<nums.length && nums[j]==cur){  
                     len++;  
                     j++;  
                 }  
                 cnts[indx++]=len;  
             }  
             //枚举生成quanlity中的每个子集需要的数字个数  
             int m=quantity.length;  
             int[]sum=new int[1<<m];  
             for(int i=0,v,h;i<m;i++){  
                 v=quantity[i];  
                 h=1<<i;  
                 //所有比h小的状态和h状态组合  
                 for(int k=0;k<h;k++){  
                     sum[h|k]=sum[k]+v;  
                 }  
             }  
             int[][]dp=new int[n][1<<m];  
             return f(cnts,sum,0,(1<<m)-1,dp);  
         }  
   
         public boolean f(int[]cnts,int[] sum,int i,int status,int[][]dp){  
             if(status==0){  
                 return true;  
             }  
             if(i==cnts.length){  
                 return false;  
             }  
             if(dp[i][status]!=0){  
                 return dp[i][status]==1;  
             }  
             boolean ans=false;  
             //当前位置的数值个数  
             int k=cnts[i];  
             //开始判定当前数值可以解决那些sum需求组合  
             //当前位运算如11111的子集  
             for(int j=status;j>0;j=(status & (j-1))){  
                 if(sum[j]<=k && f(cnts,sum,i+1,status^j,dp)){  
                     ans=true;  
                     break;  
                 }  
             }  
             //当前i位置的数值个数无论如何都解决不了  
             if(!ans){  
                 //则不考虑当前位置数值  
                 ans=f(cnts,sum,i+1,status,dp);  
             }  
             dp[i][status]=ans?1:-1;  
             return ans;  
         }  
     }  
 }

参考资料