AcWing
  • 首页
  • 课程
  • 题库
  • 更多
    • 竞赛
    • 题解
    • 分享
    • 问答
    • 应用
    • 校园
  • 关闭
    历史记录
    清除记录
    猜你想搜
    AcWing热点
  • App
  • 登录/注册

AcWing 529. 宝藏【状压DP+最小生成树】    原题链接    困难

作者: 作者的头像   一只野生彩色铅笔 ,  2021-07-30 23:27:43 ,  所有人可见 ,  阅读 3136


51


12

最近在补全提高课所有题目的题解,宣传一下汇总的地方提高课题解补全计划

题目描述

给定一个 $n$ 个点,$m$ 条边且连通的 无向图

初始时,无向图中没有边,每个 点 都是 互不连通 的

我们初始可以选择任意一个点作为 起点,且该选择 起点 的操作 费用 是 $0$

然后开始维护这个包含起点的 连通块

每次我们选择一个与 连通块内的点 相连的一条边,使得该边的另一个点(本不在连通块内的点)加入连通块

该次选边操作的费用是:$边权 \times 该点到起点的简单路径中经过的点数$

最终我们的目标是,使得 所有点都加入当前的连通块内

求解一个 方案 ,在达成目标的情况下,费用最小,输出方案的费用

分析

这题我试着预处理了一下合法状态和合法转移,结果爆MLE了
说明本题的时间复杂度也处在一个极限的情况下,暂时没有更好的优化思路了

题目要求已经连通的两点之间无须连接额外的边(这个条件其实不用给,可以推出来)
即我们只需选择 $n-1$ 条边即可

因此,我们的目标是:找出图中的 最小生成树

但本题的 最小生成树 和广义的最小生成树不同

因为在本题中,加入连通块的费用是随当前点到起点的路径线性变化的 (设经过的点数为 $k$,则费用为$w \times k$)

计算该次加边的费用是用到了如下两个参数:

  1. 起点
  2. 当前点在当前生成树上到起点的简单路径上经过的点数

如果恰好是只有其中一项限制,我们都可以直接套最小生成树的模版改一改即可

  1. 只有起点限制,我们可以暴力枚举起点然后套Prim即可 $O(n^3)$
  2. 只有当前生成树的状态限制,我们可以额外开一个数组,记录加入生成树的点到起点经过的点数,然后dfs $O(2^{2n})$

然而本题这两个参数都要考虑,就不能直接套模版来改了(不优化的话,时间复杂度要到 $O(n2^{2n})$ )

因此我们不妨采用 爆搜优化 -> 记忆化搜索 -> 动态规划 来解决该问题

我们把当前 生成树 的状态作为 DP 的阶段,那么需要额外记录的参数就是树的 深度

记录深度之后,可以保证我们枚举下一个阶段的状态时,能够轻易的计算他到起点的路径经过的点数

即我们把起点作为树的 root 然后一层一层构造 生成树

闫氏DP分析法

状态表示—集合$f_{i,j}:$ 当前生成树的状态是$i$ ($i$ 采用二进制压缩存储),且树的深度为 $j$ 的方案
状态表示—属性$f_{i,j}:$ 该方案构成的生成树,费用最少$Min$
状态计算—$f_{i,j}:$
$$ f_{i,j} = \min (f_{pre,j-1} + j \times cost) $$

初始状态: $f_{i,0} \quad (0\le i \lt n)$
目标状态: $f_{1\cdots1,j} \quad (0\le j \lt n)$

其中,$pre$ 是枚举的所有树 $i$ 的子集,$cost$ 则是由 $pre$ 扩展一层后变成 $i$ 所用的所有边的边权之和

需要注意⚠️:这里枚举 $pre$首先必须满足一定能够由 $pre$ 自身的点向外扩散一层后能够变成状态 $i$

这算是一个简单的状态转移优化,因为如果所有状态都枚举的话,时间复杂度会退化成爆搜


可能有人看到该 DP分析 第一时间会有疑问(至少我有):

在第 $j-1$ 到第 $j$ 层扩展的时候,有没有可能发生一次 $k-1$ 向 $k$ 层的扩展 (其中 $k < j$)

就是深层向下扩展时,出现了一个浅层往下延伸一个点的情况

根据我们的 转移方程,会把那个浅层扩展的路径数按照当前树的深度来计算,使得该方案费用比实际更大

这种方案是实际存在的,而且也是可能成为本题最优解答案的,但是我们可以不用考虑

为什么?因为该最优解方案是可以映射到我们DP方案中的某一个方案上的

我举一个简单的例子:

$$ f(0001_b,0) \rightarrow f(0011_b, 1) \rightarrow f(1111_b, 2) $$

其中点 $1$ 是通过 $1$ 层的点 $3$ 延伸到第 $2$ 层的,而点 $2$ 是通过起点 $4$ 延伸到第 $1$ 层的

IMG_A9751972D483-1.jpeg

则该类方案都一一映射到下面这类方案下

$$ f(0001_b,0) \rightarrow f(0111_b, 1) \rightarrow f(1111_b, 2) $$

IMG_3B8E9937E813-1.jpeg

也就证明了我们该类DP方案的最终答案可以是最优解

Code

时间复杂度:

  1. 预处理 $O(2^n \times n^2)$
  2. 动态规划 $O(n^2 3^n)$ 借鉴 y总的分析
#include <iostream>
#include <cstring>

using namespace std;

const int N = 15, M = 1 << N, K = 1010;
const int INF = 0x3f3f3f3f;

int n, m;
int g[N][N];
int ne[M];
int f[M][K];

void init()
{
    cin >> n >> m;
    memset(g, 0x3f, sizeof g);
    for (int i = 0; i < n; ++ i) g[i][i] = 0;
    while (m -- )
    {
        int a, b, c;
        cin >> a >> b >> c;
        a --, b -- ;
        g[a][b] = g[b][a] = min(g[a][b], c);
    }
    for (int st = 1; st < 1 << n; ++ st)//预处理所有状态能够扩展一层后得到的最大的下一层状态
        for (int i = 0; i < n; ++ i)
            if (st >> i & 1)
                for (int j = 0; j < n; ++ j)
                    if (g[i][j] != INF)
                        ne[st] |= 1 << j;
}
int get_cost(int cur, int pre)
{
    if ((ne[pre] & cur) != cur) return -1;//如果pre延伸不到cur直接剪枝
    int remain = pre ^ cur;//待更新的方案
    int cost = 0;//记录边权总和
    for (int i = 0; i < n; ++ i)
    {
        if (remain >> i & 1)
        {
            int t = INF;//找出当前连通块内能把该点加入所用的最小边的边长
            for (int j = 0; j < n; ++ j)
                if (pre >> j & 1)
                    t = min(t, g[j][i]);
            cost += t;
        }
    }
    return cost;//返回该次扩展的费用
}
//dp
int dp()
{
    memset(f, 0x3f, sizeof f);
    for (int i = 0; i < n; ++ i) f[1 << i][0] = 0;//开局免费选一个起点(初始状态)
    for (int cur = 1, cost; cur < 1 << n; ++ cur)
        for (int pre = cur - 1 & cur; pre; pre = pre - 1 & cur)
            if (~(cost = get_cost(cur, pre)))
                for (int k = 1; k < n; ++ k)
                    f[cur][k] = min(f[cur][k], f[pre][k - 1] + cost * k);
    int res = INF;
    for (int k = 0; k < n; ++ k) res = min(res, f[(1 << n) - 1][k]);//目标状态中找最优解
    return res;
}
int main()
{
    init();
    cout << dp() << endl;
    return 0;
}

32 评论


用户头像
VVOVV   2023-05-30 16:09      1    踩      回复
//只有当前生成树的状态限制,我们可以额外开一个数组,记录加入生成树的点到起点经过的点数,然后dfs O(22n)

很想知道暴力dfs怎么写呢

用户头像
糖豆   2023-05-31 10:19      3    踩      回复
#include <bits/stdc++.h>

using namespace std;

const int INF = 1000000; // 这个上限有点意思,需要仔细思考
const int N = 15;        // 点的个数上限 
int g[N][N];             // 记录边,就是地图
int n, m;                // n个节点,m条边
vector<int> p[N];        // 第一维:哪一行;第二维:此行第几个是哪个点
bool st[N];              // 每个点是否被用过
int res = INF;           // 结果

/*
 * u:现在考虑第u层
 * k:第几号节点
 * cnt:已经使用了cnt个点
 * sum:目前这个生成树的最小价值
 */
void dfs(int u, int k, int cnt, int sum) {
    if (sum >= res) return;           // 最优性剪枝,走一半就别人的最终结果大,就没有活下去的必要了
    if (p[u - 1].size() == 0) return; // 上一层没有选入点(上层为空),树无法继续构建。
    if (cnt == n) {                   // 所有点都已经被构建到树中,此时的结果有资格参选
        res = min(sum, res);
        return;
    }

    if (k > n) { // 该层所有点都讨论过一遍,继续下一层
        dfs(u + 1, 1, cnt, sum);
        return;
    }

    // 如果u层,k号点被标识为用过了
    if (st[k]) { // 该点已用,直接讨论下一个点
        dfs(u, k + 1, cnt, sum);
        return;
    }

    // 选用此点(可能1)
    st[k] = 1; // 标识k点已使用

    int cost = INF;                           // 找到该点与上层的最小联系
    for (int i = 0; i < p[u - 1].size(); i++) // 若没有联系(即与上层所有点不连通),则mini是INF,会被最优性剪枝减去
        cost = min(cost, g[p[u - 1][i]][k]);  // 上一行的每个节点,如果与k号节点的边相连,并且,权值最小的话,更新cost

    // 找到了上一层到u层,使得k点点亮的最小代价值

    p[u].push_back(k);
    dfs(u, k + 1, cnt + 1, sum + cost * u);
    p[u].pop_back();

    st[k] = 0; // 回溯

    // 不用此点(可能2)
    dfs(u, k + 1, cnt, sum);
}
int main() {
    memset(g, 0x3f, sizeof g); // 数组初始化
    scanf("%d %d", &n, &m);

    for (int i = 1; i <= m; i++) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        g[a][b] = g[b][a] = min(g[a][b], c); // 无向图双向赋值
    }

    for (int i = 1; i <= n; i++) { // 每个点都当一次根
        st[i] = 1;                 // i点已用
        p[0].push_back(i);         // 根看做第0层,0层第1个放的是i
        dfs(1, 1, 1, 0);           // 从第一层,第一个节点,已经使用了一个节点(根),现在的代价是0
        p[0].pop_back();
        st[i] = 0; // i点不用了
    }
    cout << res << endl;
    return 0;
}
用户头像
VVOVV   2023-05-31 10:42    回复了 糖豆 的评论         踩      回复

四国以内!

用户头像
VVOVV   2023-05-31 10:43    回复了 糖豆 的评论         踩      回复

我跪!


用户头像
user.banned   2023-05-29 20:03      1    踩      回复

彩铅氏dp分析法


用户头像
大胖子dpz   2022-11-24 01:10      1    踩      回复

555,刷题全靠彩铅老师


用户头像
sangkun   2021-10-11 16:23      1    踩      回复

只有当前生成树的状态限制,我们可以额外开一个数组,记录加入生成树的点到起点经过的点数,然后dfs O(2^2n)这个2^(2^n)是怎么算出来的呀?

用户头像
_27   2022-02-27 16:19      1    踩      回复

理论最大有2^n颗树,每次计算一颗树的最小费用总共时间约2^n

用户头像
CCCkk   2022-07-31 13:16    回复了 _27 的评论         踩      回复

怎么得出有$2^n$棵树的?

用户头像
CCCkk   2022-07-31 13:19    回复了 CCCkk 的评论         踩      回复

得出树了不应该还要这么多时间计算吧

用户头像
CCCkk   2022-07-31 13:22    回复了 CCCkk 的评论         踩      回复

可能您想的是找到树之后去找根,我想这是$O(n)$的?真奇怪

用户头像
_27   2022-07-31 14:04    回复了 CCCkk 的评论      1    踩      回复

我当时考虑的很有问题sorry....实际上最大有n^(n-2)棵树,共有n个节点,复杂度达到可怕的n^(n-1)

用户头像
CCCkk   2022-07-31 14:44    回复了 _27 的评论         踩      回复

感觉你算错了

用户头像
_27   2022-07-31 14:56    回复了 CCCkk 的评论      1    踩      回复

emmmm无向图中生成树的最大个数就是n^(n-2)罢


用户头像
itdef   2024-07-04 15:56         踩      回复

写得好,我写了一个超时的版本想后面优化. dp[x][y][st],x结尾的y层的状态st的转移过程。 写到一半才发现,状态想错了。


用户头像
user.banned   2023-05-29 20:53         踩      回复

举报!本题题解会RE!!!


用户头像
huazai676   2023-01-13 17:18         踩      回复

for (int pre = cur - 1 & cur; pre; pre = pre - 1 & cur)
这是怎么做到枚举每一个状态子集的。。。

用户头像
彩虹小马   2023-03-03 09:17         踩      回复

首先,不&cur的话,就是从大到小枚举所有数,然后加了&,就保证了是子集(某T大佬的解释)


用户头像
梦忆晴天   2022-11-09 11:50         踩      回复

~(cost = get_cost(cur, pre))
这句话在代码中是什么意思呢?

用户头像
梦忆晴天   2022-11-09 12:09         踩      回复

看懂了

用户头像
直接偷偷学习   2023-01-02 13:48    回复了 梦忆晴天 的评论         踩      回复

大佬你好,请问这句代码是什么意思啊

用户头像
梦忆晴天   2023-01-02 13:52    回复了 直接偷偷学习 的评论         踩      回复

get_cost返回值赋给cost不为-1就更新

用户头像
直接偷偷学习   2023-01-03 09:54    回复了 梦忆晴天 的评论         踩      回复

嗷嗷,好的,谢谢大佬


用户头像
CCCkk   2022-07-31 13:16         踩      回复

选定根的话不是只有$n$个树吗,复杂度应该是$n2^n$吧

用户头像
CCCkk   2022-07-31 13:17         踩      回复

树是指指数枚举的搜索树

用户头像
CCCkk   2022-07-31 13:19         踩      回复

希望有人能够解答

用户头像
CCCkk   2022-07-31 14:41         踩      回复

更正一下,好像是$3^n$,虽然这好像是正解复杂度,真不知道$2^{2n}$是怎么算的?


用户头像
炽热的   2022-05-17 09:42         踩      回复

其中点 $1$ 是通过 $1$ 层的点 $3$ 延伸到第 $2$ 层的,而点 $2$ 是通过起点 $4$ 延伸到第 $1$ 层的.
最后那句话, 点 $2$ 不是由起点 $4$ 延伸到第 $2$ 层吗

用户头像
kimmmm   2022-05-20 21:40         踩      回复

确实

用户头像
炽热的   2022-05-20 21:44    回复了 kimmmm 的评论         踩      回复

想不到能在这遇见你


用户头像
迷弟   2021-07-30 23:58         踩      回复

tql! 爱了㊫

用户头像
一只野生彩色铅笔   2021-07-31 09:11         踩      回复

QwQ


App 内打开
你确定删除吗?
1024
x

© 2018-2025 AcWing 版权所有  |  京ICP备2021015969号-2
用户协议  |  隐私政策  |  常见问题  |  联系我们
AcWing
请输入登录信息
更多登录方式: 微信图标 qq图标 qq图标
请输入绑定的邮箱地址
请输入注册信息