[bzoj4006][JLOI2015]管道连接

时间:2021-03-31 18:51:40

Description

给出一张$n$个点$m$条边的无向图和$p$个特殊点,每个特殊点有一个颜色。要求选出若干条边,使得颜色相同的特殊点在同一个连通块内。输出最小边权和。

Input

第一行包含三个整数 $n、m、p$,表示情报站的数量,可以建立的通道数量和重要情报站的数量。接下来$m$行,每行包含三个整数$u_i、v_i、w_i$,表示可以建立的通道。最后有$p$行,每行包含两个整数$c_i、d_i$,表示重要情报站的频道和情报站的编号。

Output

输出一行一个整数,表示任意相同频道的情报站之间都建立通道连接所花费的最少资源总量。
 
$0 <c_i \leq p \leq 10$
$0 <u_i,v_i,d_i \leq n \leq 1000$
$0 \leq m \leq 3000$
 
 
题解
我们暂且忽略不同频道的问题,先假设我们只需求通过选边使得所有关键点相互联通的代价。
 
这是一个用状压DP和斯坦纳树解决的问题。
最终连出来的一定是一颗树,因此我们就称之为斯坦纳树
由于关键点的数量非常少,我们可以所有的关键点是否在当前联通块内的状态压缩
设$F[i][sta]$为在$i$所在的联通块中,关键点联通状态为$sta$时联通块内边权和的最小值
转移有两种
第一种:以相邻点的相同状态加上边权转移
第二种:以该点本身的不同状态之和转移
列出式子大概是这样(其中$w_{(i,j)}$为以$(i,j)$为顶点的边权):
$$ F[i][sta]=min\{F[j][sta]+w_{(i,j)}\} $$
$$ F[i][sta]=min\{F[i][S_1]+F[i][S_2]\}(S_1|S_2==sta)$$
 
转移顺序
我们按从小到大的顺序枚剧每一层状态。
再枚举状态的每一个子集,再对于每个点用这个子集和状态异或上这个子集的两个答案来更新当前状态的答案。
对于每一层状态,进行完上一行所属的转移后,再在不同的点之间进行更新,这一步可以用$SPFA$或堆优化的$Dijkstra$完成。
 
这样我们就可以解决忽略频道意义下的斯坦纳树。
 
但是这道题只需要相同颜色的关键点联通,因此直接算斯坦纳树会多算一部分答案,所以需要将答案拓展到斯坦纳森林的情况。
 
我们发现,即便拓展到斯坦纳森林,图还是由每个频道的斯坦纳树构成,因此我们可以先求出每一个频道的斯坦纳树,再这个的基础上再进行状压DP,状态为每一个频道的点是否在当前的斯坦纳森林中,最后求出答案。
 
复杂度方面,这里就不再仔细计算了,很显然的是主要复杂度为$O($枚举状态$\times$枚举子集$\times$枚举点$)$。
考虑优化发现枚举状态和枚举点都不可避免,其中枚举子集中多枚举了许多状态,具体如下:
 
朴素的枚举子集:
for(int sta=1;sta<(1<<K);sta++){
	for(int S=0;S<sta;S++){
		if((S&sta)!=S) continue;
		
	}
}
 
带有技巧的枚举子集:
for(int sta=1;sta<(1<<K);sta++){
	for(int S=((sta-1)&sta);S;S=((S-1)&sta)){
		
	}
}
这样能恰好不重不漏的依次枚举到$sta$的每一个子集。
 
 
AC代码如下:
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<queue>
#define LL long long
#define M 100200
#define INF 1000000000
using namespace std;
int read(){
	int nm=0,fh=1;char cw=getchar();
	for(;!isdigit(cw);cw=getchar()) if(cw=='-') fh=-fh;
	for(;isdigit(cw);cw=getchar()) nm=nm*10+(cw-'0');
	return nm*fh;
}
int n,m,tot,fs[M],nt[M],to[M],len[M],u,v,tmp,dst,MAXN,dt;
int kd[M],pos[M],st[20],F[1050],dis[1050][1050],cnt[20];
bool vis[1050];
void link(int x,int y){nt[tmp]=fs[x],fs[x]=tmp,to[tmp]=y,len[tmp++]=dst;}
struct STA{
    int dist,ps,sa;
    STA(){} 
    STA(int _ps,int _sa,int _dist){ps=_ps,sa=_sa,dist=_dist;}
    bool operator <(const STA&ot)const{return dist>ot.dist;} 
};
priority_queue<STA> Q;
void DIJK(int sta){
	memset(vis,false,sizeof(vis));
	while(!Q.empty()) Q.pop();
	for(int i=1;i<=n;i++) if(dis[i][sta]<INF) Q.push(STA(i,sta,dis[i][sta]));
	while(!Q.empty()){
		int x=Q.top().ps; Q.pop();
		if(vis[x]) continue;
		else vis[x]=false;
		for(int i=fs[x];i!=-1;i=nt[i]){
			dst=len[i]+dis[x][sta],v=to[i];
			if(dis[v][sta]<=dst) continue;
			dis[v][sta]=dst,Q.push(STA(v,sta,dst));
		}
	}
}
int main(){
	memset(dis,0x3f,sizeof(dis));
	memset(fs,-1,sizeof(fs)),memset(F,0x3f,sizeof(F));
	n=read(),m=read(),tot=read(),MAXN=(1<<tot)-1,kd[0]=1,F[0]=0;
	for(int i=1;i<=m;i++) u=read(),v=read(),dst=read(),link(u,v),link(v,u);
	for(int i=1;i<=n;i++) dis[i][0]=0;
	for(int i=0;i<tot;i++){
	    u=read(),v=read(),pos[v]=(1<<i);
		st[u]|=pos[v],dis[v][pos[v]]=0;
	}
	for(int i=0;i<=tot;i++) kd[st[i]]=1;
	for(int now=1;now<=MAXN;now++){
		for(int S=((now-1)&now);S;S=((S-1)&now)){
			for(int i=1;i<=n;i++){
			    dis[i][now]=min(dis[i][now],dis[i][S]+dis[i][now^S]);
			}
		}
		DIJK(now);
		for(int i=1;i<=n;i++) F[now]=min(F[now],dis[i][now]);
	}
	for(int now=1;now<=MAXN;now++){
		for(int S=((now-1)&now);S;S=((S-1)&now)){
			if((!kd[S])||(!kd[now^S])) continue;
			kd[now]=1,F[now]=min(F[now],F[S]+F[now^S]);
		}
	}
	printf("%d\n",F[MAXN]);
	return 0;
}