LeetCode 1595. 连通两组点的最小成本【记忆化搜索,状压DP】2537

2023-10-20 06:44

本文主要是介绍LeetCode 1595. 连通两组点的最小成本【记忆化搜索,状压DP】2537,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

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

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

给你两组点,其中第一组中有 size1 个点,第二组中有 size2 个点,且 size1 >= size2

任意两点间的连接成本 cost 由大小为 size1 x size2 矩阵给出,其中 cost[i][j] 是第一组中的点 i 和第二组中的点 j 的连接成本。如果两个组中的每个点都与另一组中的一个或多个点连接,则称这两组点是连通的。 换言之,第一组中的每个点必须至少与第二组中的一个点连接,且第二组中的每个点必须至少与第一组中的一个点连接。

返回连通两组点所需的最小成本。

示例 1:

输入:cost = [[15, 96], [36, 2]]
输出:17
解释:连通两组点的最佳方法是:
1--A
2--B
总成本为 17

示例 2:

输入:cost = [[1, 3, 5], [4, 1, 1], [1, 5, 3]]
输出:4
解释:连通两组点的最佳方法是:
1--A
2--B
2--C
3--A
最小成本为 4 。
请注意,虽然有多个点连接到第一组中的点 2 和第二组中的点 A ,但由于题目并不限制连接点的数目,所以只需要关心最低总成本。

示例 3:

输入:cost = [[2, 5, 1], [3, 4, 7], [8, 1, 2], [6, 2, 4], [3, 8, 8]]
输出:10

提示:

  • size1 == cost.length
  • size2 == cost[i].length
  • 1 <= size1, size2 <= 12
  • size1 >= size2
  • 0 <= cost[i][j] <= 100

相似题目(状压 DP):

    1. 两个数组最小的异或值之和
    1. 数组的最大与和
    1. 最小的必要团队
    1. 公平分发饼干
    1. 并行课程 II
  • LCP 53. 守护太空城
    1. 完成任务的最少工作时间段

以后可以补两个解法,时间复杂度 O ( n 3 ) O(n^3) O(n3) 的二分图匹配,或者网络流 min ⁡ ( O ( n 3 ) , O ( n 2 m log ⁡ ( m ) ) ) \min(O(n ^ 3), O(n^2 m \log(m))) min(O(n3),O(n2mlog(m)))。由于牵涉到数据转换,转换后可能是稀疏图,m可能远小于n。

解法 记忆化搜索

1. 寻找子问题

下文中 n = size 1 n=\textit{size}_1 n=size1 m = size 2 m=\textit{size}_2 m=size2 。假设 n = 5 , m = 3 n=5,m=3 n=5,m=3 。第一组的点编号为 0 , 1 , 2 , 3 , 4 0,1,2,3,4 0,1,2,3,4 ,第二组的点编号为 0 , 1 , 2 0,1,2 0,1,2

用「枚举选哪个」来思考。考虑第一组的最后一个点( 4 4 4),可以枚举它和第二组的 0 , 1 , 2 0,1,2 0,1,2 相连。假设和第二组的 1 1 1 相连,那么问题变成:「第一组的 0 , 1 , 2 , 3 0,1,2,3 0,1,2,3 和第二组的 0 , 1 , 2 0,1,2 0,1,2 相连,且第二组的 0 , 2 0,2 0,2 未被连接时,最小成本是多少」。然后考虑第一组的点 3 3 3 和第二组的哪个相连(可以连之前连过的点),接着考虑第一组的点 2 2 2,点 1 1 1,最后是点 0 0 0

第一组的点全部连完后,第二组的某些点可能未被连接,这些点可以去第一组找个成本最小的点连上

上述做法枚举了第一组的每个点连接第二组的所有情况,并在最后用贪心的思路处理第二组剩余没有连的点,所以一定可以算出(枚举出)最优解。

2. 状态定义与状态转移方程

根据上面的讨论,定义 dfs ( i , j ) \textit{dfs}(i,j) dfs(i,j) 表示第一组的 0 , 1 , ⋯ , i 0,1,\cdots,i 0,1,,i 和第二组的 0 , 1 , ⋯ , m − 1 0,1,\cdots,m-1 0,1,,m1 相连,且第二组的集合 j j j 中元素未被连接时,最小成本是多少。

枚举第一组的点 i i i 和第二组的 0 , 1 , ⋯ , m − 1 0,1,\cdots,m-1 0,1,,m1 其中一个点相连,取最小值,即
dfs ( i , j ) = min ⁡ k = 0 m − 1 dfs ( i − 1 , j ∖ { k } ) + cost [ i ] [ k ] \textit{dfs}(i,j) =\min_{k=0}^{m-1} \textit{dfs}(i-1,j\setminus\{k\}) + \textit{cost}[i][k] dfs(i,j)=k=0minm1dfs(i1,j{k})+cost[i][k]
​其中 j ∖ { k } j\setminus\{k\} j{k} 表示从集合 j j j 中去掉元素 k k k 后的集合。注意,如果 k k k 不在 j j j 中,那么 j j j 不变。

递归边界:第一组点已经连接完了,我们设集合 j j j 中第二组的点 x x x 与第一组的点连接时,最小成本是 minCost [ x ] \textit{minCost}[x] minCost[x] ,那么有
dfs ( − 1 , j ) = ∑ k ∈ j minCost [ k ] \textit{dfs}(-1,j) = \sum_{k\in j} \textit{minCost}[k] dfs(1,j)=kjminCost[k]
递归入口: dfs ( n − 1 , { 0 , 1 , ⋯ , m − 1 } ) \textit{dfs}(n-1,\{0,1,\cdots,m-1\}) dfs(n1,{0,1,,m1})

代码实现时, m i n C o s t minCost minCost 数组可以提前预处理出来。

3. 记忆化搜索

下文用 x − y x-y xy 表示第一组的点 x x x 连第二组的点 y y y

假设 n = 5 , m = 3 n=5,m=3 n=5,m=3 。由于无论是先 4 − 0 4-0 40 3 − 1 3-1 31 ,还是先 4 − 1 4-1 41 3 − 0 3-0 30 ,都会递归到 dfs ( 2 , { 2 } ) \textit{dfs}(2,\{2\}) dfs(2,{2}) 这个状态上。一叶知秋,整个递归中有大量重复递归调用(递归入参相同)。由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:

  • 如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个 memo \textit{memo} memo 数组(或哈希表)中。
  • 如果一个状态不是第一次遇到,那么直接返回 memo \textit{memo} memo 中保存的结果。

问:能不能枚举第二组的点,去连接第一组的点?
答:也可以,但这样做的时间复杂度是 O ( n m 2 n ) \mathcal{O}(nm2^n) O(nm2n) ,相比 O ( n m 2 m ) \mathcal{O}(nm2^m) O(nm2m) 更慢。注意本题 n ≥ m n\ge m nm

class Solution {
public:int connectTwoGroups(vector<vector<int>>& cost) {int n = cost.size(), m = cost[0].size();vector<int> minCost(m, INT_MAX);for (int j = 0; j < m; ++j)for (auto &c : cost)minCost[j] = min(minCost[j], c[j]);vector<vector<int>> memo(n, vector<int>(1 << m, INT_MAX));function<int(int, int)> dfs = [&](int i, int j) -> int {if (i < 0) {int ans = 0;for (int k = 0; k < m; ++k)if (j >> k & 1) // 第二组的点集中k未连接ans += minCost[k]; // 去第一组找个成本最小的点连接return ans;}int &ans = memo[i][j]; // 引用if (ans != INT_MAX) return ans; // 之前算过了for (int k = 0; k < m; ++k) // 第一组的点i与第二组的点kans = min(ans, dfs(i - 1, j & ~(1 << k)) + cost[i][k]);return ans;};return dfs(n - 1, (1 << m) - 1);}
};

复杂度分析:

  • 时间复杂度: O ( n m 2 m ) \mathcal{O}(nm2^m) O(nm2m),其中 n n n m m m 分别为 cost \textit{cost} cost 的行数和列数。动态规划的时间复杂度 == 状态个数 × \times × 单个状态的计算时间。本题中状态个数等于 O ( n 2 m ) \mathcal{O}(n2^m) O(n2m) ,单个状态的计算时间为 O ( m ) \mathcal{O}(m) O(m) ,所以总的时间复杂度为 O ( n m 2 m ) \mathcal{O}(nm2^m) O(nm2m)
  • 空间复杂度: O ( n 2 m ) \mathcal{O}(n2^m) O(n2m)

解法2 递推

去掉递归中的「递」,只保留「归」的部分,即自底向上计算。通用做法:

  • dfs \textit{dfs} dfs 改成 f f f 数组;
  • 递归改成循环;
  • 递归边界改成 f f f 的初始状态。相当于原来是用递归去计算每个状态,现在是用循环去计算每个状态。

具体来说, f [ i ] [ j ] f[i][j] f[i][j] 的含义和状态转移方程和上面的记忆化搜索是一样的,即:
f [ i ] [ j ] = min ⁡ k = 0 m − 1 f [ i − 1 ] [ j ∖ { k } ] + cost [ i ] [ k ] f[i][j] =\min_{k=0}^{m-1} f[i-1][j\setminus\{k\}] + \textit{cost}[i][k] f[i][j]=k=0minm1f[i1][j{k}]+cost[i][k]
但当 i = 0 i=0 i=0 时,等号右边会出现负数下标。或者说,这种定义方式没有状态能表示递归边界。

解决办法:在 f f f 数组的上边加一排,把原来的 f [ i ] f[i] f[i] 改成 f [ i + 1 ] f[i+1] f[i+1] f [ i − 1 ] f[i-1] f[i1] 改成 f [ i ] f[i] f[i] 。此时 f [ 0 ] [ j ] f[0][j] f[0][j] 就对应着 dfs ( − 1 , j ) \textit{dfs}(-1,j) dfs(1,j)

修改后的递推式为
f [ i + 1 ] [ j ] = min ⁡ k = 0 m − 1 f [ i ] [ j ∖ { k } ] + cost [ i ] [ k ] f[i+1][j] =\min_{k=0}^{m-1} f[i][j\setminus\{k\}] + \textit{cost}[i][k] f[i+1][j]=k=0minm1f[i][j{k}]+cost[i][k]
注意只需要把 f f f 中的 i i i 加一, cost \textit{cost} cost 中的 i i i 不受影响。初始值 f [ 0 ] [ j ] = ∑ k ∈ j minCost [ k ] f[0][j]= \sum\limits_{k\in j} \textit{minCost}[k] f[0][j]=kjminCost[k]
答案为 f [ n ] [ { 0 , 1 , ⋯ , m − 1 } ] f[n][\{0,1,\cdots,m-1\}] f[n][{0,1,,m1}]

class Solution {
public:int connectTwoGroups(vector<vector<int>>& cost) {int n = cost.size(), m = cost[0].size();vector<int> minCost(m, INT_MAX);for (int j = 0; j < m; ++j)for (auto &c : cost)minCost[j] = min(minCost[j], c[j]);vector<vector<int>> f(n + 1, vector<int>(1 << m));for (int j = 0; j < 1 << m; ++j) for (int k = 0; k < m; ++k)if (j >> k & 1) // 第二组的点k未连接f[0][j] += minCost[k]; //去第一组找个成本最小的点连接for (int i = 0; i < n; ++i) {for (int j = 0; j < 1 << m; ++j) {int ans = INT_MAX;for (int k = 0; k < m; ++k) // 第一组的点i与第二组的点k连接ans = min(ans, f[i][j & ~(1 << k)] + cost[i][k]);f[i + 1][j] = ans;}}return f[n][(1 << m) - 1];}
};

复杂度分析:

  • 时间复杂度: O ( n m 2 m ) \mathcal{O}(nm2^m) O(nm2m),其中 n n n m m m 分别为 cost \textit{cost} cost 的行数和列数。动态规划的时间复杂度 == 状态个数 × \times × 单个状态的计算时间。本题中状态个数等于 O ( n 2 m ) \mathcal{O}(n2^m) O(n2m) ,单个状态的计算时间为 O ( m ) \mathcal{O}(m) O(m) ,所以总的时间复杂度为 O ( n m 2 m ) \mathcal{O}(nm2^m) O(nm2m)
  • 空间复杂度: O ( n 2 m ) \mathcal{O}(n2^m) O(n2m)
两个优化

由于 f [ i + 1 ] f[i+1] f[i+1] 只和 f [ i ] f[i] f[i] 有关,因此可以仿照 0-1 背包,优化掉第一个维度,只用一个长为 2 m 2^m 2m 的一维数组。

去掉第一个维度后,考虑 f f f 数组初始值的计算,还可以进一步优化

假设现在算出了 f [ 00 ] , f [ 01 ] , f [ 10 ] , f [ 11 ] f[00],f[01],f[10],f[11] f[00],f[01],f[10],f[11](这里用二进制数表示集合),那么结合 minCost [ 2 ] \textit{minCost}[2] minCost[2] ,可以算出
f [ 100 ] = f [ 00 ] + minCost [ 2 ] f [ 101 ] = f [ 01 ] + minCost [ 2 ] f [ 110 ] = f [ 10 ] + minCost [ 2 ] f [ 111 ] = f [ 11 ] + minCost [ 2 ] \begin{aligned} f[100] = f[00] + \textit{minCost}[2]\\ f[101] = f[01] + \textit{minCost}[2]\\ f[110] = f[10] + \textit{minCost}[2]\\ f[111] = f[11] + \textit{minCost}[2]\end{aligned} f[100]=f[00]+minCost[2]f[101]=f[01]+minCost[2]f[110]=f[10]+minCost[2]f[111]=f[11]+minCost[2]
这样就得到了 f [ 0 ] f[0] f[0] f [ 111 ] f[111] f[111] 。按照同样的方式,结合 minCost [ 3 ] \textit{minCost}[3] minCost[3] ,可以算出 f [ 0 ] f[0] f[0] f [ 1111 ] f[1111] f[1111] 。依此类推。

这样每个初始值只需要 O ( 1 ) \mathcal{O}(1) O(1) 的时间就能递推算出来。

class Solution {
public:int connectTwoGroups(vector<vector<int>>& cost) {int n = cost.size(), m = cost[0].size();int f[1 << m];f[0] = 0;for (int j = 0; j < m; ++j) {int mn = INT_MAX;for (auto &c : cost) mn = min(mn, c[j]);int bit = 1 << j;for (int mask = 0; mask < bit; ++mask)f[bit | mask] = f[mask] + mn;}for (auto &row : cost) {for (int j = (1 << m) - 1; j >= 0; --j) {int ans = INT_MAX;for (int k = 0; k < m; ++k) // 第一组的点i与第二组的点kans = min(ans, f[j & ~(1 << k)] + row[k]);f[j] = ans;}}return f[(1 << m) - 1];}
};

复杂度分析:

  • 时间复杂度: O ( n m 2 m ) \mathcal{O}(nm2^m) O(nm2m) ,其中 n n n m m m 分别为 cost \textit{cost} cost 的行数和列数。计算 f f f 数组的初始值,循环次数为 2 0 + 2 1 + 2 2 ⋯ + 2 m − 1 = 2 m − 1 2^0+2^1+2^2\cdots+2^{m-1} = 2^m-1 20+21+22+2m1=2m1 ,这比优化前的 m 2 m m2^m m2m 要快。动态规划的时间复杂度 = 状态个数 × \times × 单个状态的计算时间。本题中状态个数等于 O ( n 2 m ) O(n2^m) O(n2m) ,单个状态的计算时间为 O ( m ) \mathcal{O}(m) O(m) ,所以总的时间复杂度为 O ( n m 2 m ) \mathcal{O}(nm2^m) O(nm2m)
  • 空间复杂度: O ( 2 m ) \mathcal{O}(2^m) O(2m)

这篇关于LeetCode 1595. 连通两组点的最小成本【记忆化搜索,状压DP】2537的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

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

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];

poj 2349 Arctic Network uva 10369(prim or kruscal最小生成树)

题目很麻烦,因为不熟悉最小生成树的算法调试了好久。 感觉网上的题目解释都没说得很清楚,不适合新手。自己写一个。 题意:给你点的坐标,然后两点间可以有两种方式来通信:第一种是卫星通信,第二种是无线电通信。 卫星通信:任何两个有卫星频道的点间都可以直接建立连接,与点间的距离无关; 无线电通信:两个点之间的距离不能超过D,无线电收发器的功率越大,D越大,越昂贵。 计算无线电收发器D