【Luogu P3959】NOIP2017 宝藏

题目链接

题目描述

参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n 个深埋在地下的宝藏屋, 也给出了这 n 个宝藏屋之间可供开发的 m 条道路和它们的长度。

小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是,每个宝藏屋距离地面都很远, 也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路 则相对容易很多。

小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某 个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。

在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以 任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路 所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏 屋之间的道路无需再开发。

新开发一条道路的代价是:
L × K L×K
L代表这条道路的长度,K代表从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的 宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋) 。

请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代 价最小,并输出这个最小值。

Sol

直接暴力压点集跑最短路是错的

因为当前点集的答案优了,但是你这个时候的到根的距离可能对后面的答案影响很大

MST就更错了

但是我们可以总结出他们错误的共同原因: 不能保证当前状态的选择对后面状态没有影响

扫描二维码关注公众号,回复: 3408373 查看本文章

也就是每一个点的深度必须要是强制确定的,这样才能保证最优性和无后效性

所以考虑一个基于深度的dp


d p [ i ] [ j ] dp[i][j] 表示当前计算到了第 i i 层,选取的点集是 j j 的最小花费

最后答案就是每一层的满状态的 d p dp

d p dp 过程其实也比较明显,先枚举深度,直接每次枚举一下当前的目标状态,再枚举一个子集进行转移即可
转移时把这次新选的点直接在已经选的点中选一个边权最小的点接上去,深度可以直接视为 i i ,因为接在上面层的应该先就被上面算过了,并且无论是否真的接在了这一层,接错了也只会让答案变差,因此不会影响答案的正确性

这样复杂度是 O ( n 3 3 n ) O(n^33^n)

由于每一个点在已经确定已选点集的情况下的连边是固定的,我们可以直接把一个点在已经选了什么点的情况下的连边预处理出来

这样复杂度就变成了 O ( n 2 3 n ) O(n^23^n)

代码:

#include<bits/stdc++.h>
#define Set(a,b) memset(a,b,sizeof(a))
using namespace std;
const int N=15;
const int MAXN=1<<15;
int dis[N][N];
int f[N][MAXN];//选了集合为 j 的 i 在这些点中边权的最小值
int g[N][MAXN];//选到了第 i 层,已经选了的点集为 j 的最小花费
const int INF=2e9;
int n,m;
inline void Print(int x)
{
	for(register int i=1;i<=n;++i){
		putchar((x&1)+'0');x>>=1;
	}
}
int main()
{
	scanf("%d %d",&n,&m);
	for(register int i=0;i<N;++i)for(register int j=0;j<MAXN;++j) f[i][j]=g[i][j]=INF;
	register int u,v,w;
	for(register int i=1;i<=m;++i){
		scanf("%d %d %d",&u,&v,&w);
		if(!dis[u][v]) dis[u][v]=dis[v][u]=w;
		else dis[u][v]=dis[v][u]=min(dis[u][v],w);
	}
	register int UP=1<<n;
	for(register int i=1;i<=n;++i)
		for(register int j=1;j<UP;++j)
			for(register int k=1;k<=n;++k)
				if(1<<k-1&j) f[i][j]=min(f[i][j],(dis[i][k]==0? INF:dis[i][k]));
	for(register int i=1;i<=n;++i) g[1][1<<i-1]=0;
	for(register int i=2;i<=n;++i){
		
		for(register int j=1;j<UP;++j){
			
			for(register int s=(j-1)&j;s;s=(s-1)&j){
				register int sum=0;bool flag=1;
				if(g[i-1][s]==INF) continue;
				for(register int k=1;k<=n;++k) {if((1<<k-1&j)&&!(1<<k-1&s)) {if(f[k][s]==INF) {flag=0;break;} sum+=f[k][s];}}//在已有集合中选择一个点
				//无论是否真的接在了这一层,由于接在上面层的应该先就被上面算过了,接错了也只会让答案变差,因此不会不合法
				if(!flag) continue;
				g[i][j]=min(g[i-1][s]+sum*(i-1),g[i][j]);
			}
		}
	}
	register int ans=INF;
	for(register int i=1;i<=n;++i) ans=min(ans,g[i][UP-1]);
	printf("%d\n",ans);
}

猜你喜欢

转载自blog.csdn.net/element_hero/article/details/82760839