宝藏
难度级别: NOI
题目描述
参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n 个深埋在地下的宝藏屋,也给出了这 n 个宝藏屋之间可供开发的 m条道路和它们的长度。 小明决心亲自前往挖掘所有宝藏屋中的宝藏。
但是,每个宝藏屋距离地面都很远,也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路则相对容易很多。小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。
在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。
已经开凿出的道路可以任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏屋之间的道路无需再开发。
新开发一条道路的代价是:
这条道路的长度 × 从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋)。
请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代价最小,并输出这个最小值。
输入格式
第一行两个用空格分离的正整数 n 和 m,代表宝藏屋的个数和道路数。
接下来 m 行,每行三个用空格分离的正整数,分别是由一条道路连接的两个宝藏屋的编号(编号为 1∼n),和这条道路的长度 v。
输出格式
输出共一行,一个正整数,表示最小的总代价。
数据范围
1≤n≤12, 0≤m≤1000, v≤5∗10^5
输入样例:
4 5
1 2 1
1 3 3
1 4 1
2 3 4
3 4 1
输出样例:
4
输入样例
4 5
1 2 1
1 3 3
1 4 1
2 3 4
3 4 2
输出样例
5
几种解题思路
思路一: 考虑到n不超过12,可以使用状态压缩DP
思路二: 模拟退火,退火算法也是贪心算法,引入随机因素,以一定的概率接受一个比当前解要差的解,并且这个概率随着时间的推移而逐渐降低。模拟退火算法具有收敛性,已在理论上被证明是一种以概率收敛于全局最优解的全局优化算法。
思路三: DFS + BFS + 剪枝优化
代码
//思路三: 搜索算法
#include<iostream>
#include<cstdio>
#include<algorithm>
//宝藏 BFS +DFS
using namespace std;
const int INF = 0x3f3f3f3f;
//bfwd: 已经被访问过的点
int bfwd[20];//按一定的顺序广度搜索 bfwd 记录访问顺序
//例,bfwd[1] =2 第1个访问的点是2号宝藏库,即开发商从地面打通到2号库
int dis[20]; //dis: 点距离开发商打通的宝藏库的距离
int cd[20]; //cd: 第i个点的出度个数
int c[20][20],ljb[20][20];
//c:费用 ,邻接矩阵
//ljb: 存下每个点的所有出度。二维数组模拟邻接表
int ans = INF;
int mincdsum, tot, cnt;
int n, m;//点 边
int p;//枚举邻接表的行,即每个节点
bool cmp(int a, int b){
return c[p][a] < c[p][b];//对出度排序
}
void dfs(int num ,int edge){
for(int i =num; i<=cnt; i++){
//按被访问的点的顺序来扩展
if(tot+ mincdsum * dis[bfwd[i]]>=ans) return;
//最优性剪枝,如果当前tot加上极限最小的费用都比 现有ans大
//这次搜索一定不是最优解,剪掉
for(int j=edge; j<=cd[bfwd[i]];j++)//按顺序枚举bfwd[i]的所有出度
if(!dis[ljb[bfwd[i]][j]]) {//未被访问的点,dis为零
cnt++;//累加访问的宝库的个数
bfwd[cnt] = ljb[bfwd[i]][j];
mincdsum -= c[bfwd[cnt]][ljb[bfwd[cnt]][1]]; //剩余未访问的点的参照出度和
tot += c[bfwd[i]][bfwd[cnt]] * dis[bfwd[i]];//加上路的成本 [bfwd[i],bfwd[cnt]]
dis[bfwd[cnt]]= dis[bfwd[i]]+ 1;//dis 加 1
dfs(i,j+1); //枚举bfwd[i]宝藏的下一个出度,广搜
tot -= c[bfwd[i]][bfwd[cnt]]* dis[bfwd[i]];//回溯
dis[bfwd[cnt]]=0;//回溯
mincdsum += c[bfwd[cnt]][ljb[bfwd[cnt]][1]];//回溯
cnt --;//回溯
}
edge =1;//从下个点出发,继续遍历下个点的所有出度,发现是否还有未访问的点
}
if(cnt ==n ){//已经访问了n个点,更新答案
if(tot < ans) ans =tot;
return;
}
}
int main(){
int u, v, w;
cin>> n>> m;
for(int i=1; i<=n;i++)
for(int j=1;j<=n;j++)
c[i][j] =INF;
for(int i=1; i<=m;i++){
cin >> u>>v>>w;
if(c[u][v]==INF)//第一次更新c[u][v],统计u,v的出度
ljb[u][++cd[u]] =v, ljb[v][++cd[v]] =u;
c[u][v]=c[v][u]=min(c[v][u],w);//构建邻接矩阵 ,去掉无用的重边
}
for(int i=1;i<=n;i++){
p =i;
sort(ljb[i]+1, ljb[i]+1+cd[i],cmp);//排序某个点的所有出度
mincdsum+=c[i][ljb[i][1]];//各点最小出度和;可参照,用于剪枝
}
for(int i=1;i<=n;i++){//枚举可能打井的点
tot =0;cnt=1;//cnt现在已经访问过的点数
bfwd[1] =i;//i为打井的点,第一个访问
mincdsum = mincdsum - c[i][ljb[i][1]];//剩余未访问的点的参照出度和
dis[i]=1;//打井的点位,距离为1
dfs(1,1);//第一个参数:bfwd数组的下标,对应的bfwd[1]为地面直达宝库。
//第二个参数:待访问的宝藏屋的出度,默认从1开始,代价从小到大
//从小到大能快速找到一个较小的答案,方便最优化剪枝
//回溯后,会调整出度访问顺序,不保证从小到大
dis[i]=0;//回溯
mincdsum+= c[i][ljb[i][1]];
}
cout << ans<<endl;
return 0;
}