LeetCode 2172. 数组的最大与和【状压DP,记忆化搜索;最小费用最大流】2392

2023-10-22 07:15

本文主要是介绍LeetCode 2172. 数组的最大与和【状压DP,记忆化搜索;最小费用最大流】2392,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

给你一个长度为 n 的整数数组 nums 和一个整数 numSlots ,满足2 * numSlots >= n 。总共有 numSlots 个篮子,编号为 1 到 numSlots 。

你需要把所有 n 个整数分到这些篮子中,且每个篮子 至多 有 2 个整数。一种分配方案的 与和 定义为每个数与它所在篮子编号的 按位与运算 结果之和。

  • 比方说,将数字 [1, 3] 放入篮子 1 中,[4, 6] 放入篮子 2 中,这个方案的与和为 (1 AND 1) + (3 AND 1) + (4 AND 2) + (6 AND 2) = 1 + 1 + 0 + 2 = 4 。

请你返回将 nums 中所有数放入 numSlots 个篮子中的最大与和。

示例 1:

输入:nums = [1,2,3,4,5,6], numSlots = 3
输出:9
解释:一个可行的方案是 [1, 4] 放入篮子 1 中,[2, 6] 放入篮子 2 中,[3, 5] 放入篮子 3 中。
最大与和为 (1 AND 1) + (4 AND 1) + (2 AND 2) + (6 AND 2) + (3 AND 3) + (5 AND 3) = 1 + 0 + 2 + 2 + 3 + 1 = 9

示例 2:

输入:nums = [1,3,10,4,7,1], numSlots = 9
输出:24
解释:一个可行的方案是 [1, 1] 放入篮子 1 中,[3] 放入篮子 3 中,[4] 放入篮子 4 中,[7] 放入篮子 7 中,[10] 放入篮子 9 中。
最大与和为 (1 AND 1) + (1 AND 1) + (3 AND 3) + (4 AND 4) + (7 AND 7) + (10 AND 9) = 1 + 1 + 3 + 4 + 7 + 8 = 24 。
注意,篮子 2568 是空的,这是允许的。

提示:

  • n == nums.length
  • 1 <= numSlots <= 9
  • 1 <= n <= 2 * numSlots
  • 1 <= nums[i] <= 15

解法 记忆化搜索/状压DP

数据范围很小,考虑状压DP。

本题与1879. 两个数组最小的异或值之和【记忆化搜索,状压DP,位运算】2145,有些相似但也不同。通过对题目条件的转换,可以缩小二者之间的差别。

由于每个篮子至多可以放 2 2 2 个整数,可以视为 2 n u m S l o t s 2numSlots 2numSlots 个篮子(组成一个数组,该数组中每个元素的值是篮子的原始编号),每个篮子至多可以放 1 1 1 个整数(放了整数,才计算该整数与篮子编号的与值)

由于篮子个数很少,可以用二进制数 x x x 表示这 2 n u m S l o t s 2 numSlots 2numSlots 个篮子中放了数字的篮子集合,其中 x x x 从低到高的第 i i i 位为 1 1 1 ,表示第 i i i 个篮子放了数字、为 0 0 0 表示没有放数字。

m e m o [ i ] [ j ] memo[i][j] memo[i][j] 表示对前 0 ∼ i 0\sim i 0i 个数, j j j 代表的篮子可以被选中时的最大与和。记忆化搜索的代码如下:

class Solution {
public:int maximumANDSum(vector<int>& nums, int numSlots) {// memo[i][j] 表示对前0-i个数,j代表的篮子可以被选中时的最大与和int n = nums.size();vector<vector<int>> memo(n, vector<int>(1 << (numSlots * 2), -1));function<int(int, int)> f = [&](int i, int j) -> int {if (i < 0) return 0;int &ans = memo[i][j];if (ans != -1) return ans;for (int k = 0; k < (numSlots * 2); ++k) {if (j >> k & 1) { // 这个篮子已经被选中ans = max(ans, f(i - 1, j ^ (1 << k)) + (nums[i] & (k / 2 + 1)));}}return ans;};int ans = f(n - 1, (1 << (numSlots * 2)) - 1);return ans;}};

设数字 i i i 的二进制表示中 1 1 1 的个数为 c c c ,定义 f [ i ] f[i] f[i] 表示 n u m s nums nums 的前 c c c 个数字放入篮子数组中的篮子里,且放了数字的篮子集合为 i i i 时的最大与和。初始值 f [ 0 ] = 0 f[0] = 0 f[0]=0

考虑 n u m s [ c ] nums[c] nums[c] 放到一个空篮子中的状态转移方程(下标从 0 0 0 开始,此时 n u m s [ c ] nums[c] nums[c] 还没被放入篮子中),我们可以枚举 i i i 中的 0 0 0 ,即空篮子位置 j j j ,该篮子对应的编号为 j 2 + 1 \dfrac{j}{2} +1 2j+1 ,则有:
f [ i + 2 j ] = max ⁡ ( f [ i + 2 j ] , f [ i ] + ( j 2 + 1 ) & n u m s [ c ] ) f[i + 2^j] = \max( f[i + 2^j],\ f[i] + (\dfrac{j}{2} + 1)\ \&\ nums[c]) f[i+2j]=max(f[i+2j], f[i]+(2j+1) & nums[c])
nums \textit{nums} nums 的长度为 n n n ,最后答案为 max ⁡ c = n ( f ) \max_{c=n}(f) maxc=n(f)

代码实现时需要注意,若 c ≥ n c\ge n cn f [ i ] f[i] f[i] 无法转移,需要跳过。

class Solution {
public:int maximumANDSum(vector<int> &nums, int numSlots) {int ans = 0;vector<int> f(1 << (numSlots * 2));for (int i = 0; i < f.size(); ++i) {int c = __builtin_popcount(i);if (c >= nums.size()) continue;for (int j = 0; j < numSlots * 2; ++j) {if ((i & (1 << j)) == 0) { // 枚举空篮子 jint s = i | (1 << j);f[s] = max(f[s], f[i] + ((j / 2 + 1) & nums[c]));ans = max(ans, f[s]);}}}return ans;}
};

解法2 最小费用最大流

此外还可用最小费用最大流解决,这其实是一种比较典型的建图技巧。

设集合 A A A 为数字,集合 B B B 为篮子,额外建立超级源点和超级汇点:

  • 从源点连容量为 1 1 1 费用为 0 0 0 的边到 A A A 中各点;
  • B B B 中各点连容量为 2 2 2 费用为 0 0 0 的边到汇点;
  • A A A 的每个数字 nums [ i ] \textit{nums}[i] nums[i] B B B 的每个篮子 j j j 连边,容量为 + ∞ +\infty + ,费用为 − nums [ i ] & j -\textit{nums}[i]\& j nums[i]&j ,取负号是为了求最小费用最大流。

这样跑最小费用最大流得到的结果的相反数就是匹配 A A A 中所有数字的最大花费,即最大与和。时间复杂度 O ( n m ( n + m ) ) O(nm(n+m)) O(nm(n+m)) 。Go 的实现:

func maximumANDSum(nums []int, numSlots int) (ans int) {const inf int = 1e9// 集合 A 和 B 的大小n, m := len(nums), numSlots// 建图type neighbor struct{ to, rid, cap, cost int } // 相邻节点、反向边下标、容量、费用g := make([][]neighbor, n+m+2)addEdge := func(from, to, cap, cost int) {g[from] = append(g[from], neighbor{to, len(g[to]), cap, cost})g[to] = append(g[to], neighbor{from, len(g[from]) - 1, 0, -cost})}start := n + m   // 超级源点end := start + 1 // 超级汇点for i, num := range nums {addEdge(start, i, 1, 0)for j := 1; j <= m; j++ {addEdge(i, n+j-1, inf, -(num & j))}}for i := 0; i < m; i++ {addEdge(n+i, end, 2, 0)}// 下面为最小费用最大流模板dist := make([]int, len(g))type vi struct{ v, i int }fa := make([]vi, len(g))spfa := func() bool {for i := range dist {dist[i] = inf}dist[start] = 0inQ := make([]bool, len(g))inQ[start] = trueq := []int{start}for len(q) > 0 {v := q[0]q = q[1:]inQ[v] = falsefor i, e := range g[v] {if e.cap == 0 {continue}w := e.toif newD := dist[v] + e.cost; newD < dist[w] {dist[w] = newDfa[w] = vi{v, i}if !inQ[w] {q = append(q, w)inQ[w] = true}}}}return dist[end] < inf}for spfa() {// 沿 start-end 的最短路尽量增广minFlow := inffor v := end; v != start; {p := fa[v]if c := g[p.v][p.i].cap; c < minFlow {minFlow = c}v = p.v}for v := end; v != start; {p := fa[v]e := &g[p.v][p.i]e.cap -= minFlowg[v][e.rid].cap += minFlowv = p.v}ans -= dist[end] * minFlow}return
}

解法3 3进制状态压缩

……

这篇关于LeetCode 2172. 数组的最大与和【状压DP,记忆化搜索;最小费用最大流】2392的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/259925

相关文章

哈希leetcode-1

目录 1前言 2.例题  2.1两数之和 2.2判断是否互为字符重排 2.3存在重复元素1 2.4存在重复元素2 2.5字母异位词分组 1前言 哈希表主要是适合于快速查找某个元素(O(1)) 当我们要频繁的查找某个元素,第一哈希表O(1),第二,二分O(log n) 一般可以分为语言自带的容器哈希和用数组模拟的简易哈希。 最简单的比如数组模拟字符存储,只要开26个c

认识、理解、分类——acm之搜索

普通搜索方法有两种:1、广度优先搜索;2、深度优先搜索; 更多搜索方法: 3、双向广度优先搜索; 4、启发式搜索(包括A*算法等); 搜索通常会用到的知识点:状态压缩(位压缩,利用hash思想压缩)。

hdu1240、hdu1253(三维搜索题)

1、从后往前输入,(x,y,z); 2、从下往上输入,(y , z, x); 3、从左往右输入,(z,x,y); hdu1240代码如下: #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#inc

hdu2241(二分+合并数组)

题意:判断是否存在a+b+c = x,a,b,c分别属于集合A,B,C 如果用暴力会超时,所以这里用到了数组合并,将b,c数组合并成d,d数组存的是b,c数组元素的和,然后对d数组进行二分就可以了 代码如下(附注释): #include<iostream>#include<algorithm>#include<cstring>#include<stack>#include<que

hdu4826(三维DP)

这是一个百度之星的资格赛第四题 题目链接:http://acm.hdu.edu.cn/contests/contest_showproblem.php?pid=1004&cid=500 题意:从左上角的点到右上角的点,每个点只能走一遍,走的方向有三个:向上,向下,向右,求最大值。 咋一看像搜索题,先暴搜,TLE,然后剪枝,还是TLE.然后我就改方法,用DP来做,这题和普通dp相比,多个个向上

hdu1011(背包树形DP)

没有完全理解这题, m个人,攻打一个map,map的入口是1,在攻打某个结点之前要先攻打其他一个结点 dp[i][j]表示m个人攻打以第i个结点为根节点的子树得到的最优解 状态转移dp[i][ j ] = max(dp[i][j], dp[i][k]+dp[t][j-k]),其中t是i结点的子节点 代码如下: #include<iostream>#include<algorithm

hdu4865(概率DP)

题意:已知前一天和今天的天气概率,某天的天气概率和叶子的潮湿程度的概率,n天叶子的湿度,求n天最有可能的天气情况。 思路:概率DP,dp[i][j]表示第i天天气为j的概率,状态转移如下:dp[i][j] = max(dp[i][j, dp[i-1][k]*table2[k][j]*table1[j][col] )  代码如下: #include <stdio.h>#include

usaco 1.1 Broken Necklace(DP)

直接上代码 接触的第一道dp ps.大概的思路就是 先从左往右用一个数组在每个点记下蓝或黑的个数 再从右到左算一遍 最后取出最大的即可 核心语句在于: 如果 str[i] = 'r'  ,   rl[i]=rl[i-1]+1, bl[i]=0 如果 str[i] = 'b' ,  bl[i]=bl[i-1]+1, rl[i]=0 如果 str[i] = 'w',  bl[i]=b

poj 1258 Agri-Net(最小生成树模板代码)

感觉用这题来当模板更适合。 题意就是给你邻接矩阵求最小生成树啦。~ prim代码:效率很高。172k...0ms。 #include<stdio.h>#include<algorithm>using namespace std;const int MaxN = 101;const int INF = 0x3f3f3f3f;int g[MaxN][MaxN];int n

poj 1287 Networking(prim or kruscal最小生成树)

题意给你点与点间距离,求最小生成树。 注意点是,两点之间可能有不同的路,输入的时候选择最小的,和之前有道最短路WA的题目类似。 prim代码: #include<stdio.h>const int MaxN = 51;const int INF = 0x3f3f3f3f;int g[MaxN][MaxN];int P;int prim(){bool vis[MaxN];