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

相关文章

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu

JAVA中整型数组、字符串数组、整型数和字符串 的创建与转换的方法

《JAVA中整型数组、字符串数组、整型数和字符串的创建与转换的方法》本文介绍了Java中字符串、字符数组和整型数组的创建方法,以及它们之间的转换方法,还详细讲解了字符串中的一些常用方法,如index... 目录一、字符串、字符数组和整型数组的创建1、字符串的创建方法1.1 通过引用字符数组来创建字符串1.2

如何提高Redis服务器的最大打开文件数限制

《如何提高Redis服务器的最大打开文件数限制》文章讨论了如何提高Redis服务器的最大打开文件数限制,以支持高并发服务,本文给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录如何提高Redis服务器的最大打开文件数限制问题诊断解决步骤1. 修改系统级别的限制2. 为Redis进程特别设置限制

vue如何监听对象或者数组某个属性的变化详解

《vue如何监听对象或者数组某个属性的变化详解》这篇文章主要给大家介绍了关于vue如何监听对象或者数组某个属性的变化,在Vue.js中可以通过watch监听属性变化并动态修改其他属性的值,watch通... 目录前言用watch监听深度监听使用计算属性watch和计算属性的区别在vue 3中使用watchE

C# ComboBox下拉框实现搜索方式

《C#ComboBox下拉框实现搜索方式》文章介绍了如何在加载窗口时实现一个功能,并在ComboBox下拉框中添加键盘事件以实现搜索功能,由于数据不方便公开,作者表示理解并希望得到大家的指教... 目录C# ComboBox下拉框实现搜索步骤一步骤二步骤三总结C# ComboBox下拉框实现搜索步骤一这

哈希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