【状压dp,最短路】牛客CSP-S提高组赛前集训营5 B 十二桥问题

题目

十二桥问题

题目大意

给你一个n个点m条边的无向图,每条边有一个权值d,其中有k条边必须经过,从1号点出发,求经过这k条边,并且回到1号点的最小花费。

数据范围

n ≤ 50000,m ≤ 200000, d ≤ 1e9, k ≤ 12

分析

状压dp

首先拿到这道题,我们看到一个k ≤ 12的范围,是不是就想到了状压dp?

可是我们一般状压dp上面存的是点,(比如:dp[S][i] 表示选了集合S,最后一个点是i的最小花费),这道题要我们存边,如果把每一条边上的点都拿来做的话,点数猛增到24,然而
2 24 = 16 , 777 , 216 2^{24}=16,777,216 224=16,777,216
我们还要转移,相当于直接给这个方法判了死刑,那我们要怎么转移呢?

状压dp的优化

我们用 dp[S][i][0/1] 表示集合S上最后是第i条边 (这里的i是指这k条必走的边中的顺序,下方的i同义) 上的第一个/第二个点时的最小花费。
转移就是

for(re int S = 3; S < (1<<k);S += 2)
	for(re int i=1; i<=k; i++)//i: decided
	{
		int biti = 1 << (i-1);
		if( !(S & biti) ) continue;
		for(re int j=1; j<=k; j++)//j: going to decide
		{
			int bitj = 1 << (j-1);
			if( i == j || !(S & bitj) ) continue;
			dp[S][j][0] = min(dp[S][j][0], min( dp[S^bitj][i][0] + cost[i*2-1][j*2],
			dp[S^bitj][i][1] + cost[i*2][j*2]) + a[j].val);
			
			dp[S][j][1] = min(dp[S][j][1], min( dp[S^bitj][i][0] + cost[i*2-1][j*2-1],
			dp[S^bitj][i][1] + cost[i*2][j*2-1]) + a[j].val);
		}
	}

其中 cost[i][j] 是第i个点到第j个点的距离, a[i].val 表示第i条边的长度。

  • 至于为什么可以这么转移,留给读者自己思考。才不是懒得画图

最短路

最短路算法就不多说了,这里主要是分析一下计算最短路消耗的时间。

首先,如果我们算出每两个点之间的距离,每一次Dijkstra算法复杂度为 O ( m log ⁡ 2 n ) O(m\log_2n) O(mlog2n) ,要做n次,那么求最短路的总时间就是 O ( m ∗ n log ⁡ 2 n ) O(m*n\log_2n) O(mnlog2n),明显过不了。

那么我们考虑,事实上会用到的点只有这k条重要边上的点,我们其实只需要计算每个点到其他重要点的距离就可以了。复杂度为 O ( k ∗ m log ⁡ 2 n ) O(k*m\log_2n) O(kmlog2n),可以接受。

  • 注意这里给重要点编了一个号,方便访问
void Dij(int s)
{
	memset(dis, 0x3f, sizeof dis);inf=dis[0];
	while(!Q.empty()) Q.pop();
	Q.push(node(s, 0));dis[s]=0;
	
	while(!Q.empty())
	{
		node t = Q.top(); Q.pop();
		int u = t.p;
		if(dis[u] < t.d) continue;
		for(re int i=h[u]; ~i; i=e[i].nxt)
		{
			int v = e[i].to;
			LL val = e[i].val;
			LL tmp = dis[u];
			if(tmp + val < dis[v])
			{
				dis[v] = tmp + val;
				Q.push(node(v, dis[v]));
			}
		}
	}
}
for(re int i=1; i<=k; i++)
{
	u=a[i].a;v=a[i].b;
	Dij(u);
	for(re int j=1; j<=k; j++)
	{
		cost[i*2-1][j*2-1] = dis[a[j].a];
		cost[i*2-1][j*2] = dis[a[j].b];
	}
	Dij(v);
	for(re int j=1; j<=k; j++)
	{
		cost[i*2][j*2-1] = dis[a[j].a];
		cost[i*2][j*2] = dis[a[j].b];
	}
}

一点小技巧

事实上,细心的读者已经发现了我们之前的dp状态转移时集合 S 的枚举是很奇怪的:

for(re int S = 3; S < (1<<k);S += 2)

那么就要提到这道题的一个性质了,**我们必须要从1号点出发。**而普通的状压dp并不会固定第一个枚举的是什么,然而我们要特殊处理第一个点又很麻烦。

那么我们要怎么能在确保首先选第一个点的情况下,不增加太多代码难度呢?

其实我们可以把1号点看做第一个特殊点,不管它是否连重要边,然后从dp[1][1][0/1]开始转移即可

最后一个1号点,只需要统计完后再在结果中去计算最后一个1号点,统计答案即可

总结

综上,总的时间复杂度为 O ( k ∗ m log ⁡ 2 n + 2 k ∗ k 2 ) O( k*m\log_2n+ 2^k*k^2) O(kmlog2n+2kk2)

除了一些很模板的算法之外,再加上快读,register,inline等技巧(虽然说程序还是取决于算法,可是这些用来 骗骗分 还是很有用的。。。情况好的话可以多10分左右),性能上还是不错的结果

代码

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
#include <iostream>
#include <map>
#include <vector>
#include <set>

#define re register
#define ud unsigned
using namespace std;
typedef long long LL;

#define gc() getchar()
#define digit(x) (x>='0' && x<='9')

LL read()
{
	LL x=0,f=1; char c=gc();
	while(!digit(c)){if(c=='-') f=-f; if(c==-1) return -1; c=gc();}
	while(digit(c)) x=(x<<3)+(x<<1)+c-'0', c=gc();
	return x*f;
}

#undef gc
#undef digit

const int N = 50005, M = 200005, K = 16;
LL inf;

struct Edge
{
	int to, nxt;
	LL val;
	
	Edge(){}
	Edge(int T, int N, LL V){to=T;nxt=N;val=V;}
}e[M<<1];int cnte;
struct E
{
	int a, b;
	LL val;
	E(){}
	E(int A, int B, LL V){a=A;b=B;val=V;}
}a[K];
int h[N];
inline void Add_edge(int u, int v, LL val)
{
	e[++cnte] = Edge(v, h[u], val), h[u]=cnte;
	e[++cnte] = Edge(u, h[v], val), h[v]=cnte;
}
struct node
{
	int p;
	LL d;
	
	node(){}
	node(int P, LL D){p=P;d=D;}
	inline bool operator < (const node &b)const
	{return d > b.d;}
};

int n, m, k;
LL dis[N];
LL cost[K<<1][K<<1];
LL dp[1<<K][K][2];

priority_queue<node> Q;

void Dij(int s)
{
	memset(dis, 0x3f, sizeof dis);inf=dis[0];
	while(!Q.empty()) Q.pop();
	Q.push(node(s, 0));dis[s]=0;
	
	while(!Q.empty())
	{
		node t = Q.top(); Q.pop();
		int u = t.p;
		if(dis[u] < t.d) continue;
		for(re int i=h[u]; ~i; i=e[i].nxt)
		{
			int v = e[i].to;
			LL val = e[i].val;
			LL tmp = dis[u];
			if(tmp + val < dis[v])
			{
				dis[v] = tmp + val;
				Q.push(node(v, dis[v]));
			}
		}
	}
}

int main()
{
	memset(h, -1, sizeof h);
	n=read();m=read();k=read();
	
	int u, v;LL P;
	a[1].a=1;a[1].b=1;a[1].val=0;
	for(re int i=1; i<=m; i++)
	{
		u=read();v=read();P=read();
		if(i<=k) a[i+1].a=u, a[i+1].b=v, a[i+1].val=P;
		Add_edge(u, v, P);
	}
	
	k++;
	
	for(re int i=1; i<=k; i++)
	{
		u=a[i].a;v=a[i].b;
		Dij(u);
		for(re int j=1; j<=k; j++)
		{
			cost[i*2-1][j*2-1] = dis[a[j].a];
			cost[i*2-1][j*2] = dis[a[j].b];
		}
		Dij(v);
		for(re int j=1; j<=k; j++)
		{
			cost[i*2][j*2-1] = dis[a[j].a];
			cost[i*2][j*2] = dis[a[j].b];
		}
	}
	
	memset(dp, 0x3f, sizeof dp);
	dp[1][1][0]=dp[1][1][1]=0;
	
	for(re int S = 3; S < (1<<k);S += 2)
		for(re int i=1; i<=k; i++)//i: decided
		{
			int biti = 1 << (i-1);
			if( !(S & biti) ) continue;
			for(re int j=1; j<=k; j++)//j: going to decide
			{
				int bitj = 1 << (j-1);
				if( i == j || !(S & bitj) ) continue;
				dp[S][j][0] = min(dp[S][j][0], min( dp[S^bitj][i][0] + cost[i*2-1][j*2],
				dp[S^bitj][i][1] + cost[i*2][j*2]) + a[j].val);
				
				dp[S][j][1] = min(dp[S][j][1], min( dp[S^bitj][i][0] + cost[i*2-1][j*2-1],
				dp[S^bitj][i][1] + cost[i*2][j*2-1]) + a[j].val);
			}
		}
	
	LL ans = inf;
	for(re int i=2; i<=k; i++)
		ans = min( ans, min( dp[ (1 << k) - 1][i][0] + cost[i*2-1][1], dp[ (1 << k) - 1][i][1] + cost[i*2][1] ) );
	
	printf("%lld\n", ans);
	
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值