最近在补全提高课所有题目的题解,宣传一下汇总的地方提高课题解补全计划
题目描述
给定一个 n 个点,m 条边且连通的 无向图
初始时,无向图中没有边,每个 点 都是 互不连通 的
我们初始可以选择任意一个点作为 起点,且该选择 起点 的操作 费用 是 0
然后开始维护这个包含起点的 连通块
每次我们选择一个与 连通块内的点 相连的一条边,使得该边的另一个点(本不在连通块内的点)加入连通块
该次选边操作的费用是:边权×该点到起点的简单路径中经过的点数
最终我们的目标是,使得 所有点都加入当前的连通块内
求解一个 方案 ,在达成目标的情况下,费用最小,输出方案的费用
分析
这题我试着预处理了一下合法状态和合法转移,结果爆MLE了
说明本题的时间复杂度也处在一个极限的情况下,暂时没有更好的优化思路了
题目要求已经连通的两点之间无须连接额外的边(这个条件其实不用给,可以推出来)
即我们只需选择 n−1 条边即可
因此,我们的目标是:找出图中的 最小生成树
但本题的 最小生成树 和广义的最小生成树不同
因为在本题中,加入连通块的费用是随当前点到起点的路径线性变化的 (设经过的点数为 k,则费用为w×k)
计算该次加边的费用是用到了如下两个参数:
- 起点
- 当前点在当前生成树上到起点的简单路径上经过的点数
如果恰好是只有其中一项限制,我们都可以直接套最小生成树的模版改一改即可
- 只有起点限制,我们可以暴力枚举起点然后套Prim即可 O(n3)
- 只有当前生成树的状态限制,我们可以额外开一个数组,记录加入生成树的点到起点经过的点数,然后dfs O(22n)
然而本题这两个参数都要考虑,就不能直接套模版来改了(不优化的话,时间复杂度要到 O(n22n) )
因此我们不妨采用 爆搜优化 -> 记忆化搜索 -> 动态规划 来解决该问题
我们把当前 生成树 的状态作为 DP 的阶段,那么需要额外记录的参数就是树的 深度
记录深度之后,可以保证我们枚举下一个阶段的状态时,能够轻易的计算他到起点的路径经过的点数
即我们把起点作为树的 root 然后一层一层构造 生成树
闫氏DP分析法
状态表示—集合fi,j: 当前生成树的状态是i (i 采用二进制压缩存储),且树的深度为 j 的方案
状态表示—属性fi,j: 该方案构成的生成树,费用最少Min
状态计算—fi,j:
fi,j=min
初始状态: f_{i,0} \quad (0\le i \lt n)
目标状态: f_{1\cdots1,j} \quad (0\le j \lt n)
其中,pre 是枚举的所有树 i 的子集,cost 则是由 pre 扩展一层后变成 i 所用的所有边的边权之和
需要注意⚠️:这里枚举 pre首先必须满足一定能够由 pre 自身的点向外扩散一层后能够变成状态 i
这算是一个简单的状态转移优化,因为如果所有状态都枚举的话,时间复杂度会退化成爆搜
可能有人看到该 DP分析 第一时间会有疑问(至少我有):
在第 j-1 到第 j 层扩展的时候,有没有可能发生一次 k-1 向 k 层的扩展 (其中 k < j)
就是深层向下扩展时,出现了一个浅层往下延伸一个点的情况
根据我们的 转移方程,会把那个浅层扩展的路径数按照当前树的深度来计算,使得该方案费用比实际更大
这种方案是实际存在的,而且也是可能成为本题最优解答案的,但是我们可以不用考虑
为什么?因为该最优解方案是可以映射到我们DP方案中的某一个方案上的
我举一个简单的例子:
f(0001_b,0) \rightarrow f(0011_b, 1) \rightarrow f(1111_b, 2)
其中点 1 是通过 1 层的点 3 延伸到第 2 层的,而点 2 是通过起点 4 延伸到第 1 层的
则该类方案都一一映射到下面这类方案下
f(0001_b,0) \rightarrow f(0111_b, 1) \rightarrow f(1111_b, 2)
也就证明了我们该类DP方案的最终答案可以是最优解
Code
时间复杂度:
- 预处理 O(2^n \times n^2)
- 动态规划 O(n^2 3^n) 借鉴 y总的分析
#include <iostream>
#include <cstring>
using namespace std;
const int N = 15, M = 1 << N, K = 1010;
const int INF = 0x3f3f3f3f;
int n, m;
int g[N][N];
int ne[M];
int f[M][K];
void init()
{
cin >> n >> m;
memset(g, 0x3f, sizeof g);
for (int i = 0; i < n; ++ i) g[i][i] = 0;
while (m -- )
{
int a, b, c;
cin >> a >> b >> c;
a --, b -- ;
g[a][b] = g[b][a] = min(g[a][b], c);
}
for (int st = 1; st < 1 << n; ++ st)//预处理所有状态能够扩展一层后得到的最大的下一层状态
for (int i = 0; i < n; ++ i)
if (st >> i & 1)
for (int j = 0; j < n; ++ j)
if (g[i][j] != INF)
ne[st] |= 1 << j;
}
int get_cost(int cur, int pre)
{
if ((ne[pre] & cur) != cur) return -1;//如果pre延伸不到cur直接剪枝
int remain = pre ^ cur;//待更新的方案
int cost = 0;//记录边权总和
for (int i = 0; i < n; ++ i)
{
if (remain >> i & 1)
{
int t = INF;//找出当前连通块内能把该点加入所用的最小边的边长
for (int j = 0; j < n; ++ j)
if (pre >> j & 1)
t = min(t, g[j][i]);
cost += t;
}
}
return cost;//返回该次扩展的费用
}
//dp
int dp()
{
memset(f, 0x3f, sizeof f);
for (int i = 0; i < n; ++ i) f[1 << i][0] = 0;//开局免费选一个起点(初始状态)
for (int cur = 1, cost; cur < 1 << n; ++ cur)
for (int pre = cur - 1 & cur; pre; pre = pre - 1 & cur)
if (~(cost = get_cost(cur, pre)))
for (int k = 1; k < n; ++ k)
f[cur][k] = min(f[cur][k], f[pre][k - 1] + cost * k);
int res = INF;
for (int k = 0; k < n; ++ k) res = min(res, f[(1 << n) - 1][k]);//目标状态中找最优解
return res;
}
int main()
{
init();
cout << dp() << endl;
return 0;
}
//只有当前生成树的状态限制,我们可以额外开一个数组,记录加入生成树的点到起点经过的点数,然后dfs O(22n)
很想知道暴力dfs怎么写呢
#include <bits/stdc++.h> using namespace std; const int INF = 1000000; // 这个上限有点意思,需要仔细思考 const int N = 15; // 点的个数上限 int g[N][N]; // 记录边,就是地图 int n, m; // n个节点,m条边 vector<int> p[N]; // 第一维:哪一行;第二维:此行第几个是哪个点 bool st[N]; // 每个点是否被用过 int res = INF; // 结果 /* * u:现在考虑第u层 * k:第几号节点 * cnt:已经使用了cnt个点 * sum:目前这个生成树的最小价值 */ void dfs(int u, int k, int cnt, int sum) { if (sum >= res) return; // 最优性剪枝,走一半就别人的最终结果大,就没有活下去的必要了 if (p[u - 1].size() == 0) return; // 上一层没有选入点(上层为空),树无法继续构建。 if (cnt == n) { // 所有点都已经被构建到树中,此时的结果有资格参选 res = min(sum, res); return; } if (k > n) { // 该层所有点都讨论过一遍,继续下一层 dfs(u + 1, 1, cnt, sum); return; } // 如果u层,k号点被标识为用过了 if (st[k]) { // 该点已用,直接讨论下一个点 dfs(u, k + 1, cnt, sum); return; } // 选用此点(可能1) st[k] = 1; // 标识k点已使用 int cost = INF; // 找到该点与上层的最小联系 for (int i = 0; i < p[u - 1].size(); i++) // 若没有联系(即与上层所有点不连通),则mini是INF,会被最优性剪枝减去 cost = min(cost, g[p[u - 1][i]][k]); // 上一行的每个节点,如果与k号节点的边相连,并且,权值最小的话,更新cost // 找到了上一层到u层,使得k点点亮的最小代价值 p[u].push_back(k); dfs(u, k + 1, cnt + 1, sum + cost * u); p[u].pop_back(); st[k] = 0; // 回溯 // 不用此点(可能2) dfs(u, k + 1, cnt, sum); } int main() { memset(g, 0x3f, sizeof g); // 数组初始化 scanf("%d %d", &n, &m); for (int i = 1; i <= m; i++) { int a, b, c; scanf("%d %d %d", &a, &b, &c); g[a][b] = g[b][a] = min(g[a][b], c); // 无向图双向赋值 } for (int i = 1; i <= n; i++) { // 每个点都当一次根 st[i] = 1; // i点已用 p[0].push_back(i); // 根看做第0层,0层第1个放的是i dfs(1, 1, 1, 0); // 从第一层,第一个节点,已经使用了一个节点(根),现在的代价是0 p[0].pop_back(); st[i] = 0; // i点不用了 } cout << res << endl; return 0; }
四国以内!
我跪!
彩铅氏dp分析法
555,刷题全靠彩铅老师
只有当前生成树的状态限制,我们可以额外开一个数组,记录加入生成树的点到起点经过的点数,然后dfs O(2^2n)这个2^(2^n)是怎么算出来的呀?
理论最大有2^n颗树,每次计算一颗树的最小费用总共时间约2^n
怎么得出有2^n棵树的?
得出树了不应该还要这么多时间计算吧
可能您想的是找到树之后去找根,我想这是O(n)的?真奇怪
我当时考虑的很有问题sorry....实际上最大有n^(n-2)棵树,共有n个节点,复杂度达到可怕的n^(n-1)
感觉你算错了
emmmm无向图中生成树的最大个数就是n^(n-2)罢
写得好,我写了一个超时的版本想后面优化. dp[x][y][st],x结尾的y层的状态st的转移过程。 写到一半才发现,状态想错了。
举报!本题题解会RE!!!
for (int pre = cur - 1 & cur; pre; pre = pre - 1 & cur)
这是怎么做到枚举每一个状态子集的。。。
首先,不&cur的话,就是从大到小枚举所有数,然后加了&,就保证了是子集(某T大佬的解释)
~(cost = get_cost(cur, pre))
这句话在代码中是什么意思呢?
看懂了
大佬你好,请问这句代码是什么意思啊
get_cost返回值赋给cost不为-1就更新
嗷嗷,好的,谢谢大佬
选定根的话不是只有n个树吗,复杂度应该是n2^n吧
树是指指数枚举的搜索树
希望有人能够解答
更正一下,好像是3^n,虽然这好像是正解复杂度,真不知道2^{2n}是怎么算的?
其中点 1 是通过 1 层的点 3 延伸到第 2 层的,而点 2 是通过起点 4 延伸到第 1 层的.
最后那句话, 点 2 不是由起点 4 延伸到第 2 层吗
确实
想不到能在这遇见你
tql! 爱了㊫
QwQ