《P3959 [NOIP 2017 提高组] 宝藏》
题目背景
NOIP2017 D2T2
题目描述
参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n 个深埋在地下的宝藏屋, 也给出了这 n 个宝藏屋之间可供开发的 m 条道路和它们的长度。
小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是,每个宝藏屋距离地面都很远,也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路则相对容易很多。
小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。
在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以 任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏屋之间的道路无需再开发。
新开发一条道路的代价是 L×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→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≤5×103 且所有的 v 都相等。
对于 40% 的数据: 1≤n≤8,0≤m≤103,v≤5×103 且所有的 v 都相等。
对于 70% 的数据: 1≤n≤8,0≤m≤103,v≤5×103。
对于 100% 的数据: 1≤n≤12,0≤m≤103,v≤5×105。
upd 2022.7.27:新增加 50 组 Hack 数据。
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 110; // 最大节点数
const int MAXM = 2010; // 最大边数
int graph[MAXN][MAXN]; // 邻接矩阵存储图
int nodeCount, edgeCount; // 节点数和边数
int i, j, k, x, y, z; // 循环和临时变量
int minCost = INT_MAX; // 最小总代价
int startNode; // 起始节点
// 模拟退火相关变量
int parent[MAXN]; // 每个节点的父节点
int tempParent[MAXN]; // 临时父节点数组
int oldCost, newCost; // 旧代价和新代价
int visited[MAXN]; // 访问标记数组
double temperature; // 模拟退火温度
// 快速读取整数的函数
int read() {
int value = 0, sign = 1;
char c = getchar();
while((c < '0') || (c > '9')) {
if(c == '-') sign = -1;
c = getchar();
}
while((c >= '0') && (c <= '9')) {
value = value * 10 + (c - '0');
c = getchar();
}
return value * sign;
}
// 深度优先搜索检查是否形成合法树结构
int dfsCheck(int currentNode, int depth) {
if(depth > 13) return 0; // 深度过大,可能存在环
if(currentNode != startNode) {
visited[currentNode] = dfsCheck(tempParent[currentNode], depth + 1);
return visited[currentNode];
}
else return 1; // 回到起点,形成环
}
// 检查当前父节点数组是否构成合法树
int isValidTree() {
memset(visited, 0, sizeof(visited));
for(i = 1; i <= nodeCount; i++) {
if(visited[i] == 0) visited[i] = dfsCheck(i, 1);
if(visited[i] == 0) return 0; // 存在无法到达的节点
}
return 1; // 所有节点都在树中
}
// 计算当前树结构的代价
int dfsCost(int currentNode) {
if(currentNode == startNode || visited[currentNode] != 0)
return visited[currentNode];
int depth = dfsCost(tempParent[currentNode]) + 1;
visited[currentNode] = depth;
newCost += depth * graph[tempParent[currentNode]][currentNode];
return depth;
}
// 计算新的总代价
void calculateNewCost() {
newCost = 0;
memset(visited, 0, sizeof(visited));
for(i = 1; i <= nodeCount; i++) {
if(visited[i] == 0) dfsCost(i);
}
}
// 使用BFS构建初始树并计算代价
void bfsBuildTree(int start) {
int queue[MAXN], head = 1, tail = 1;
queue[head] = start;
oldCost = 0;
parent[start] = 0; // 根节点的父节点为0
// 初始化哈希数组为-1,表示未访问
int hash[MAXN];
memset(hash, -1, sizeof(hash));
hash[start] = 0;
while(head <= tail) {
int current = queue[head++];
for(i = 1; i <= nodeCount; i++) {
if(hash[i] == -1 && graph[current][i] != INT_MAX) {
hash[i] = hash[current] + 1;
queue[++tail] = i;
parent[i] = current;
oldCost += graph[current][i] * hash[i];
}
}
}
minCost = min(minCost, oldCost);
}
int main() {
// 读取输入
nodeCount = read();
edgeCount = read();
// 初始化邻接矩阵
for(i = 1; i <= nodeCount; i++) {
for(j = 1; j <= nodeCount; j++) {
if(i == j) graph[i][j] = 0;
else graph[i][j] = INT_MAX;
}
}
// 读取边信息
for(i = 1; i <= edgeCount; i++) {
x = read();
y = read();
z = read();
graph[x][y] = min(graph[x][y], z);
graph[y][x] = min(graph[y][x], z);
}
// 初始化随机数种子
srand(19260817);
// 枚举每个节点作为起点
for(i = 1; i <= nodeCount; i++) {
startNode = i;
bfsBuildTree(i);
// 节点数太少时不进行模拟退火
if(nodeCount <= 2) continue;
// 模拟退火算法
for(temperature = 10000; temperature >= 0.00001; temperature *= 0.9999) {
// 随机选择两个节点
x = (rand() % (nodeCount - 1)) + 1;
if(x >= startNode) x++; // 跳过起始节点
y = (rand() % (nodeCount - 1)) + 1;
if(y >= x) y++;
// 如果两点之间没有边,跳过
if(graph[x][y] == INT_MAX) continue;
// 备份当前父节点数组
for(j = 1; j <= nodeCount; j++)
tempParent[j] = parent[j];
// 修改边,尝试新解
tempParent[x] = y;
// 检查是否形成合法树
if(!isValidTree()) continue;
// 计算新解的代价
calculateNewCost();
// 判断是否接受新解
if(newCost <= oldCost ||
exp((oldCost - newCost) / temperature) >= ((rand() % 1000000) / 1000000.0)) {
minCost = min(minCost, newCost);
for(j = 1; j <= nodeCount; j++)
parent[j] = tempParent[j];
oldCost = newCost;
}
}
}
printf("%d\n", minCost);
return 0;
}