【题目链接】
ybt 2134:【25CSPS提高组】道路修复
洛谷 P14362 [CSP-S 2025] 道路修复
注:洛谷P14362的评测时间为2s。ybt 2134的评测时间为1s,更加严格。
【题目考点】
1. 图论:最小生成树 kruskal算法
2. 深搜回溯:深搜子集树
【解题思路】
原图中每个城市是一个顶点,每个乡镇是可选顶点 ,对乡镇进行城市化改造的费用可以认为是点权 (顶点权值),修复一条道路的费用为边权 (边的权值)。
原图指的是由城市顶点构成的图,不包括可选顶点。
可选顶点有 k k k个, k ≤ 10 k\le 10 k≤10,即可选顶点的数量不超过10个。
已知:"任意两座城市都能通过若干条道路相互到达",即原图是连通图。
本题求:将原有的 n n n座城市两两连通的最小费用,如果只考虑原图,需要找到原图的一个子图,该子图需要包含原图所有顶点,是连通图,且边权加和最小。显然,该子图是原图的最小生成树 。
本题难点在于如何处理可选顶点。
1. 非正解 解法:
1) 针对性质A的解法
对于测试数据的性质A:对于所有 1 ≤ j ≤ k 1 \leq j \leq k 1≤j≤k,均有 c j = 0 c_j = 0 cj=0 且均存在 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n 满足 a j , i = 0 a_{j,i} = 0 aj,i=0。
即每个可选顶点的点权为0,且每个可选顶点都与原图顶点存在一条权值为0的边。
可选顶点的点权为0,选择任意数量的可选顶点不会增加总费用。
那么如果选择了所有可选顶点,那么所有可选顶点与原图顶点之间的边也都算作图中的边。在执行kruskal算法的过程中,会先选择权值最小的边,也就会先选择可选顶点与原图顶点之间的权值为0的边,选择这些边不会增加总费用。
少选一些可选顶点 与 选择所有可选顶点的点权费用是一样的。多选择可选顶点,最差情况下也是连一条权值为0的边到原图,不会增加费用。而多选择可选顶点还会提供更多的可选择的边,更有利于构造出权值加和更小的生成树。
再继续进行kruskal算法,求出原图加上所有可选顶点构成的图的最小生成树。该最小生成树的边权加和即为本题的结果。
时间复杂度分析:
由于得到的图的总边数为 m + k n m+kn m+kn,使用kruskal算法的时间复杂度瓶颈是快排。
总体时间复杂度: O ( ( m + k n ) log ( m + k n ) ) O((m+kn)\log (m+kn)) O((m+kn)log(m+kn))
2) 搜索子集+kruskal
可选顶点不超过10个,则可以用深搜求集合子集的方法,搜索确定可选顶点的所有子集。
在确定了可选顶点有哪些后,总费用先加上每个可选顶点的点权,而后对原图顶点以及选出的可选顶点构成的图建立最小生成树,求最小生成树的边权加和。求所有出现过的最小生成树的边权加和的最小值,即为本题的结果。
时间复杂度分析:
搜求集合子集的时间复杂度为 O ( 2 k ) O(2^k) O(2k),确定的图的边数为 m + k n m+kn m+kn,进行排序的时间复杂度为 O ( ( m + k n ) log ( m + k n ) ) O((m+kn)\log (m+kn)) O((m+kn)log(m+kn))。
总体时间复杂度为: O ( 2 k ( m + k n ) log ( m + k n ) ) O(2^k(m+kn)\log (m+kn)) O(2k(m+kn)log(m+kn))
其中边数 m m m最大为 10 6 10^6 106, k k k最大为10, 2 k 2^k 2k最大为 1024 1024 1024,因此该算法的时间复杂度会达到 10 9 10^9 109量级,一定会超时。
整合以上两种方法,得到的非正解代码提交后可以得到56pt。
2. 正解 解法1:深搜求子集
非正解解法的主要性能瓶颈在于 m m m达到了 10 6 10^6 106,每次执行kruskal算法都要对最多 10 6 10^6 106条边进行排序。
而kruskal算法只取权值最小的,不会成环的 n − 1 n-1 n−1条边。
思考实际可能用到的原图中的边,会得到以下结论:
性质1 :原图顶点添加一些可选顶点及边后,再执行kruskal算法,不会选择原图中不属于原图最小生成树的边。
反证法:
如果原图顶点添加一些可选顶点及边后,再执行kruskal算法,选择了原图中不属于原图最小生成树的边e。
在新构成的图的最小生成树中去掉边e,整个图分成两个连通分量。
两个连通分量中原图的顶点分为:顶点集合A、顶点集合B。
原图的最小生成树中,一定存在一条边t,该边连接的两个顶点分属于集合A和集合B。
在原图求最小生成树时选择了t没有选择e
情况1:t的权值比e的权值小
在新构成的图的最小生成树中去掉e,选择边t,得到的子图的权值加和比最小生成树的权值加和更小,产生矛盾。
情况2:在原图执行kruskal算法时,选择e会产生环。
设e连接顶点x、y,那么原图中存在一条从x到y的路径,该路径上每条边的权值都小于e。如果一定要选择一条边使x、y连通,那么可以选择该x到y路径上的一条比e权值更小的边,不需要选择e。得到的子图的权值加和比最小生成树的权值加和更小,产生矛盾。
因此原命题得证。
因此,可以先对原图求最小生成树,只保留最小生成树中的边,有 n − 1 n-1 n−1条边。将这些边添加到集合 g g g中。
为了避免多次排序,可以先将所有可选顶点和原图顶点之间的边添加到集合 g g g中,最多 k n kn kn条边。此时该集合 g g g的元素数量为 O ( k n ) O(kn) O(kn)量级,将集合 g g g用顺序表保存,按照边的权值进行升序排序。
深搜可选顶点的所有子集,确定可选顶点,统计选择的顶点的点权加和。
执行kruskal算法时,不需要进行排序。直接遍历有序的顺序表 g g g,对于从 g g g中取到的每条边,如果该边所连的顶点有未选择的可选顶点,就略过该边。而后正常执行kruskal算法,求最小生成树的边权加和。求所有出现过的最小生成树的边权加和的最小值,即为本题的结果。
2. 正解 解法2:状态压缩 枚举状态
正解解法1中深搜求可选顶点所有子集的过程,也可以替换为对二进制集合状态进行枚举所有状态的过程。
该方法中,可选顶点的编号从0到k-1。
设集合状态 s s s: s s s在二进制下第 i i i位为1表示选择了第 i i i个可选顶点。为0表示没有选择第 i i i个可选顶点。
将集合状态从0遍历到 2 k 2^k 2k(即1<<k),统计其中选择的可选顶点的点权加和。根据集合状态也可以确定一个可选顶点是否已被选择。
其余流程与正解解法1相同。
以上两种解法的时间复杂度分析:
对原图求最小生成树: O ( m log m ) O(m\log m) O(mlogm)
对原图最小生成树的边和可选顶点与原图顶点的边构成的顺序表排序: O ( k n log ( k n ) ) O(kn\log(kn)) O(knlog(kn))
枚举(或搜索)可选顶点的所有子集: O ( 2 k ) O(2^k) O(2k)
对于每种可选顶点的选择情况执行不排序的kruskal,由于顶点数量最多 k + n k+n k+n个,选择的边的数量最多为 k + n − 1 k+n-1 k+n−1,因此时间复杂度为: O ( k + n ) O(k+n) O(k+n)
整体时间复杂度: O ( m log m + k n log ( k n ) + 2 k ( k + n ) ) O(m\log m+kn\log(kn)+2^k(k+n)) O(mlogm+knlog(kn)+2k(k+n))
其中瓶颈为 2 k ( k + n ) 2^k(k+n) 2k(k+n), k k k最大为10,n最大为 10 4 10^4 104, 2 k ( k + n ) 2^k(k+n) 2k(k+n)最大为 10 7 10^7 107量级,1s内可以通过。
【题解代码】
非正解 解法1:性质A,搜索子集+kruskal (56pt)
时间复杂度: O ( 2 k ( m + k n ) log ( m + k n ) ) O(2^k(m+kn)\log (m+kn)) O(2k(m+kn)log(m+kn))
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 100015, K = 15;
struct Edge
{
int u, v, w;
bool operator < (const Edge &b) const
{
return w < b.w;
}
};
bool propertyA = true;
int n, m, k, fa[N], c[K], a[K][N];//a[i][j]:第i乡镇到第j城市的费用
LL ans = 1e18;
vector<Edge> g;
void init(int n)
{
for(int i = 1; i <= n; ++i)
fa[i] = i;
}
int find(int x)
{
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y)
{
fa[find(x)] = find(y);
}
LL kruskal(vector<Edge> tg)//tg是传入参数g的复制
{
LL res = 0;
init(n+k);
sort(tg.begin(), tg.end());
for(Edge e : tg)
{
int u = e.u, v = e.v, w = e.w;
if(find(u) != find(v))
{
res += w;
merge(u, v);
}
}
return res;
}
void dfs(int i, LL sum)//搜索确定选择哪些乡镇进行改造
{
if(i > k)
{
ans = min(ans, sum+kruskal(g));
return;
}
dfs(i+1, sum);
for(int j = 1; j <= n; ++j)
g.push_back(Edge{i+n, j, a[i][j]});//第i乡镇当做顶点n+i
dfs(i+1, sum+c[i]);
for(int j = 1; j <= n; ++j)
g.pop_back();
}
int main()
{
int u, v, w;
cin >> n >> m >> k;
for(int i = 1; i <= m; ++i)
{
cin >> u >> v >> w;
g.push_back(Edge{u, v, w});
}
for(int i = 1; i <= k; ++i)
{
cin >> c[i];
if(c[i] > 0)
propertyA = false;
for(int j = 1; j <= n; ++j)
cin >> a[i][j];
}
if(propertyA)
{//如果有性质A,跑kruskal,那么会先选所有权值为0的边,把所有乡镇顶点都连到城市顶点,而且无花费。
for(int i = 1; i <= k; ++i)
for(int j = 1; j <= n; ++j)
g.push_back(Edge{i+n, j, a[i][j]});
ans = kruskal(g);
}
else
dfs(1, 0);
cout << ans;
return 0;
}
正解 解法1:深搜求子集
时间复杂度: O ( m log m + k n log ( k n ) + 2 k ( k + n ) ) O(m\log m+kn\log(kn)+2^k(k+n)) O(mlogm+knlog(kn)+2k(k+n))
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 10015, K = 15;
struct Edge
{
int u, v, w;
bool operator < (const Edge &b) const
{
return w < b.w;
}
};
int n, m, k, fa[N], c[K];
bool used[K];
LL ans = 1e18;
vector<Edge> og, g;
void init(int n)
{
for(int i = 1; i <= n; ++i)
fa[i] = i;
}
int find(int x)
{
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y)
{
fa[find(x)] = find(y);
}
void kruskal_init()
{
init(n);
sort(og.begin(), og.end());
for(Edge e : og)
{
int u = e.u, v = e.v, w = e.w;
if(find(u) != find(v))
{
g.push_back(e);//使e中只保留原图最小生成树的边
merge(u, v);
}
}
}
LL kruskal(int cn)//cn:选择了cn个乡镇顶点
{
LL res = 0, edgeNum = 0;
init(n+k);
for(Edge e : g)//不用排序,此时g是有序的
{
int u = e.u, v = e.v, w = e.w;
if(u > n && !used[u-n] || v > n && !used[v-n])//如果u或v是没有被选择的乡镇,则略过
continue;
if(find(u) != find(v))
{
res += w;
merge(u, v);
if(++edgeNum == n+cn-1)//当前共有n+cn个顶点
break;
}
}
return res;
}
void dfs(int i, LL sum, int cn)//当前看第i个乡镇,乡镇建设总费用sum,选择了cn个乡镇
{
if(i > k)
{
ans = min(ans, sum+kruskal(cn));
return;
}
dfs(i+1, sum, cn);
used[i] = true;
dfs(i+1, sum+c[i], cn+1);
used[i] = false;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int u, v, w;
cin >> n >> m >> k;
for(int i = 1; i <= m; ++i)
{
cin >> u >> v >> w;
og.push_back(Edge{u, v, w});
}
kruskal_init();//g中只保留最小生成树中的边
for(int i = 1; i <= k; ++i)
{
cin >> c[i];
for(int j = 1; j <= n; ++j)
{
cin >> w;
g.push_back(Edge{i+n, j, w});
}
}
sort(g.begin(), g.end());
dfs(1, 0, 0);
cout << ans;
return 0;
}
正解 解法2:状态压缩 枚举状态
时间复杂度: O ( m log m + k n log ( k n ) + 2 k ( k + n ) ) O(m\log m+kn\log(kn)+2^k(k+n)) O(mlogm+knlog(kn)+2k(k+n))
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 10015, K = 15;
struct Edge
{
int u, v, w;
bool operator < (const Edge &b) const
{
return w < b.w;
}
};
int n, m, k, fa[N], c[K];
LL ans = 1e18;
vector<Edge> og, g;
void init(int n)
{
for(int i = 1; i <= n; ++i)
fa[i] = i;
}
int find(int x)
{
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y)
{
fa[find(x)] = find(y);
}
void kruskal_init()
{
init(n);
sort(og.begin(), og.end());
int edgeNum = 0;
for(Edge e : og)
{
int u = e.u, v = e.v, w = e.w;
if(find(u) != find(v))
{
g.push_back(e);//使e中只保留原图最小生成树的边
merge(u, v);
if(++edgeNum == n-1)
break;
}
}
}
LL solve(int sta)//sta:乡镇顶点集合状态 返回选择边权的最小加和
{
LL res = 0, cn = 0, edgeNum = 0;
init(n+k);
for(int i = 0; i < k; ++i) if(sta & 1<<i)//统计选择的乡镇顶点的数量
{
cn++;
res += c[i];
}
for(Edge e : g)//不用排序,此时g是有序的
{
int u = e.u, v = e.v, w = e.w;
if(u > n && !(sta & 1<<u-n-1) || v > n && !(sta & 1<<v-n-1))//如果u或v是没有被选择的乡镇,则略过
continue;
if(find(u) != find(v))
{
res += w;
merge(u, v);
if(++edgeNum == n+cn-1)//当前共有n+cn个顶点
break;
}
}
return res;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int u, v, w;
cin >> n >> m >> k;
for(int i = 1; i <= m; ++i)
{
cin >> u >> v >> w;
og.push_back(Edge{u, v, w});
}
kruskal_init();//g中只保留最小生成树中的边
for(int i = 0; i < k; ++i)//乡镇下标从0开始
{
cin >> c[i];
for(int j = 1; j <= n; ++j)
{
cin >> w;
g.push_back(Edge{i+n+1, j, w});
}
}
sort(g.begin(), g.end());
for(int i = 0; i < 1<<k; ++i)
ans = min(ans, solve(i));
cout << ans;
return 0;
}