luogu3959 宝藏 题解

这题我只想称之为神

最优解中,开凿的道路一定联通,且一定是一棵树。这是因为如果最终不连通,那么显然不符合题意;如果最后不是一棵树,那么删去若干一条边后,不仅仍然可能连通,而且会减少代价。

暴搜显然不合适,那就考虑状压 DP。

DP 需要一定的顺序。和树形 DP 一样设子树信息为状态?不行,一开始并不是一棵树,直接 pass 掉。节点编号?也不行,根本转移不动。考虑到最后求出的实际上是一棵生成树,那么可以用生成树高度作为顺序,将免费打通的节点看作根。

于是就有了状态。设 \(f(i,S)\) 为联通的节点集合为 \(S\),当前生成树高度为 \(i\) 时,需要的最小代价。由于可以免费打通一个点,不难想到边界为 \[ f(i,S) = \begin{cases} 0 & i=1, |S|=1 \\ \infty & \text{otherwise} \end{cases} \] 设全集为 \(U\),答案为 \(\min \limits_{i \in [1,n]}{ \{ f(i,U) \} }\)

 

接下来就是转移了。显然,设 \(S_0 \subseteq S\),一定能从 \(f(i-1,S_0)\) 转移到 \(f(i,S)\)

因为不管高度为 \(S_0\) 的这一棵树是怎么打通的,只要打通 \(S-S_0\) 中的所有点,就一定能够达到 \(S\) 这个状态。我们可以枚举每一个子集 \(S_0\),然后将这个状态加上转移所需要的最小代价,满足最优子结构性,这样一定是正确的。

代价怎么计算呢?道路长度可以贪心地选择最小的。由于是按照树高由低到高的顺序计算,所以经过的节点数就是i-1。那么所有新打通的边的 \(K\) 都是相同的(就是题目中的 \(K\))。所以设 \(d(i,j)\) 为状态 \(i\) 转移到状态 \(j\) 的最小边权和。

转移为 \[ f(i,j) = \min \limits_{k \subseteq j} { \{ f(i-1,k)+ (i-1) \cdot d(k,j) \} } \] \(d(i,j)\) 可以直接预处理,参考下面代码。

复杂度分析

预处理要枚举子集,枚举能够打通的点,复杂度为 \(O(n^23^n)\)

DP 时要枚举树高,枚举子集,复杂度为 \(O(n3^n)\)

总复杂度为 \(O(n^23^n)\)

当然你也可以用 预处理完一个 \(d(i,j)\) 后马上转移 来降低常数。

不过这也完全足够了,而且看着思路很清晰。

关于如下枚举子集的两个循环复杂度为何是 \(3^n\),这里不做赘述。

for(int i=0;i<=(1<<n)-1;++i) for(int j=i;j;j=(j-1)&1)

code

这题实现并不显然,注意代码,具体看注释。

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
#define SET(x,y) memset(x,y,sizeof(x))
const int N=13, M=1<<N, inf=0x3f3f3f3f;
int n, m, U, ans=inf, a[N][N], f[N][M], d[M][M];

void init() {
    scanf("%d%d",&n,&m);
    U=(1<<n)-1;
    SET(a,0x3f), SET(f,0x3f);
    for(int i=1,x,y,z;i<=m;++i) {
        scanf("%d%d%d",&x,&y,&z);
        a[x][y]=a[y][x]=min(a[x][y],z);
    }
}

void pre() {
    for(int i=0;i<=U;++i) for(int j=i;j;j=(j-1)&i) {
        int fg=1, u=i^j;
        // j这个循环意思是枚举i的所有子集
        // u是j关于i的补集,就是j->i要打通的节点集
        for(int k=0;k<n;++k) if((u>>k)&1) {
            // 枚举u的每一位,如果是1的话就找到达这个节点的最短边
            // 这个点记为a
            int t=inf;
            for(int o=0;o<n;++o) if((j>>o)&1)
            // 枚举j的每一位,如果是1的话,就记录这个点到达p的边权
            // 记为b,取b->a的最小值
                t=min(t,a[o+1][k+1]);
            	// 注意这里o和k都是要+1的,因为上面枚举的是二进制位。
            if(t==inf) { fg=0; break; }
            // t=inf 不存在b->a的边
            d[j][i]+=t;
            //直接累加
        }
        if(!fg) d[j][i]=inf;
        // 不存在的话,自然是inf了
    }
}

int main() {
    init(), pre();
    for(int i=0;i<n;++i) f[1][1<<i]=0;
    // 预处理
    for(int i=2;i<=n;++i) for(int j=0;j<=U;++j)
        for(int k=j;k;k=(k-1)&j)
        // 枚举树高,状态以及它的子集
            if(d[k][j]!=inf) f[i][j]=min(f[i][j],f[i-1][k]+(i-1)*d[k][j]);
    		// d[k][j]!=inf 可以转移
    for(int i=1;i<=n;++i) ans=min(ans,f[i][U]);
    printf("%d\n",ans); // 很友好,不会爆int
}

luogu3959 宝藏 题解
https://yozora0908.github.io/2022/lg3959-solution/
作者
yozora0908
发布于
2022年1月28日
许可协议