LeetCode 1595 连通两组点的最小成本 状压DP

1. 题目描述

1.1. Limit

Time Limit: 2000 ms

Memory Limit: 131072 kB

1.2. Problem Description

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

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

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

1.3. Sample Input 1

输入:cost = [[15, 96], [36, 2]]

1.4. Sample Output 1

输出:17
解释:连通两组点的最佳方法是:
1--A
2--B
总成本为 17

1.5. Sample Input 2

输入:cost = [[1, 3, 5], [4, 1, 1], [1, 5, 3]]

1.6. Sample Output 2

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

1.7. Source

LeetCode 1595 连通两组点的最小成本

2. 解读

状态压缩DP

2.1. 解法一

以输入 cost = [[1, 3, 5], [4, 1, 1], [1, 5, 3]] 为例。

dp[i][state]表示 前 i在每一行都至少有一条边连接的情况下 构成连接状态 state 的最小成本。

( i , s t a t e ) (i, state) (i,state) ( 001 ) B (001)_B (001)B ( 010 ) B (010)_B (010)B ( 011 ) B (011)_B (011)B ( 100 ) B (100)_B (100)B ( 101 ) B (101)_B (101)B ( 110 ) B (110)_B (110)B ( 111 ) B (111)_B (111)B
11345689
25426243
36939374

表1 状态示例 \text{表1 状态示例} 状态示例

首先对每一行 i i i 的每个状态 s t a t e state state 进行遍历,连接下一行的某一条边,得到连接成下一个状态的最小成本。

temp[nextState] = min(temp[nextState], dp[state] + cost[i][j]

接下来对每个状态 s t a t e state state 进行反转,得到反转后的状态 f l i p S t a t e flipState flipState,然后枚举 f l i p S t a t e flipState flipState 的子状态 $subState = w & flipState $, $ w \in [1, flipState]$,将子状态和原状态相或,得到新状态 n e w S t a t e = s t a t e ∣ s u b S t a t e newState = state | subState newState=statesubState,尝试根据 n e w S t a t e newState newState 连接新的边,得到新边的权值 s u m sum sum,取最小值。

temp[nextState] = min(temp[nextState], dp[state] + sum)

2.2. 解法二

dp[i][s] 表示左边前 i 个点,右边点连接状况为 s 的最小 cost

三重循环,和Floyd算法的思想有点类似。

Floyd算法是遍历每个点,判断把这个点作为某条路径的中间结点是否能缩短路径长度。

这里是遍历右边的每个点,判断把它和 i - 1点 相连的最小成本,即将 cost[i - 1][j]dp[i - 1][s]dp[i][s]相加所得的和的最小值。

比如表1中的 dp[2][(111)B],可以由 dp[1][(011)B] + cost[1][2] 得到,也可以由 dp[2][(011)B] + cost[1][2]得到,由于 dp[2][(011)B] < dp[1][(011)B],最终结果为3

for (int i = 1; i <= n; i++) // 枚举左边每个点
    for (int s = 0; s < lim; s++) // 枚举右边的连接状态
        for (int j = 0; j < m; j++) // 枚举连接右边的点
            // 连接一条边,得到新状态
            dp[i][s | (1 << j)] = min({ dp[i][s | (1 << j)],
                dp[i - 1][s] + cost[i - 1][j], // 用前i-1个点连成s状态,进行递推
                dp[i][s] + cost[i - 1][j] }); // 用前i个点连成s状态,进行递推

3. 代码

代码参考自一位题友的题解

// 解法一
class Solution {
public:
    int connectTwoGroups(vector<vector<int>>& cost)
    {
        int size1 = cost.size(), size2 = cost[0].size(), stateNum = 1 << size2; //stateNum为第二组总的状态数+1
        vector<int> dp(stateNum, INT_MAX); //dp数组初始化为很大的数
        dp[0] = 0; //初始状态
        for (int i = 0; i < size1; ++i) { //迭代每一行
            vector<int> temp(stateNum, INT_MAX); //滚动数组
            for (int state = 0; state < stateNum; ++state) { //枚举所有状态
                if (dp[state] == INT_MAX)
                    continue; //若状态不可达,continue
                for (int j = 0; j < size2; ++j) { //方案一:任选一条边相连
                    int nextState = state | (1 << j); //相连后到达的状态
                    temp[nextState] = min(temp[nextState], dp[state] + cost[i][j]); //更新最小花费
                }
                int flipState = (stateNum - 1) ^ state; //方案二:连接若干未连接的边,使用异或进行位反转得到所有未连接的边
                for (int subState = flipState; subState; subState = flipState & (subState - 1)) { //枚举未连接的边的子集
                    int nextState = state | subState; //相连后到达的状态
                    if (nextState > state) { // 若添加了新边
                        int sum = 0; //记录花费
                        for (int k = 0; k < size2; ++k) //枚举size2
                            if (subState & (1 << k))
                                sum += cost[i][k]; //若子集中存在该边,则更新花费
                        temp[nextState] = min(temp[nextState], dp[state] + sum); //更新最小花费
                    }
                }
            }
            dp = move(temp); //滚动数组
        }
        return dp.back(); //返回结果
    }

};

代码参考自另一位题友的题解

// 解法二
int dp[15][1 << 12 + 5];

class Solution {
public:
    int connectTwoGroups(vector<vector<int>>& cost)
    {
        int n = cost.size(), m = cost[0].size();
        int lim = 1 << m;
        // 初始化
        for (int i = 0; i <= n; i++)
            for (int s = 0; s < lim; ++s)
                dp[i][s] = INT_MAX;
        dp[0][0] = 0;
        // DP
        for (int i = 1; i <= n; i++) // 枚举左边每个点
            for (int s = 0; s < lim; s++) // 枚举右边的连接状态
                for (int j = 0; j < m; j++) // 枚举连接右边的点
                    // 连接一条边,得到新状态
                    dp[i][s | (1 << j)] = min({ dp[i][s | (1 << j)],
                        dp[i - 1][s] + cost[i - 1][j], // 用前i-1个点连成s状态,进行递推
                        dp[i][s] + cost[i - 1][j] }); // 用前i个点连成s状态,进行递推

        return dp[n][lim - 1];
    }
};

联系邮箱:curren_wong@163.com

CSDN:https://me.csdn.net/qq_41729780

知乎:https://zhuanlan.zhihu.com/c_1225417532351741952

公众号:复杂网络与机器学习

欢迎关注/转载,有问题欢迎通过邮箱交流。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值