class081 状压dp-下【算法】

class081 状压dp-下【算法】

算法讲解081【必备】状压dp-下

在这里插入图片描述

Code1 1434. 每个人戴不同帽子的方案数

// 每个人戴不同帽子的方案数
// 总共有 n 个人和 40 种不同的帽子,帽子编号从 1 到 40
// 给你一个整数列表的列表 hats ,其中 hats[i] 是第 i 个人所有喜欢帽子的列表
// 请你给每个人安排一顶他喜欢的帽子,确保每个人戴的帽子跟别人都不一样,并返回方案数
// 由于答案可能很大,请返回它对10^9+7取余后的结果
// 测试链接 : https://leetcode.cn/problems/number-of-ways-to-wear-different-hats-to-each-other

package class081;

import java.util.Arrays;
import java.util.List;

// 每个人戴不同帽子的方案数
// 总共有 n 个人和 40 种不同的帽子,帽子编号从 1 到 40
// 给你一个整数列表的列表 hats ,其中 hats[i] 是第 i 个人所有喜欢帽子的列表
// 请你给每个人安排一顶他喜欢的帽子,确保每个人戴的帽子跟别人都不一样,并返回方案数
// 由于答案可能很大,请返回它对10^9+7取余后的结果
// 测试链接 : https://leetcode.cn/problems/number-of-ways-to-wear-different-hats-to-each-other
public class Code01_NumberOfWaysWearDifferentHats {
    
    

	public static int MOD = 1000000007;

	public static int numberWays(List<List<Integer>> arr) {
    
    
		// 帽子颜色的最大值
		int m = 0;
		for (List<Integer> person : arr) {
    
    
			for (int hat : person) {
    
    
				m = Math.max(m, hat);
			}
		}
		int n = arr.size();
		// 1 ~ m 帽子,能满足哪些人,状态信息 -> int
		int[] hats = new int[m + 1];
		for (int pi = 0; pi < n; pi++) {
    
    
			for (int hat : arr.get(pi)) {
    
    
				hats[hat] |= 1 << pi;
			}
		}
		int[][] dp = new int[m + 1][1 << n];
		for (int i = 0; i <= m; i++) {
    
    
			Arrays.fill(dp[i], -1);
		}
		return f2(hats, m, n, 1, 0, dp);
	}

	// m : 帽子颜色的最大值, 1 ~ m
	// n : 人的数量,0 ~ n-1
	// i : 来到了什么颜色的帽子
	// s : n个人,谁没满足状态就是0,谁满足了状态就是1
	// dp : 记忆化搜索的表
	// 返回 : 有多少种方法
	public static int f1(int[] hats, int m, int n, int i, int s, int[][] dp) {
    
    
		if (s == (1 << n) - 1) {
    
    
			return 1;
		}
		// 还有人没满足
		if (i == m + 1) {
    
    
			return 0;
		}
		if (dp[i][s] != -1) {
    
    
			return dp[i][s];
		}
		// 可能性1 : i颜色的帽子,不分配给任何人
		int ans = f1(hats, m, n, i + 1, s, dp);
		// 可能性2 : i颜色的帽子,分配给可能的每一个人
		int cur = hats[i];
		// 用for循环从0 ~ n-1枚举每个人
		for (int p = 0; p < n; p++) {
    
    
			if ((cur & (1 << p)) != 0 && (s & (1 << p)) == 0) {
    
    
				ans = (ans + f1(hats, m, n, i + 1, s | (1 << p), dp)) % MOD;
			}
		}
		dp[i][s] = ans;
		return ans;
	}

	public static int f2(int[] hats, int m, int n, int i, int s, int[][] dp) {
    
    
		if (s == (1 << n) - 1) {
    
    
			return 1;
		}
		if (i == m + 1) {
    
    
			return 0;
		}
		if (dp[i][s] != -1) {
    
    
			return dp[i][s];
		}
		int ans = f2(hats, m, n, i + 1, s, dp);
		int cur = hats[i];
		// 不用for循环枚举
		// 从当前帽子中依次提取能满足的人
		// 提取出二进制状态中最右侧的1,讲解030-异或运算,题目5,Brian Kernighan算法
		// cur :
		// 0 0 0 1 1 0 1 0
		// -> 0 0 0 0 0 0 1 0
		// -> 0 0 0 0 1 0 0 0
		// -> 0 0 0 1 0 0 0 0
		int rightOne;
		while (cur != 0) {
    
    
			rightOne = cur & -cur;
			if ((s & rightOne) == 0) {
    
    
				ans = (ans + f2(hats, m, n, i + 1, s | rightOne, dp)) % MOD;
			}
			cur ^= rightOne;
		}
		dp[i][s] = ans;
		return ans;
	}

}

Code2 465.最优账单平衡

// 最优账单平衡
// 给你一个表示交易的数组 transactions
// 其中 transactions[i] = [fromi, toi, amounti]
// 表示 ID = fromi 的人给 ID = toi 的人共计 amounti
// 请你计算并返回还清所有债务的最小交易笔数
// 测试链接 : https://leetcode.cn/problems/optimal-account-balancing/

465 最优账单平衡 困难
给你一个表示交易的数组 transactions ,其中 transactions[i] = [fromi,toi, amounti] 表示 ID =fromi的人给ID = toi的人共计amounti $

请你计算并返回还清所有债务的最小交易笔数

示例 1:

输入: transactions = [[0,1,10],[2,0,5]]
输出:2
解释:
#0给#1 $10
#2给#0 $5
需要进行两笔交易。一种结清债务的方式是 #1 给 #0 和 #2 各$5
package class081;

import java.util.Arrays;

// 最优账单平衡
// 给你一个表示交易的数组 transactions
// 其中 transactions[i] = [fromi, toi, amounti]
// 表示 ID = fromi 的人给 ID = toi 的人共计 amounti
// 请你计算并返回还清所有债务的最小交易笔数
// 测试链接 : https://leetcode.cn/problems/optimal-account-balancing/
public class Code02_OptimalAccountBalancing {
    
    

	// 题目说了人员编号的最大范围:0 ~ 12
	public static int MAXN = 13;

	public static int minTransfers(int[][] transactions) {
    
    
		// 加工出来的debt数组中一定不含有0
		int[] debt = debts(transactions);
		int n = debt.length;
		int[] dp = new int[1 << n];
		Arrays.fill(dp, -1);
		return n - f(debt, (1 << n) - 1, 0, n, dp);
	}

	public static int[] debts(int[][] transactions) {
    
    
		int[] help = new int[MAXN];
		for (int[] tran : transactions) {
    
    
			help[tran[0]] -= tran[2];
			help[tran[1]] += tran[2];
		}
		int n = 0;
		for (int num : help) {
    
    
			if (num != 0) {
    
    
				n++;
			}
		}
		int[] debt = new int[n];
		int index = 0;
		for (int num : help) {
    
    
			if (num != 0) {
    
    
				debt[index++] = num;
			}
		}
		return debt;
	}

	public static int f(int[] debt, int set, int sum, int n, int[] dp) {
    
    
		if (dp[set] != -1) {
    
    
			return dp[set];
		}
		int ans = 0;
		if ((set & (set - 1)) != 0) {
    
     // 集合中不只一个元素
			if (sum == 0) {
    
    
				for (int i = 0; i < n; i++) {
    
    
					if ((set & (1 << i)) != 0) {
    
    
						// 找到任何一个元素,去除这个元素
						// 剩下的集合进行尝试,返回值 + 1
						ans = f(debt, set ^ (1 << i), sum - debt[i], n, dp) + 1;
						// 然后不需要再尝试下一个元素了,因为答案一定是一样的
						// 所以直接break
						break;
					}
				}
			} else {
    
    
				for (int i = 0; i < n; i++) {
    
    
					if ((set & (1 << i)) != 0) {
    
    
						ans = Math.max(ans, f(debt, set ^ (1 << i), sum - debt[i], n, dp));
					}
				}
			}
		}
		dp[set] = ans;
		return ans;
	}

}

Code3 1994. 好子集的数目

// 好子集的数目
// 给你一个整数数组 nums,好子集的定义如下:
// nums的某个子集,所有元素的乘积可以表示为一个或多个互不相同质数的乘积
// 比如nums = [1, 2, 3, 4]
// [2, 3],[1, 2, 3],[1, 3] 是好子集
// 乘积分别为6=23,6=23,3=3
// [1, 4]和[4]不是好子集,因为乘积分别为4=22和4=22
// 请你返回nums中不同的好子集的数目对10^9+7取余的结果
// 如果两个子集删除的下标不同,那么它们被视为不同的子集
// 测试链接 : https://leetcode.cn/problems/the-number-of-good-subsets/

package class081;

import java.util.Arrays;

// 好子集的数目
// 给你一个整数数组 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中不同的好子集的数目对10^9+7取余的结果
// 如果两个子集删除的下标不同,那么它们被视为不同的子集
// 测试链接 : https://leetcode.cn/problems/the-number-of-good-subsets/
public class Code03_TheNumberOfGoodSubsets {
    
    

	public static int MAXV = 30;

	public static int LIMIT = (1 << 10);

	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 static int numberOfGoodSubsets1(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);
		}
		int ans = 0;
		for (int s = 1; s < LIMIT; s++) {
    
    
			ans = (ans + f1(MAXV, s, cnt, dp)) % MOD;
		}
		return ans;
	}

	// 1....i范围的数字,每种数字cnt[i]个
	// 最终相乘的结果一定要让质因子的状态为s,且每种质因子只能有1个
	// 请问子集的数量是多少
	// s每一位代表的质因子如下
	// 质数: 29 23 19 17 13 11 7 5 3 2
	// 位置: 9 8 7 6 5 4 3 2 1 0
	public static int f1(int i, int s, int[] cnt, int[][] dp) {
    
    
		if (dp[i][s] != -1) {
    
    
			return dp[i][s];
		}
		int ans = 0;
		if (i == 1) {
    
    
			if (s == 0) {
    
    
				ans = 1;
				for (int j = 0; j < cnt[1]; j++) {
    
    
					ans = (ans << 1) % MOD;
				}
			}
		} else {
    
    
			ans = f1(i - 1, s, cnt, dp);
			int cur = own[i];
			int times = cnt[i];
			if (cur != 0 && times != 0 && (s & cur) == cur) {
    
    
				// 能要i这个数字
				ans = (int) (((long) f1(i - 1, s ^ cur, cnt, dp) * times + ans) % MOD);
			}
		}
		dp[i][s] = ans;
		return ans;
	}

	// 空间压缩优化
	public static int[] cnt = new int[MAXV + 1];

	public static int[] dp = new int[LIMIT];

	public static int numberOfGoodSubsets2(int[] nums) {
    
    
		Arrays.fill(cnt, 0);
		Arrays.fill(dp, 0);
		for (int num : nums) {
    
    
			cnt[num]++;
		}
		dp[0] = 1;
		for (int i = 0; i < cnt[1]; i++) {
    
    
			dp[0] = (dp[0] << 1) % MOD;
		}
		for (int i = 2, cur, times; i <= MAXV; i++) {
    
    
			cur = own[i];
			times = cnt[i];
			if (cur != 0 && times != 0) {
    
    
				for (int status = LIMIT - 1; status >= 0; status--) {
    
    
					if ((status & cur) == cur) {
    
    
						dp[status] = (int) (((long) dp[status ^ cur] * times + dp[status]) % MOD);
					}
				}
			}
		}
		int ans = 0;
		for (int s = 1; s < LIMIT; s++) {
    
    
			ans = (ans + dp[s]) % MOD;
		}
		return ans;
	}

}

Code4 1655. 分配重复整数

// 分配重复整数
// 给你一个长度为n的整数数组nums,这个数组中至多有50个不同的值
// 同时你有m个顾客的订单quantity,其中整数quantity[i]是第i位顾客订单的数目
// 请你判断是否能将nums中的整数分配给这些顾客,且满足:
// 第i位顾客恰好有quantity[i]个整数、第i位顾客拿到的整数都是相同的
// 每位顾客都要满足上述两个要求,返回是否能都满足
// 测试链接 : https://leetcode.cn/problems/distribute-repeating-integers/

package class081;

import java.util.Arrays;

// 分配重复整数
// 给你一个长度为n的整数数组nums,这个数组中至多有50个不同的值
// 同时你有m个顾客的订单quantity,其中整数quantity[i]是第i位顾客订单的数目
// 请你判断是否能将nums中的整数分配给这些顾客,且满足:
// 第i位顾客恰好有quantity[i]个整数、第i位顾客拿到的整数都是相同的
// 每位顾客都要满足上述两个要求,返回是否能都满足
// 测试链接 : https://leetcode.cn/problems/distribute-repeating-integers/
public class Code04_DistributeRepeatingIntegers {
    
    

	// 时间复杂度O(n * 3的m次方),空间复杂度O(n * 2的m次方)
	// ppt上有时间复杂度解析
	public static boolean canDistribute(int[] nums, int[] quantity) {
    
    
		Arrays.sort(nums);
		int n = 1;
		for (int i = 1; i < nums.length; i++) {
    
    
			if (nums[i - 1] != nums[i]) {
    
    
				n++;
			}
		}
		int[] cnt = new int[n];
		int c = 1;
		for (int i = 1, j = 0; i < nums.length; i++) {
    
    
			if (nums[i - 1] != nums[i]) {
    
    
				cnt[j++] = c;
				c = 1;
			} else {
    
    
				c++;
			}
		}
		cnt[n - 1] = c;
		int m = quantity.length;
		int[] sum = new int[1 << m];
		// 下面这个枚举是生成quantity中的每个子集,所需要数字的个数
		for (int i = 0, v, h; i < quantity.length; i++) {
    
    
			v = quantity[i];
			h = 1 << i;
			for (int j = 0; j < h; j++) {
    
    
				sum[h | j] = sum[j] + v;
			}
		}
		int[][] dp = new int[1 << m][n];
		return f(cnt, sum, (1 << m) - 1, 0, dp);
	}

	// 当前来到的数字,编号index,个数cnt[index]
	// status : 订单状态,1还需要去满足,0已经满足过了
	public static boolean f(int[] cnt, int[] sum, int status, int index, int[][] dp) {
    
    
		if (status == 0) {
    
    
			return true;
		}
		// status != 0
		if (index == cnt.length) {
    
    
			return false;
		}
		if (dp[status][index] != 0) {
    
    
			return dp[status][index] == 1;
		}
		boolean ans = false;
		int k = cnt[index];
		// 这是整个实现最核心的枚举
		// j枚举了status的所有子集状态
		// 建议记住
		for (int j = status; j > 0; j = (j - 1) & status) {
    
    
			if (sum[j] <= k && f(cnt, sum, status ^ j, index + 1, dp)) {
    
    
				ans = true;
				break;
			}
		}
		if (!ans) {
    
    
			ans = f(cnt, sum, status, index + 1, dp);
		}
		dp[status][index] = ans ? 1 : -1;
		return ans;
	}

}

2023-12-05 16:04:48

猜你喜欢

转载自blog.csdn.net/qq_51625007/article/details/134757315