一、看懂题意
给出一个无向图,要求找一棵生成树,使得代价和最小。
(1)从哪个点出发都可以。
(2)代价和有自己的计算办法:增加一个节点的代价=选择的边权*树的高度。
二、解题思路
1、状态压缩DP
看到数据范围就知道大概是个状压了,\(n\)值很小嘛,\(2^n\)数量可控!
2、状态设计原则
进行状态设计时,考虑对于每个时刻需要知道的信息,以此设计状态维度。
每个时刻,我们关心的只有我们已经把多少点加进生成树了,以及加入新的节点时的树高是多少。
设\(f[S][i]\)为:当前生成树已经包含集合\(S\)中的点,并且树高是\(i\)的情况下,花费费用的最小值。
状态转移方程:
其中\(S_{pre}\) 是\(S\)的前序子集,通过\(S_{pre}\)加边(一条或多条都可以,是状态的转化,而不是某个点、某条边的转化,是一类的转化)一定可以连接成\(S\),\(cost\)是这次加边的花费。
问题1:如何获得\(S_{pre}\)这个集合?
怎么判断\(S_{pre}\)在转移中是否合法呢?设\(G[S_{pre}]\)是\(S_{pre}\)能拓展到的状态集合,显然\(G\)是可以预处理出来的。如果\(G[S_{pre}]\)可以包含\(S\),那么就说明\(S_{pre}\)可以一步转化为\(S\)。
问题2:如何计算这个费用\(cost\)?
设\(S_{remain}=S\ xor\ S_{pre}\) ,即\(S_{remain}\)是在\(S\)中存在,但在\(S_{pre}\)中不存在的点,也就是本轮增加进来的节点。这里\(cost\)的计算显然是对于每个\(S_{remain}\)中的节点,取\(S_{pre}\)中的元素向它连一条最短的边求和后\(\times i\)。
现在来整理一下思路:
-
枚举集合,对于每个集合的子集,通过\(G[S]\)判断该子集是否合法,如果合法,枚举所有需要被连向的点的最小边权求和乘深度,作为答案。
-
答案就是全集在\(1~n\)深度的最小值。
三、实现代码
#include <bits/stdc++.h>
using namespace std;
const int N = 20; //节点个数上限
const int M = 1 << N; //节点的状态表示 2^N种状态,状压DP的二进制表示法
const int INF = 0x3f3f3f3f;//正无穷
int d[N][N]; //d[i][j]表示i号点和j号点之间边的长度,如果两点之间没有边可以连通,则长度定义为为正无穷
int n; //节点数量
int m; //边数量
int g[M]; //g[i]表示状态i+状态i一步可达的状态
int f[M][N]; //f[i][j]表示在状态i、树高度为j时的最小花费
int main() {
//优化输入
ios::sync_with_stdio(false);
//读入节点数量和边的数量
cin >> n >> m;
//1、距离数组初始化
memset(d, 0x3f, sizeof d); //初始化距离d数组为正无穷,描述最开始所有节点之间均未连通
for (int i = 0; i < n; i++) d[i][i] = 0; //自己到自己的距离是0,注意这里的数组下标是从0开始的,
// 因为状态压缩DP是基于二进制的,二进制喜欢的是下标从0开始
//2、DP数组初始化
memset(f, 0x3f, sizeof f);//为了统计最小值,初始化正无穷
//"赞助商决定免费赞助他打通一条从地面到某个宝藏屋的通道"
//选择打通哪一条都是一样的,都不要钱,所以在选择每一个结点为起点时,费用都是0
for (int i = 0; i < n; i++) f[1 << i][0] = 0;
//读入边及权值
for (int i = 1; i <= m; i++) {//m条边
int a, b, c;
cin >> a >> b >> c; //起点,终点,权值
//因为我们要用状态压缩,故所有点下标从0开始比较好,方便进行二进制运算,所以都进行了--
//若从1开始,第一个点就是2的1次方,即10,明显不好统计
a--, b--;
//(1)无向图 (2)防止重边,留最小值
d[a][b] = d[b][a] = min(d[a][b], c);
}
//思考:为什么需要引入g数组,它的存在价值是什么?
//答:经过一次预处理,以后再求i的后续可用状态时,可以实现O(1)级判定
//核心1:预处理出g数组,即i状态一步可达的状态g[i]
for (int i = 0; i < 1 << n; i++) //枚举每个状态,例:当i为1010时,表示此状态有第2、4个节点
for (int j = 0; j < n; j++) //枚举状态i中的每一位
if (i >> j & 1) { //判定状态i中第j位是否为1,即判定状态i中是否存在j这个节点
//尝试从j点出发,通过j的连接边,找到所以未加入到集合S中的点,即j节点一步可达的所有点
for (int k = 0; k < n; k++)//枚举每一个节点
if (d[j][k] != INF) //判断宝藏点j能否一步到达宝藏点k
g[i] |= 1 << k; //利用位运算,将g[i]的第k位设置为1,描述i这个状态,可以一步转移到第k位是1的状态
//这里边包含处理了j==k的情况,因为初始时,d[i][i]=0,不是INF,也是可达的
}
//状态转移
for (int i = 0; i < 1 << n; i++)//枚举每个状态
//枚举状态i的所有状态子集j,i必然是由j转化而来,但不一定是一步转化,还需要再次的检查
for (int j = (i - 1) & i; j; j = (j - 1) & i) {
//g[j]表示j能一步到达的状态
//若g[j]包含状态i的所有点,则表示状态j能一步到达状态i
if ((g[j] & i) == i) {
//因为j是i的子集,remain表示i中j的补集,
//即状态j到达状态i过程中新增的宝藏点的状态
int remain = i ^ j;//这一轮补充哪些节点
//下面是为j -> i新增的节点找到最小边的操作,同时统计最小花费
int cost = 0;//cost表示在状态j到达状态i过程中用到的最小花费
for (int k = 0; k < n; k++) //枚举remain状态每一位
if (remain >> k & 1) { //找出remain状态中的节点
int t = INF;//t表示remain中每一位(即新增的节点)到达j中某一点的最小花费
for (int u = 0; u < n; u++)//枚举j状态的每一位
if (j >> u & 1)//找出j状态中的节点
t = min(t, d[k][u]);
cost += t;
}
//状态转移
for (int k = 1; k < n; k++)//枚举树的高度
f[i][k] = min(f[i][k], f[j][k - 1] + cost * k);
}
}
//枚举答案最小值
int ans = INF;
//最小值可能出现在任意一层,比如这道题的样例,从1号点开始挖,途径1-2,1-4,4-3,最大层是2层
for (int i = 0; i < n; i++)
ans = min(ans, f[(1 << n) - 1][i]);//(1 << n) - 1表示全选状态,即111111...
//输出
printf("%d", ans);
return 0;
}