参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n 个深埋在地下的宝藏屋, 也给出了这 n 个宝藏屋之间可供开发的m 条道路和它们的长度。
小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是,每个宝藏屋距离地面都很远, 也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路 则相对容易很多。
小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某 个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。
在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以 任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路 所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏 屋之间的道路无需再开发。
新开发一条道路的代价是:
L×K
L代表这条道路的长度,K代表从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的 宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋) 。
请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代 价最小,并输出这个最小值。
【样例解释1】
小明选定让赞助商打通了1 号宝藏屋。小明开发了道路 1→2,挖掘了 2 号宝 藏。开发了道路 1→4,挖掘了 4号宝藏。还开发了道路 4→3,挖掘了3号宝 藏。工程总代价为:1×1+1×1+1×2=4
【样例解释2】
小明选定让赞助商打通了1 号宝藏屋。小明开发了道路1→2,挖掘了 2 号宝 藏。开发了道路1→3,挖掘了 3号宝藏。还开发了道路 1→4,挖掘了4号宝 藏。工程总代价为:1×1+3×1+1×1=5
【数据规模与约定】
对于20%的数据: 保证输入是一棵树,1≤n≤8,v≤5000 且所有的v都相等。
对于40%的数据: 1≤n≤8,m≤1000,v≤5000 且所有的v都相等。
对于70%的数据: 1≤n≤8,m≤1000,v≤5000
对于100%的数据:1≤n≤12,m≤1000,v≤500000
https://www.luogu.org/problemnew/show/P3959
算法构建与设计:
话说。。。这题不应该先看数据范围的嘛
我们发现,题目中明确:
对于100%的数据,1<=n<=12
以我们学习OI的经验,这题肯定是个状态压缩(状压)
(因为数据范围小到屈指可数啊)
那么,什么是状压呢?
听起来高大上,其实没什么
下面讲一讲我自己的理解吧(应该有专业术语的,可是我懒,不想度娘)
状压,即状态压缩的简称,是一种(在数据范围较小的情况下)将每个物品或者东西选与不选的状态“压”成一个整数的方法
通常我们采用二进制状压法,(对于二进制状压)即对于一个我们“压”成的状态,这个整数在二进制下中的1表示某个物品已选,而0代表某个物品未选,这样我们就可以通过枚举这些“压”成的整数来达到枚举所有的物品选与不选的总情况,通常我们称这些情况叫做子集,对于这个状态整数,通常设为s
(对于二进制状压)通过二进制下的位运算来达到判断某个物品选与不选的情况,再通过这个状态来进行一些其他的扩展,所以状压能简化我们对于问题的求解方式
而状压dp正是用到了这一点,通过一个状态来表示整体情况,对于这个情况进行一些最优化操作,最终达到求得全局最优解的目的
在这里,状压dp需要用到一些位运算的知识,下面给出一张图说明
以上是一些二进制的基本运算,掌握这些对于状压很有帮助!
那么有了这些位运算的法则,我们就可以快速判断一个东西在状态s中是否处于选或不选的状态,便于我们对于状态的更新与某一位的判断
(笔者的语句可能有些粗糙,理解大意即可,毕竟本人理解可能不太到位)
然而对于状压dp,本质上是与dp无异的
说白了,状压dp,没有改变dp本质的优化方法,阶段还是要照分,状态还是老样子,决策依旧要做,转移方程还是得列,最优还是最优,无后性还是无后性,所以它还是比较好理解的。
本题解决:状压dp
其实上面一段话,正包含了本题的解法:状压dp
我们顺利想到了状压,但是问题又来了:如何进行dp呢?
我们不妨先来一步一步分析题目所给条件:
1.参与考古挖掘的小明得到了一份藏宝图,
藏宝图上标出了n个深埋在地下的宝藏屋,
也给出了这n个宝藏屋之间可供开发的m条道路和它们的长度。
2.小明的决心感动了考古挖掘的赞助商,
赞助商决定免费赞助他打通一条从地面到某个宝藏屋的通道,
通往哪个宝藏屋则由小明来决定。
哦,这是在说这是个有n个点的图,并且起点任意,有m条边
3.已经开凿出的道路可以任意通行不消耗代价。
每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路
所能到达的宝藏屋的宝藏。
另外,小明不想开发无用道路,
即两个已经被挖掘过的宝藏屋之间的道路无需再开发。
哦,这是在说已经访问过的节点之间不能再次连边,凡是能到达的节点的宝藏,小明都会得到
4.新开发一条道路的代价是:L*K
L代表这条道路的长度,
K代表从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的宝藏屋的数量
(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋) 。
哦,这是告诉了我们连边的代价,为L*K(意义如上所述)
那么最后要求什么呢?
5.请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,
使得工程总代价最小
那么到了这里,我们可以总结出一句话题意:
请选择合适的方案,满足已经属于同一个联通块中的两点间不会有直接相连的第二条边,
同时给定两点间连边的代价,为L*K
试求在花费最少代价的情况下使得最终所有节点在同一个联通块中
(L*K为起点到当前点的所经过的节点数乘当前边长度)
哦,明白了,其实就是找到一个连边的顺序,最终使得所有点联通同时总代价最小
那么这其中一定存在着对于每个点选或不选的状态,正好可以用上面的状压解决
我们不妨设f[s]表示状态s下,使得这些选择的点在同一个联通块中的最小代价
那么,显然有如下的转移方程:(从i点转移到j点)
f[1<<(j-1)|s]=f[s]+dis[i]*g[i][j]
其中g[i][j]表示i,j两点间道路的距离,dis[i]表示距离i根节点间的节点数
既然需要求出最优解,那么我们当然需要用到搜索的思想,枚举每一个点是否被选,用dfs实现
但是又因为这张图的起点不固定,不过没关系,我们只需要以每个点为根都进行一次dfs+状压dp就行了
到了这里,其实本题也就迎刃而解了
可能本人的思维有点跳跃,请见谅(本来就是菜,语言表达不太到位)
下面放代码,短小精悍的代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=14;
const int inf=1061109567;//0x3f
int f[1<<maxn],dis[maxn],g[maxn][maxn];
int n,m,ans=inf;
inline int dfs(int s)
{
for(int i=1;i<=n;i++){
if(1<<(i-1)&s){
for(int j=1;j<=n;j++){
if(!(1<<(j-1)&s)&&g[i][j]!=inf){
if(f[1<<(j-1)|s]>f[s]+dis[i]*g[i][j]){
int val=dis[j];
f[1<<(j-1)|s]=f[s]+dis[i]*g[i][j];
dis[j]=dis[i]+1;
dfs(1<<(j-1)|s);
dis[j]=val;
}
}
}
}
}
}
int main()
{
scanf("%d%d",&n,&m);
memset(g,0x3f,sizeof(g));
for(int i=1;i<=m;i++){
int x,y,w;
scanf("%d%d%d",&x,&y,&w);
g[x][y]=g[y][x]=min(g[x][y],w);
}
for(int i=1;i<=n;i++){
memset(dis,0x3f,sizeof(dis));
memset(f,0x3f,sizeof(f));
dis[i]=1;
f[1<<(i-1)]=0;
dfs(1<<(i-1));
ans=min(ans,f[(1<<n)-1]);
}
printf("%d",ans);
return 0;
}