题解 P3264 【[JLOI2015]管道连接】

· · 题解

(不好意思麻烦管理员啦,以前没用过图床,结果这回写完就把图删掉了/笑哭)

算法:斯坦纳树+状压DP

此题题意很简单,就是要在图中选出一些边把给出的几个点集各子内部的点连通起来,问最小边权和。

看道题目就很容易想到斯坦纳树,但是注意:斯坦纳树是要求所有关键点联通,而此题只需要同一个类的关键点联通,不同类关键点连不连通都可以。那么就不能直接斯坦纳树了,该怎么做呢?

我们考虑一下斯坦纳树的状态设计,f_{s, i} 表示 s 集合内部的点都联通且根为 i 时的最小代价,是不是有启发呢?这道题棘手之处就在于你不知道哪些类会连在一棵树里,那么就可以一样状压 DP 啊!

我们把点的类别(频道)叫做”颜色“,设 g_s 表示把 s 集合内部的点构成斯坦纳树森林,且保证

很显然有转移:

g_s=\min\limits_{s' \subseteq s}{\ g_{s'}+g_{s-s'}}

设所有关建点构成的集合为 all ,最后的答案就是 g_{all}

也许你可能有疑惑:这样合并不会重复算边吗?

的确不错,是会重复算边,但是不影响。因为如果我们把所有的情况考虑到了,由于边权非负,最后得到的最优答案一定时没有重复算边权的

但是我认为此题难点不在这里,而是在 g 数组的初始化(也可能是我太菜了),因此在这里把我写的过程犯的几个错误说一下,希望能帮到一些朋友 QAQ。

先赋 inf:

    memset(g, 0x3f, sizeof(g));

然后解决初值问题。

最开始,我是这么写的(求大佬不要嘲笑啊):

    for(int i=1; i<=K; ++i) if(p[i]){
        int S=p[i];
        for(int j=1; j<=n; ++j)
            g[S]=min(g[S], f[S][j]);
//      print(S),cout<<g[S]<<endl;
    }
直接把每个类的点搞一棵斯坦纳树嘛,之后不就可以合并了? 然而错的很离谱(话说这题数据是真的水,到后面你就知道了)…… ![WA 70分](https://cdn.luogu.com.cn/upload/image_hosting/z9vitp7h.png) 为什么?**因为完全可以把几类点放到一棵树里头啊!否则可能后面 DP 最小值就可能会得到重复算边代价的答案,导致答案偏大。** 那么,改一下就好了嘛: ``` for(int S=1; S<(1<<K); ++S){ work(S); for(int i=1; i<=K; ++i) if(S&p[i]==p[i]){ for(int j=1; j<=n; ++j) g[S]=min(g[S], f[S][j]); break; } } ``` 然而错的更离谱!要注意到,**&的优先级是低于==的!** 所以这样根本不是在判断是否为子集! 然而居然~ ![WA 80](https://cdn.luogu.com.cn/upload/image_hosting/juxrvv91.png?x-oss-process=image/resize,m_lfit,h_170,w_225) 好的赶快把这里改过来,以后要记得加括号,防止优先级问题。 改成了这样: ```cpp for(int S=1; S<(1<<K); ++S){ work(S); for(int i=1; i<=K; ++i) if(p[i]&&(S&p[i])==p[i]){ for(int j=1; j<=n; ++j) g[S]=min(g[S], f[S][j]); break; } } ``` 结果~ ![WA 85](https://cdn.luogu.com.cn/upload/image_hosting/tr322wb5.png) 噫,怎么还是WA,有毒啊QAQ~ 再仔细想一想,肯定还是状压DP部分的问题(斯坦纳树我是先过了模板的)。 观察评测结果,发现这次结果似乎偏小了!那么一定是不合法的状态更新了答案,也就是说,与上面状态限制中的两条发生了违背。为什么会这样呢? 仔细考虑这个判断,我们的思路是:**只要这个集合包含了某一类点的全部,就可以建成一棵斯坦纳树。** 漏洞在哪里?举个例子,如果这个集合虽然包含了所有”红色点“,但是包含了**一部分**”黄色点“的话,其实是不满足我们列出的两个条件的,因为**有黄色点在这个集合中,然而并非所有黄色点都在这个集合中,也并非所有黄色点都在一棵斯坦纳树中**。 形式化地讲,在初始化的时候(把初始化集合的点全部建到一棵树里头去),一个集合 $s$ 合法,当且仅当: **对于任何一种颜色,所有为此颜色的点要么全部在 $s$ 中,要么全部不在 $s$ 中**。 弄明白了这个,总算可以写出正确程序了: ```cpp for(int S=1; S<(1<<K); ++S){ work(S); bool flag=true; for(int i=1; i<=K; ++i) if(p[i]) if((S&p[i])!=p[i]&&(S&p[i])!=0) flag=false; if(flag) for(int i=1; i<=n; ++i) g[S]=min(g[S], f[S][i]); } ``` 下面给出完整 AC 代码: ```cpp #include <bits/stdc++.h> using namespace std; inline void read(int &x){ char c=getchar();x=0; while(!isdigit(c))c=getchar(); while(isdigit(c))x=x*10+c-'0',c=getchar(); } const int N=1005, M=3005, V=N, E=M<<1, NUM=15, SZ=1<<10|5, inf=0x3f3f3f3f; int n, m, K, key[NUM], col[NUM], p[NUM], f[SZ][N], ans, g[SZ]; int e, head[V], to[E], val[E], nxt[E]; inline void add(int u, int v, int w){ to[++e]=v, val[e]=w; nxt[e]=head[u], head[u]=e; } inline void spfa(int *f){ static bool inq[N]; static queue<int> q; for(int i=1; i<=n; ++i) if(f[i]<inf) q.push(i), inq[i]=true; while(!q.empty()){ int u=q.front(); q.pop(), inq[u]=false; for(int i=head[u]; i; i=nxt[i]){ int v=to[i], w=val[i]; if(f[v]>f[u]+w){ f[v]=f[u]+w; if(!inq[v]) q.push(v), inq[v]=true; } } } } inline void work(int S){ for(int s=(S-1)&S; s; s=(s-1)&S) for(int i=1; i<=n; ++i) f[S][i]=min(f[S][i], f[s][i]+f[S^s][i]); spfa(f[S]); } int main(){ read(n);read(m);read(K); for(int i=1, u, v, w; i<=m; ++i){ read(u);read(v);read(w); add(u, v, w), add(v, u, w); } memset(f, 0x3f, sizeof(f)); memset(g, 0x3f, sizeof(g)); for(int i=1; i<=K; ++i){ read(col[i]);read(key[i]); f[1<<(i-1)][key[i]]=0; p[col[i]]|=(1<<(i-1)); } for(int S=1; S<(1<<K); ++S){ work(S); bool flag=true; for(int i=1; i<=K; ++i) if(p[i]) if((S&p[i])!=p[i]&&(S&p[i])!=0) flag=false; if(flag) for(int i=1; i<=n; ++i) g[S]=min(g[S], f[S][i]); } for(int S=1; S<(1<<K); ++S) for(int s=(S-1)&S; s; s=(s-1)&S) g[S]=min(g[S], g[s]+g[S^s]); printf("%d\n", g[(1<<K)-1]); return 0; } ``` 这道题不能算是难题,但是写的时候还是要很注意细节,我就是因为这个初始化被坑了好久。所以建议大家在写 DP 的时候,要把状态的定义和转移条件等清晰地列出来,并根据这个考察代码的逻辑是否正确。希望这篇题解能帮到大家!(也希望管理员放通过一下,谢谢啦!)