[2019 CSP-S赛前集训] [P3959] [蒟蒻Xx_queue学DP] 3.宝藏

蒟蒻Xx_queue又来更博客啦!!!

话说之前一直都没有讲到与状压DP枚举子集有关的知识,那我们今天就来补充一下;

今天要讲的题目是:

[NOIP2017TG] 宝藏


题目传送门

题目描述

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

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

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

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

新开发一条道路的代价是:

(mathrm{L} imes mathrm{K})

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

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

输入格式

第一行两个用空格分离的正整数 (n),(m),代表宝藏屋的个数和道路数。

接下来 (m) 行,每行三个用空格分离的正整数,分别是由一条道路连接的两个宝藏 屋的编号(编号为 (1 - n)),和这条道路的长度 (v)

输出格式

一个正整数,表示最小的总代价。

输入输出样例

输入 1
4 5
1 2 1
1 3 3
1 4 1
2 3 4
3 4 1

输出 1
4

输入 2
4 5
1 2 1
1 3 3
1 4 1
2 3 4
3 4 2
输出 2
5

说明/提示

【样例解释1】

小明选定让赞助商打通了(1) 号宝藏屋。小明开发了道路 (1 o 2),挖掘了 (2) 号宝 藏。开发了道路 (1 o 4),挖掘了 (4) 号宝藏。还开发了道路 (4 o 3),挖掘了(3)号宝 藏。工程总代价为:(1 imes 1 + 1 imes 1 + 1 imes 2 = 4)

【样例解释2】

小明选定让赞助商打通了(1) 号宝藏屋。小明开发了道路 (1 o 2),挖掘了 (2) 号宝 藏。开发了道路 (1 o 3),挖掘了 (3) 号宝藏。还开发了道路 (1 o 4),挖掘了(4)号宝 藏。工程总代价为:(1 imes 1 + 3 imes 1 + 1 imes 1 = 5)

【数据规模与约定】

对于20%的数据: 保证输入是一棵树(1≤n≤8)(v≤5000) 且所有的 (v)都相等。

对于 40%的数据: (1≤n≤8)(0≤m≤1000)(v≤5000) 且所有的(v)都相等。

对于70%的数据: (1≤n≤8)(0≤m≤1000)(v≤5000)

对于100%的数据: (1≤n≤12)(0≤m≤1000)(v≤500000)


分析:

先看数据范围:(1≤n≤12),不是爆搜就是状压DP;(爆搜加玄学剪枝当然可以过,详解见洛谷题解)

(玄学的模拟退火就算了,不知道你们有没有想到最小生成树,然而是行不通的)

这里我就来讲一讲状压DP的做法,毕竟前面我才刚刚写了一个状压DP的学习总结嘛(这回不是玉米田了);

因为题目中说到:新开发一条道路的代价是:

(mathrm{L} imes mathrm{K})

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

所以这个题目应该和你打通的数的层数有关系(因为你需要计算K啊);

所以不妨设(f_{i,j})表示已经打通的宝藏屋中的最大深度为(i),宝藏屋打通的状态(状压,二进制,大概明白是什么意思吧?)为(j),

(j)的每一位中1代表这个宝藏屋已经打通了,0表示没有打通;

现有的状态可以由 这个状态的子集+新打通的一些宝藏屋 转移过来

所以比较容易得出状态转移方程就是(f_{i,j}=min{f_{i-1,k}+(i-1)*c_{k,j}})

其中(i)为深度,(j)为目标状态,(k)(j)的子集,(c_{k,j})代表从状态(k)转移至(j)的花费(也就是把集合(j)的子集(k)添加一部分元素变成集合(j)所需要的代价)

蒯了一个图,大家看一看:(来自洛谷题解)

大概就是这样吧,不知道你搞懂了吗??

这里注意一些细节:我们需要预处理出从状态(k)转移至(j)的花费(下面的代码中用trans数组储存),还要记得开long long!

具体细节以及实现过程请研究代码:


AC代码:

#include <bits/stdc++.h>
#define N 13
#define INF 0x3f3f3f3f
using namespace std;
int n,m;
int dis[N][N];//两个宝藏屋之间的距离
int trans[1<<N][1<<N];//从一个状态转移至另一个状态(从集合的子集转移至该集合)的花费
long long dp[N][1<<N];//dp[i][j],i表示深度,j表示状态(二进制数)
void gettrans(){//取得转移花费
	int siz=(1<<n);
	for(int i=0;i<siz;i++){
		for(int j=i;j!=0;j=(j-1)&i){//枚举i的子集
			bool rightt=true;
			int now=i^j;//now为i-j(集合相减),得到的是j中不含而i中含有的元素
			for(int k=n;k>=1;k--){//枚举集合now的每一位
				if(now>=(1<<(k-1))){//now的第k位是否为1
					int minn=INF;
					for(int l=1;l<=n;l++)
						if(((1<<(l-1))&j)==(1<<(l-1))) minn=min(minn,dis[l][k]);//求出转移花费
					if(minn==INF){
						rightt=false;
						break;
					}
					trans[j][i]+=minn;
					now-=(1<<(k-1));//继续求
				}
			}
			if(rightt==false) trans[j][i]=INF;//转移不了视为inf
		}
	}
}
void DP(){//dp
	int siz=(1<<n);
	memset(dp,0x3f,sizeof(dp));//初始化为inf
	for(int i=1;i<=n;i++){
		dp[1][1<<(i-1)]=0;//只有一层,则为0(免费开采第一层)
	}
	for(int i=2;i<=n;i++){
		for(int j=0;j<siz;j++){
			for(int k=j;k!=0;k=(k-1)&j){//枚举子集
				if(trans[k][j]!=INF) dp[i][j]=min(dp[i][j],dp[i-1][k]+trans[k][j]*(i-1));//转移方程(前面详细讲过了,不知道的回炉重造)
			}
		}
	}
}
int main(){
	scanf("%d%d",&n,&m);
	memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=m;i++){//读入不用解释
		int u,v,wei;
		scanf("%d%d%d",&u,&v,&wei);
		if(wei<dis[u][v]){
			dis[u][v]=dis[v][u]=wei;
		}
	}
	gettrans();
	DP();
	long long ans=0x3f3f3f3f3f3f3f3f;
	int siz=(1<<n);
	for(int i=1;i<=n;i++) ans=min(ans,dp[i][siz-1]);//统计答案
	printf("%lld",ans);
	return 0;
}

怎么样?除了玉米田,可见枚举子集也是非常非常重要的(当然还有集合的基本运算),相信你弄懂这道题目后对于状压DP有更加理解了;

还是要多多练习啊!


转载请注明出处--Xx_queue
原文地址:https://www.cnblogs.com/Xx-queue/p/11755817.html