算法
(状态压缩DP) $O(n^23^n)$
参考这篇题解所写。
状态压缩DP,下文中i
是一个 $n$ 位二进制数,表示每个点是否存在。
状态f[i][j]
表示:
- 集合:所有包含
i
中所有点,且树的高度等于j
的生成树 - 属性:最小花费
状态计算:枚举i
的所有非全集子集S
作为前j - 1
层的点,剩余点作为第j
层的点。
核心: 求出第j
层的所有点到S
的最短边,将这些边权和乘以j
,直接加到f[S][j - 1]
上,即可求出f[i][j]
。
证明:
将这样求出的结果记为f'[i][j]
f[i][j]
中花费最小的生成树一定可以被枚举到,因此f[i][j] >= f'[i][j]
;- 如果第
j
层中用到的某条边(a, b)
应该在比j
小的层,假设a
是S
中的点,b
是第j
层的点,则在枚举S + {b}
时会得到更小的花费,即这种方式枚举到的所有花费均大于等于某个合法生成树的花费,因此f[i][j] <= f'[i][j]
所以有 f[i][j] = f'[i][j]
。
时间复杂度
包含 $k$ 个元素的集合有 $C_n^k$ 个,且每个集合有 $2^k$ 个子集,因此总共有 $C_n^k2^k$ 个子集。$k$ 可以取 $0 \sim n$,则总共有 $\sum_{k=0}^n C_n^k2^k = (1+2)^n = 3^n$,这一步由二项式定理可得。
对于每个子集需要 $n^2$ 次计算来算出剩余点到子集中的最短边。
因此总时间复杂度是 $O(n^23^n)$。
C++ 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12, M = 1 << 12, INF = 0x3f3f3f3f;
int n, m;
int d[N][N];
int f[M][N], g[M];
int main()
{
scanf("%d%d", &n, &m);
memset(d, 0x3f, sizeof d);
for (int i = 0; i < n; i ++ ) d[i][i] = 0;
while (m -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
a --, b --;
d[a][b] = d[b][a] = min(d[a][b], c);
}
for (int i = 1; i < 1 << n; i ++ )
for (int j = 0; j < n; j ++ )
if (i >> j & 1)
{
for (int k = 0; k < n; k ++ )
if (d[j][k] != INF)
g[i] |= 1 << k;
}
memset(f, 0x3f, sizeof f);
for (int i = 0; i < n; i ++ ) f[1 << i][0] = 0;
for (int i = 1; i < 1 << n; i ++ )
for (int j = (i - 1) & i; j; j = (j - 1) & i)
if ((g[j] & i) == i)
{
int remain = i ^ j;
int cost = 0;
for (int k = 0; k < n; k ++ )
if (remain >> k & 1)
{
int t = INF;
for (int u = 0; u < n; u ++ )
if (j >> u & 1)
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 res = INF;
for (int i = 0; i < n; i ++ ) res = min(res, f[(1 << n) - 1][i]);
printf("%d\n", res);
return 0;
}
这题鸽了两年了窝草。。。。
不是3年吗
错啦,已经四年了
这次我要把失去的全拿回来
爆照?帅!(虽说我之前也在头像上爆过照
doge好东西
更新了更新了
又错啦,现在是5年了
不是更新了吗
y总为什么不出视频
这个题视频讲解更新下
没视频只能硬看代码了
https://www.acwing.com/video/4994/
非全集子集=真子集
y总催更视频
https://www.acwing.com/video/4994/
这个没报s组辅导课的也能看吗?不太清楚诶
可以
y总这题有重边?好像没说啊啊
哦,我明白了,$n\leq 12$,没有重边 $m$ 不可能到达 $1000$ 啊
g[]数组表示什么意思啊
g[j]&i==i表示j状态能到i状态转移
y总枚举子集那里写错了哦,
for(int j = (i - 1); j; j = (j - 1) & i)
中应该把(i - 1)
改为i
或者改为i - 1 & i
,枚举的子集才是正确的。没有看懂呀,到底怎么该怎么枚举子集呢,抽风大佬可以解释一下吗
这个就是按二进制位去算,你可以按代码手动模拟一下。
比如
i = (1101)2
的时候,S
就会枚举到:1101
、1100
、1001
、1000
、0101
、0100
、0001
、0000
这 $8$ 中情况,恰好对应了i
的子集。然后代码背一下就可以了。
至于怎么想到这么写,我就不知道了这里枚举的应该是i的非全集子集吧,在i==j时remain为空,不需要更新,不必枚举
对的。但是
j = i - 1
是错误的,枚举i
的非全集子集应该是j = i - 1 & i
。确实应该是 $j = i - 1 & i$.
太神奇了
感觉想到这么写,应该是借鉴了
lowbit
的思路。个人觉得枚举子集这里和基础课里 二进制中1的个数 这一题的思路很像。因为子集状态必然是小于本身的,所以-1,然后&上本身就可以取得最大的自己状态
对滴,多谢指正,已修正。
f[i][j]中花费最小的生成树一定可以被枚举到,因此f[i][j] >= f’[i][j];
这个推导怎么来的啊。 f[i][j] 可以连接任意边 , f’[i][j]是只连接上一层的边,是前者的子集。 那么应该是f[i][j] <= f’[i][j].
终究是错付了
$\sum\limits_{k = 0}^{n}C_{n}^{k}2^{k} = \(2 + 1\)^{n}$ 这一步是怎么推导出来的?
根据二项式定理的话,不应该是 $\(2 + 1\)^{n} = \sum\limits_{k = 0}^{n}C_{n}^{k}2^{k}C_{n}^{n - k}1^{n - k}= \sum\limits_{k = 0}^{n}C_{n}^{k}2^{k}C_{n}^{n - k}$
为什么二项式展开要再乘$C_n^{n - k}$
催更催更
https://www.acwing.com/video/4994/
需要视频!!
https://www.acwing.com/video/4994/
催更催更hhh
https://www.acwing.com/video/4994/
y总有视频吗
https://www.acwing.com/video/4994/
模拟退火调了2个小时也只能过22个数据最后一个太难搞了
y总最后一个数据是不是专门来卡模拟退火的啊
模拟退火:
https://paste.ubuntu.com/p/XDWv4PBFT4/
int j = (i - 1);这里是不是少了个& i,感觉不太对
这里不能加
&i
,亲测加上就错了hhi = 1000, j = (i - 1) = 111,这个j并不是i的子集啊,应该要加&i的吧!
我加了之后可以过
加不加都能过……
奇怪。
正确的应该就是i - 1 & i,当i为偶数的情况,例如1010(二进制),如果不加就变成了1001,但是1001并不是1010的子集
需要加的,y总这里说错了。
y总为什么f数组初始化时只能设成0x3f不能设成127
状态转移计算的时候为什么能把所有j中的点都当做深度是k-1的点呢?
我明白了,如果说是第k层的点a距离K-2层的点b的距离是1,距离k-1层点c的距离是2。按照我们的分析,a的代价是2 * k, 我们的代码计算出来的结果是1*k, 但是最优解是把a放到第k-1层去, 代价是1 * (k - 1)。所以, 即使有一部分解的代价算的比真实代价小,也不会小过最优解的代价, 并不会影响答案。
不错,也可以参照上面题解中的证明部分。
能不能画个图,展示一下,我还是没搞明白
闫总你这个证明我也没搞懂
说实话,题意我都有点懵逼,抽象不出来求最小生成树,给的样例本来都连通,为啥要求…
样例是连通的,但不是每条边都一定要选择的。只选对的,不选贵的,最终就能达到把所有节点找全,但成本最少的方法,让我们输出最小的代价。
没有图的证明看不懂呀