题目大意
参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n 个深埋在地下的宝藏屋, 也给出了这 n 个宝藏屋之间可供开发的 m 条道路和它们的长度。
小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是,每个宝藏屋距离地面都很远, 也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路 则相对容易很多。
小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某 个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。
在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以 任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路 所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏 屋之间的道路无需再开发。
新开发一条道路的代价是:
L×K
L代表这条道路的长度,K代表从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋) 。
请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代价最小,并输出这个最小值。
题解
为了这道题,上个暑假整了一下午,这个暑假整了两天,开学了以后又整了两个晚上。宝藏啊宝藏,你可坑了我不少时间啊?
这道题用状压DP会有各种逻辑问题,与其绞尽脑汁去证明,不如用暴力搜索加剪枝。
明确一点,我们要枚举的对象是生成树。
要枚举生成树,如果我们啥都不会,我们直接枚举边的组合即可。这样我们至少可以得到一点分,不至于爆零。
但这样太笨了。我们可以由Prim求最小生成树的方法得到启发。我们Dfs的对象是树,并以不断枚举From为树内点,To为树外点的边,将其纳入树中的方法得到Dfs的子问题。当当前的树是生成树时,更新答案。
下面我们考虑剪枝。
- 有重边时,只保留最小的边。
- 若当前树的边权和大于当前搜索过的最优答案,退出。
- 为了防止同一个生成树被访问多次,我们令下一条边的From节点在搜索栈中的位置必须大于前一条边的From节点在栈中的位置。
- 由3的顺序导致当前节点必然是栈中节点中距离根节点最大的点。因此我们维护每个树外点的权值最小出边的和Sum,若搜索到的树的边权和+当前节点离根的距离*Sum大于搜索过的最优答案,退出。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
#define UpdateMin(x, y) x = min(x, y)
const int MAX_NODE = 15, MAX_EDGE = 2100, INF = 0x3f3f3f3f;
int A[MAX_NODE][MAX_NODE];
int CurSum, Ans, SumInEW;
struct Node;
struct Edge;
struct Node
{
bool InTree;
int FromEdgeCnt;
int MinInEW;
Edge *Head;
Node() :MinInEW(INF) {}
}_nodes[MAX_NODE + 10];
vector<Node*> Tree;
int TotNode;
struct Edge
{
Node *To;
Edge *Next;
int Weight;
}_edges[MAX_EDGE];
int _eCount;
void AddEdge(Node *from, Node *to, int w)
{
Edge *e = _edges + _eCount++;
e->To = to;
e->Weight = w;
e->Next = from->Head;
from->Head = e;
UpdateMin(to->MinInEW, w);
}
void BuildAll()
{
for (int i = 1; i <= TotNode; i++)
for (int j = 1; j <= TotNode; j++)
if (i != j && A[i][j] < INF)
AddEdge(_nodes + i, _nodes + j, A[i][j]);
for (int i = 1; i <= TotNode; i++)
SumInEW += _nodes[i].MinInEW;
}
void Dfs(int nextId, Node *cur)//向树中加入一个from为cur的边,并枚举下一个延伸出边的节点。
{
if (CurSum + SumInEW * (cur->FromEdgeCnt + 1) >= Ans)
return;
for (Edge *e = cur->Head; e; e = e->Next)
{
if (!e->To->InTree)
{
int delta = e->Weight * (cur->FromEdgeCnt + 1);
if (CurSum + delta < Ans)
{
CurSum += delta;
e->To->InTree = true;
e->To->FromEdgeCnt = cur->FromEdgeCnt + 1;
SumInEW -= e->To->MinInEW;
Tree.push_back(e->To);
if (Tree.size() == TotNode)
UpdateMin(Ans, CurSum);
else
for (unsigned int i = nextId; i < Tree.size(); i++)
Dfs(i, Tree[i]);
CurSum -= delta;
e->To->InTree = false;
e->To->FromEdgeCnt = 0;
SumInEW += e->To->MinInEW;
Tree.pop_back();
}
}
}
}
int main()
{
int totEdge;
scanf("%d%d", &TotNode, &totEdge);
if (TotNode == 1)
{
printf("0\n");
return 0;
}
memset(A, INF, sizeof(A));
Tree.reserve(20);
for (int i = 1; i <= totEdge; i++)
{
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
UpdateMin(A[u][v], w);
UpdateMin(A[v][u], w);
}
BuildAll();
Ans = INF;
for (int i = 1; i <= TotNode; i++)
{
SumInEW -= _nodes[i].MinInEW;
_nodes[i].InTree = true;
Tree.push_back(_nodes + i);
Dfs(0, _nodes + i);
_nodes[i].InTree = false;
Tree.pop_back();
SumInEW += _nodes[i].MinInEW;
}
printf("%d\n", Ans);
return 0;
}