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

2 years ago
#include <bits/stdc++.h>
using namespace std;
/*
DP,DP
1
2DP0
*/
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)
SSpreS
Spre(111)
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]j1p[i] |= 1 << j;p[i] += 1 << j;
使OR(|=)
p[i] += 1 << j; j1p[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, kx1kcost
cost*k()
(11)
(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;
}