#include using namespace std; /* 状压DP的二进制表示法,状态压缩DP有两个性质: 1、它说多大就设置多大,不要再扩大设置,速度会慢。 2、一般状态压缩DP数组下标从0开始 */ const int N = 12; // 节点个数上限 const int M = 1 << N; // 节点的状态表示 2^N种状态 const int INF = 0x3f3f3f3f; // 正无穷 int g[N][N]; // 邻接矩阵,g[i][j]:i号点和j号点之间的距离 // 记录有效状态转移关系 int p[M]; // p[x]:状态x中的存在(指值为1)每一个数位i,通过g[i][j],可以连接到j1,j2,...等节点,最终可以到达的所有状态为p[x] // 比如i=00001->0号节点值为1,0号节点与1、3号节点之间有边,那么p[i]=p[00001]=01011 int n, m; // 节点数,边数 /* 递推关系: f[S][j]=min(f[Spre][j-1]+cost) S:当前状态,Spre是指S的所有前序状态, 也就是Spre通过引入某些合法边(当前状态某个位置有1,同时,由此处的1有相关的边。不是只有一条,是数位为1的所有节点都可以引入合法边) ,可以形成S这个状态。 由于引入边的不同,导致代价不一样,需要求代价的最小值 */ int f[M][N]; // f[i][j]:所有二进制状态表示为i,且树高为j的生成树的集合,属性:最小代价 vector v[N]; // i这个点,到哪些点有边 int main() { scanf("%d %d", &n, &m); // 节点数量和边数量 // 1、地图初始化 memset(g, 0x3f, sizeof g); // 全部初始为正无穷,描述:所有节点之间均未连通 for (int i = 0; i < n; i++) g[i][i] = 0; // 每个点到自己的距离为0 // 2、读入边 while (m--) { int a, b, w; scanf("%d %d %d", &a, &b, &w); // 起点,终点,权值 a--, b--; // 状态压缩,习惯下标从0开始,题目给的节点号是从1开始的,需要统一处理减1 g[a][b] = g[b][a] = min(g[a][b], w); // 无向图 + 防止重边 + 留最小值 v[a].push_back(b), v[b].push_back(a); // 记录下a可以到达哪些点,其实如果不这么记忆的话,那么就需要枚举每个点对着哪些点,因为数据量小,速度应该也挺快 } /* 3、预处理有效状态之间的转移关系 i->p[i] 转移办法:标识为1的节点上通过枚举已知边,扩展为新状态p[i] */ for (int i = 0; i < 1 << n; i++) // 枚举每个状态 for (int j = 0; j < n; j++) // 枚举状态的每个位置 if (i >> j & 1) { // 如果当前位是1 p[i] |= 1 << j; // 原来的i状态中的1,扩展后还是有的 for (int k = 0; k < v[j].size(); k++) p[i] |= 1 << v[j][k]; // 由j带来的扩展点有v[j].size()个 } /* Q:问题: if (i >> j & 1) { p[i] |= 1 << j; for (int k = 0; k < v[j].size(); k++) p[i] |= 1 << v[j][k]; } 可以改写成: if (i >> j & 1) { p[i] += 1 << j; for (int k = 0; k < v[j].size(); k++) p[i] += 1 << v[j][k]; } 吗?它们两个是等价的吗? 答:它们并不是完全等价。 这两个表达式都是将二进制数p[i]的第j位设置为1,但是p[i] |= 1 << j;相比p[i] += 1 << j;更加高效。 这是因为使用位运算符OR(|=)可以避免临时变量和函数调用,并且可以利用位级别的并行性加快操作速度。而加法运算则需要进行进位,速度相对较慢。 此外,p[i] += 1 << j; 如果第j位之前已经是1的话,会导致进位,此时p[i]的值会发生改变,而p[i] |= 1 << j; 不会改变已有的位。 回到上面的代码中,如果我们没有写成 | 运算符,而是采用了+运算符,那么根据业务上的逻辑,p[i]的值可能首先被 for (int k = 0; k < v[j].size(); k++) p[i] += 1 << v[j][k]; 进行填充, 那么 j'=v[j][k],就会提前将对应的数值加到p[i]中去,而下次来到执行 p[i] +=1<i增加出来的元素,也就是状态差集,都是j里没有,i里有的数字位1 int cost = 0; // cost记录在状态j到达状态i过程中用到的最小花费 for (int k = 0; k < n; k++) // 枚举x每一个节点k if (x >> k & 1) { // 找出x中数位是1的节点k int t = INF; // t表示从j到i使得覆盖掉第k个点需要的最小代价 for (int u = 0; u < n; u++) // 枚举j的每一个数位u if (j >> u & 1) // 如果此数位是1,有资格引边 t = min(t, g[u][k]); // 如果k点是通过u点连接过去的,那么带来的代价就是g[u][k] cost += t; // 把x的所有元素都纳入的最小代价和 } /* 当前状态是i,前序状态是j,它们之间的差集是x, k为枚举x的每一个数字1位,为了点亮k点,最小代价是cost, 整体代价:cost*k(层数越深,挖掘成本系数越大) 迁移方式:加边(数位是1,并且,由此可以引出合法边。不只1条,可是多条。) 理解与感悟: (1) 先有状态转移方程,再有填充顺序 (2) j->i 需要枚举有效状态转移关系 (3) k-1 -> k 就是层次增加了一层 (4) k∈[1,n) 是因为树根在0层,直线的话,最后一层是n-1 (5) 上面的都好办,关键在于这个 cost,这个东西需要枚举两个转移状态之间的对应关系,结合点与点之间的距离才能计算出来 最小的代价和,最小代价和 乘以 层数,才是真正的整体代价。 */ for (int k = 1; k < n; k++) f[i][k] = min(f[i][k], f[j][k - 1] + cost * k); } } } // 5、枚举答案, 最小值可能出现在任意一层 int res = INF; for (int i = 0; i < n; i++) res = min(res, f[(1 << n) - 1][i]); //(1 << n) - 1表示全选状态,即111111... // 6、输出结果 cout << res << endl; return 0; }