You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

125 lines
7.4 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#include <bits/stdc++.h>
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号节点值为10号节点与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<int> 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<<j 时,就会造成重复累加,结果错误。
因此,在二进制位运算时,应尽量使用位运算符。
*/
// 4、动态规划
// 4.1 DP数组初始化
memset(f, 0x3f, sizeof f); // 预求最小,先设最大
for (int i = 0; i < n; i++) f[1 << i][0] = 0; // 赞助商决定免费赞助他打通一条从地面到某个宝藏屋的通道",选择打通哪一条都是一样的都不要钱所以在选择每一个节点为起点时费用都是0
// 4.2 状态转移
for (int i = 0; i < 1 << n; i++) { // 枚举每个状态i
for (int j = i - 1; j; j = (j - 1) & i) { // 枚举i状态的真子集j,只有真子集才有资格进行转移
if ((p[j] & i) == i) { // 如果子状态j可以通过已知边扩展为可以正好匹配i,才是真正可以转移的有效子状态
int x = i ^ j; // x:j->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;
}