class081 状压dp-下【算法】

简介: 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

相关文章
|
算法
【算法】—— 简单多状态 dp 问题
【算法】—— 简单多状态 dp 问题
|
3月前
|
算法 测试技术 C++
【数位dp】【动态规划】C++算法:233.数字 1 的个数
【数位dp】【动态规划】C++算法:233.数字 1 的个数
|
4月前
|
算法 测试技术 C#
【数位dp】【C++算法】600. 不含连续1的非负整数
【数位dp】【C++算法】600. 不含连续1的非负整数
|
5月前
|
人工智能 算法 BI
class079 树型dp-下【算法】
class079 树型dp-下【算法】
31 0
|
5月前
|
人工智能 算法 BI
class077 区间dp-下【算法】
class077 区间dp-下【算法】
33 0
|
5月前
|
算法
class075 背包dp-多重背包、混合背包【算法】
class075 背包dp-多重背包、混合背包【算法】
33 0
|
28天前
|
算法
算法系列--两个数组的dp问题(2)(下)
算法系列--两个数组的dp问题(2)(下)
20 0
|
28天前
|
算法 计算机视觉
算法系列--两个数组的dp问题(1)(下)
算法系列--两个数组的dp问题(1)
19 0
|
28天前
|
算法
算法系列--两个数组的dp问题(1)(上)
算法系列--两个数组的dp问题(1)
14 0
|
3月前
|
监控 算法 测试技术
【动态规划】【树形dp】【C++算法】968监控二叉树
【动态规划】【树形dp】【C++算法】968监控二叉树