2025CSP-S组 T2 部分分做法
S组 T2
部分分分析很重要,如果全部分类写出,可以得72分,虽没想出最后正解,但也很高了
想得到满分,就要注意到n比m小, k<=10, 也很小。这两个就是瓶颈。
P14362 [CSP-S 2025] 道路修复 / road(官方数据)
【解析】
16 分:(k ≤ 0)
\(此时 k ≤ 0,不需要考虑乡镇对城市带来的影响。因此可以直接使用 kruskal 算法来计算出最小生成树的边权总和即可。具体地,将 m 条边按照边权从小到大排序,然后依次考虑连接每一条边后是否会形成环。若不形成环,则加入这条边;否则不加入。判断是否形成环,可以使用并查集。时间复杂度 O (m log m)。\)
另外 32 分:
\((特殊性质 A)此时 c_j = 0,且对于每个乡镇 j,都存在一个城市 i 满足 a_{j,i} = 0。这说明将任何一个乡镇都可以通过一个边权为 0 的边加入到一个已经连通的图中,即需要付出的代价是 0。而如果是中途将乡镇与未连通图中的某个连通块进行连接(前提是这个连通块中存在与乡镇边权为 0 的城市),然后再通过城市之间的边将每个城市进行连通,那么仍然能够发现使所有乡镇与城市连通需要的代价还是 0。 容易得到一个贪心的想法:既然任何一个时刻将乡镇与城市进行连通的代价都是 0,那么不妨一开始就让所有乡镇与它对应的 0 代价城市相连接。这样一来,就可以在一开始不费任何代价地产生尽可能多的边权更小的边,这些边都是乡镇与城市之间的边,用于原图中的城市相互之间进行连接。具体地,对于乡镇 j 进行如下额外的连边: 令乡镇的编号为 n + j,城市的编号为 i (1 ≤ i ≤ n); 那么在 n + j 与 i 之间连接一条边权为 a_{j,i} 的边。 随后,将这些额外的边与原本城市之间的边一同排序,进行一次 kruskal 算法即可计算出答案,注意使用 long long。设考虑到 m > nk,且 n > k,时间复杂度依然是 O (m log m)。 \)
另外 24 分:(k ≤ 5)
\( 注意到 k ≤ 5 很小,可以考虑 dfs 枚举每个乡镇是否使用。具体的做法与上述 “特殊性质 A” 的做法类似,对于枚举到的每一种使用乡镇的情况,只需要把用到的乡镇对应的边额外加入进来即可,以便产生权值更小的边用于城市之间的连通。dfs 部分的复杂度是 O (2^k),总时间复杂度是 O (2^k m log m)。 \)
100 分:
\(复杂度的瓶颈在于每次都将 m 条边重新排序了,需要考虑将 O (m log m) 的排序从 2^k 中提取出来。不难发现,m ≤ 10^6 虽然很大,但是 n ≤ 10^4 比较小,能够构成生成树的边至多有 O (n) 条。于是我们可以先对城市之间的 m 条边进行一次 kruskal,提取出其中有用的 n-1 条树边。 这样一来对于 dfs 枚举到的每一种乡镇情况,总边数就是 O (nk) 级别而非 O (m + nk),时间复杂度优化到了 O (m log m + 2^k nk log (nk))。然而,这样的复杂度可能还不足以通过本题。 考虑到每次还是会对边重新排序,为了避免这种情况,我们其实完全可以在 dfs 之前,提前将所有的 nk 条边与 n-1 条树边放在一起排序。为每条边标记上这条边来自于哪一个乡镇,这样一来,每次只需要枚举 nk + n - 1 条边,根据边的标记来判断对应的乡镇当前是否可以使用,进而判断当前的这条边是否可以选上。时间复杂度进一步降到了 O (m log m + 2^k nk)。 \)
16 分( \(k \le 0\) ):只需要对原有 \(m\) 条边做一次 Kruskal
思路简述:当 \(k\le0\) 时没有乡镇的影响,问题退化为对 \(n\) 个城市在给定 \(m\) 条边权下求最小生成树(MST)。经典 Kruskal:把所有边按权排序,逐条加入并查集(若不构成环则加入),最终取权和。
// 16_points.cpp
// 16 分解法:k <= 0 时,直接对 m 条边做 Kruskal 求 MST
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct DSU {
int n;
vector<int> p, sz;
DSU(int n=0){ init(n); }
void init(int _n){
n = _n;
p.resize(n+1);
sz.assign(n+1,1);
for(int i=1;i<=n;i++) p[i]=i;
}
int find(int x){ return p[x]==x?x:p[x]=find(p[x]); }
bool unite(int a,int b){
a=find(a); b=find(b);
if(a==b) return false;
if(sz[a]<sz[b]) swap(a,b);
p[b]=a; sz[a]+=sz[b];
return true;
}
};
struct Edge {
int u,v;
ll w;
Edge(){}
Edge(int _u,int _v,ll _w):u(_u),v(_v),w(_w){}
};
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,m,k;
if(!(cin>>n>>m>>k)) return 0;
vector<Edge> edges;
edges.reserve(m);
for(int i=0;i<m;i++){
int u,v; ll w;
cin>>u>>v>>w;
edges.emplace_back(u,v,w);
}
// k <= 0 时忽略乡镇输入(若有),但题中会给 k,这里按约定 k<=0 时没有乡镇数据
sort(edges.begin(), edges.end(), [](const Edge& a,const Edge& b){ return a.w < b.w; });
DSU dsu(n);
dsu.init(n);
ll ans = 0;
int cnt = 0;
for(auto &e: edges){
if(dsu.unite(e.u, e.v)){
ans += e.w;
++cnt;
if(cnt == n-1) break;
}
}
cout << ans << '\n';
return 0;
}
32 分(特殊性质 A):每个乡镇有 \(c_j=0\) 且存在某城市 \(i\) 使得 \(a_{j,i}=0\)
思路简述:既然所有乡镇的城市化成本 \(c_j=0\) ,且每个乡镇与至少一个城市的连接代价为 0,那么没有必要枚举选择与否——把所有乡镇视作“免费可用”节点,把乡镇与所有城市之间的边(权为 \(a_{j,i}\) )全部加入图,然后对整个图做一次 Kruskal。并查集大小为 \(n+k\) ,最后只需关心把原有 \(n\) 个城市连通,乡镇作为辅助节点自动参与。
// 32_points_specialA.cpp
// 32 分解法(特殊性质 A):c_j = 0 且每个乡镇存在一个 a_{j,i} = 0
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct DSU {
int n;
vector<int> p, sz;
DSU(int n=0){ init(n); }
void init(int _n){
n = _n;
p.resize(n+1);
sz.assign(n+1,1);
for(int i=1;i<=n;i++) p[i]=i;
}
int find(int x){ return p[x]==x?x:p[x]=find(p[x]); }
bool unite(int a,int b){
a=find(a); b=find(b);
if(a==b) return false;
if(sz[a]<sz[b]) swap(a,b);
p[b]=a; sz[a]+=sz[b];
return true;
}
};
struct Edge {
int u,v;
ll w;
Edge(){}
Edge(int _u,int _v,ll _w):u(_u),v(_v),w(_w){}
};
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,m,k;
if(!(cin>>n>>m>>k)) return 0;
vector<Edge> edges;
edges.reserve(m + (ll)k * n);
for(int i=0;i<m;i++){
int u,v; ll w; cin>>u>>v>>w;
edges.emplace_back(u,v,w);
}
vector<ll> c(k);
vector<vector<ll>> a(k, vector<ll>(n+1));
for(int j=0;j<k;j++){
cin>>c[j];
for(int i=1;i<=n;i++) cin>>a[j][i];
}
// 因为 c_j = 0,且至少存在 a[j][i]=0(题设保证),我们把所有乡镇节点以及它们与城市之间的边都加入
// 乡镇编号使用 n+1 .. n+k
for(int j=0;j<k;j++){
int town = n + 1 + j;
for(int i=1;i<=n;i++){
edges.emplace_back(town, i, a[j][i]);
}
}
sort(edges.begin(), edges.end(), [](const Edge& A, const Edge& B){ return A.w < B.w; });
DSU dsu(n+k);
dsu.init(n+k);
ll ans = 0;
for(auto &e: edges){
if(dsu.unite(e.u, e.v)){
ans += e.w;
}
}
cout << ans << '\n';
return 0;
}
24 分( \(k \le 5\) ):对乡镇选择做枚举(DFS / 位掩码),每种情况做 Kruskal(朴素版)
思路简述:因为 \(k\le5\) ,可以枚举每个乡镇是否“被选用”(共 \(2^k \le 32\) 种)。对于每个掩码,构造边集合:原有 \(m\) 条边 + 对被选乡镇加入其与所有城市的 \(n\) 条边(乡镇视作节点 \(n+j\) ),然后对该集合排序并 Kruskal。最后答案为所有掩码的最小值。注意用 long long,每种掩码独立做排序(实现简单但在极端 m 较大时会慢——但题目这档给分中允许此做法)。
提示:该实现直接重新排序 \(m + s\cdot n\) 条边每次,适用于 \(k\le5\) 的情况。
// 24_points_k_le_5.cpp
// 24 分解法:k <= 5,枚举乡镇是否使用(位掩码),每个掩码构造边集并 Kruskal
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct DSU {
int n;
vector<int> p, sz;
DSU(int n=0){ init(n); }
void init(int _n){
n = _n;
p.resize(n+1);
sz.assign(n+1,1);
for(int i=1;i<=n;i++) p[i]=i;
}
int find(int x){ return p[x]==x?x:p[x]=find(p[x]); }
bool unite(int a,int b){
a=find(a); b=find(b);
if(a==b) return false;
if(sz[a]<sz[b]) swap(a,b);
p[b]=a; sz[a]+=sz[b];
return true;
}
};
struct Edge {
int u,v;
ll w;
Edge(){}
Edge(int _u,int _v,ll _w):u(_u),v(_v),w(_w){}
};
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,m,k;
if(!(cin>>n>>m>>k)) return 0;
vector<Edge> baseEdges;
baseEdges.reserve(m);
for(int i=0;i<m;i++){
int u,v; ll w; cin>>u>>v>>w;
baseEdges.emplace_back(u,v,w);
}
vector<ll> c(k);
vector<vector<ll>> a(k, vector<ll>(n+1));
for(int j=0;j<k;j++){
cin>>c[j];
for(int i=1;i<=n;i++) cin>>a[j][i];
}
ll answer = (1LL<<62);
int maxMask = 1<<k;
// 枚举所有掩码
for(int mask=0; mask<maxMask; ++mask){
// 1. 构造当前掩码下的边集合:先把原始 m 条边放入
vector<Edge> edges = baseEdges;
// 2. 对于选中的乡镇 j,加入其与所有城市的 n 条边(乡镇编号 n+j+1)
ll sumC = 0;
for(int j=0;j<k;j++){
if(mask & (1<<j)){
sumC += c[j];
int town = n + 1 + j;
for(int i=1;i<=n;i++){
edges.emplace_back(town, i, a[j][i]);
}
}
}
// 3. 排序并 Kruskal(并查集大小 n + number of selected towns)
sort(edges.begin(), edges.end(), [](const Edge& A, const Edge& B){ return A.w < B.w; });
int nodes = n;
for(int j=0;j<k;j++) if(mask & (1<<j)) ++nodes; // nodes = n + popcount(mask)
// For simplicity we set DSU size to n + k (safe)
DSU dsu(n + k);
dsu.init(n + k);
ll cost = 0;
int used = 0;
for(auto &e: edges){
if(dsu.unite(e.u, e.v)){
cost += e.w;
++used;
// We can stop early if all original cities are connected:
// However to keep code simple we let loop continue; it's acceptable given constraints in this score tier.
}
}
// Final total cost: sumC + cost
answer = min(answer, cost + sumC);
}
cout << answer << '\n';
return 0;
}
100 分(完整高效解):一次对原 \(m\) 条边 Kruskal 提取 \(n-1\) 条有用边,预排序 \(n-1 + n\cdot k\) 条边,枚举掩码时线性扫描(不重复排序)
思路简述(关键点):
- 先对原始 \(m\) 条边做一次 Kruskal,得到一组“候选城市间边”(最多 \(n-1\) 条),因为在任意添加乡镇的情况下,真正可能出现在最终解的城市—城市边只来自于原有边的 MST(这是一种剪枝/启发:保留原图 Kruskal 得到的树边作为候选)。
备注:这里保留 MST 的 \(n-1\) 条边能够明显降低之后每个掩码下需要考虑的城市—城市边数。
- 对每个乡镇 \(j\) ,把它与每个城市 \(i\) 的边(权 \(a_{j,i}\) )都视为一条边,标记这条边来自乡镇 \(j\) (town id)。所有这些 \(n\cdot k\) 条边与上一步得到的 \(n-1\) 条树边合并到一个数组里,并只做一次排序(按权)。每条边要额外保存一个
town_id字段(0 表示原来城市—城市树边, \(\,1..k\) 表示来自哪个乡镇)。 - 枚举所有乡镇子集(掩码)。对于给定掩码,从排序好的数组线性扫描:遇到
town_id==0的树边就尝试 unite;遇到town_id==j>0的边,只有当掩码中包含该乡镇(即我们“投资”了该乡镇)时才尝试 unite;其它边跳过。这样每个掩码只需 \(O(nk)\) 扫描并查操作(无需重新排序 \(m\) 条边)。时间复杂度约为 \(O(m\log m + 2^k \cdot nk)\) ,能通过题目最大数据。
下面是实现(注意并查集大小为 \(n+k\) ,乡镇编号为 \(n+1\ldots n+k\) ):
// 100_points.cpp
// 100 分解法:先对 m 边 Kruskal 取出 n-1 条树边,然后把 n-1 + n*k 条边排序,枚举掩码时线性扫描
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INFLL = (1LL<<62);
struct DSU {
int n;
vector<int> p, sz;
DSU(int n=0){ init(n); }
void init(int _n){
n = _n;
p.resize(n+1);
sz.assign(n+1,1);
for(int i=1;i<=n;i++) p[i]=i;
}
int find(int x){ return p[x]==x?x:p[x]=find(p[x]); }
bool unite(int a,int b){
a=find(a); b=find(b);
if(a==b) return false;
if(sz[a]<sz[b]) swap(a,b);
p[b]=a; sz[a]+=sz[b];
return true;
}
};
struct Edge {
int u, v;
ll w;
int town_id; // 0 表示原来城市-城市的 MST 边;1..k 表示来自第 j 个乡镇 (与城市的边)
Edge(){}
Edge(int _u,int _v,ll _w,int _t):u(_u),v(_v),w(_w),town_id(_t){}
};
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, k;
if(!(cin>>n>>m>>k)) return 0;
vector<Edge> allOriginal;
allOriginal.reserve(m);
// 读入原有 m 条边(城市编号 1..n)
for(int i=0;i<m;i++){
int u,v; ll w; cin>>u>>v>>w;
allOriginal.emplace_back(u, v, w, 0); // 暂时 town_id=0(只是原始边)
}
vector<ll> c(k);
vector<vector<ll>> a(k, vector<ll>(n+1));
for(int j=0;j<k;j++){
cin>>c[j];
for(int i=1;i<=n;i++) cin>>a[j][i];
}
// 1) 先对原始 m 边做一次 Kruskal,抽取出构成 MST 的至多 n-1 条边(作为候选城市-城市边)
vector<Edge> tmp = allOriginal;
sort(tmp.begin(), tmp.end(), [](const Edge& A, const Edge& B){ return A.w < B.w; });
DSU dsu0(n);
dsu0.init(n);
vector<Edge> cityTreeEdges; cityTreeEdges.reserve(n-1);
for(auto &e: tmp){
if(dsu0.unite(e.u, e.v)){
cityTreeEdges.emplace_back(e.u, e.v, e.w, 0); // town_id 仍为 0 标记为城市边
if((int)cityTreeEdges.size() == n-1) break;
}
}
// 2) 构造 n*k 条乡镇-城市边(编号 town = n + j)
// 每条标记其 town_id = j+1 (1..k)
vector<Edge> combined;
combined.reserve((int)cityTreeEdges.size() + n * max(0,k));
// 先插入那些城市间的树边(town_id = 0)
for(auto &e: cityTreeEdges) combined.push_back(e);
// 再插入所有乡镇-城市边
for(int j=0;j<k;j++){
int town = n + 1 + j;
for(int i=1;i<=n;i++){
combined.emplace_back(town, i, a[j][i], j+1);
}
}
// 3) 对 combined 统一排序(按权)
sort(combined.begin(), combined.end(), [](const Edge& A, const Edge& B){
if(A.w != B.w) return A.w < B.w;
// 为稳定性,优先原始城市-城市边(town_id == 0),但不必严格要求
return A.town_id < B.town_id;
});
// 4) 枚举掩码(2^k),对每个掩码线性扫描 combined,使用 DSU(n+k)
ll answer = INFLL;
int maxMask = 1<<k;
for(int mask=0; mask<maxMask; ++mask){
DSU dsu(n + k);
dsu.init(n + k);
ll baseCost = 0; // 城市/乡镇边的总费用
ll sumC = 0; // 被选中乡镇的一次性费用
for(int j=0;j<k;j++) if(mask & (1<<j)) sumC += c[j];
int used = 0; // 不严谨计数,下面我们只在合并时累加权重
// 我们要保证原始城市 1..n 最终连通(通过乡镇参与或直接城市边)
for(const Edge &e: combined){
if(e.town_id == 0){
// 城市-城市树边,总是可用
if(dsu.unite(e.u, e.v)){
baseCost += e.w;
}
} else {
int j = e.town_id - 1;
if( (mask & (1<<j)) == 0 ) continue; // 未选择该乡镇,跳过
// 否则乡镇可用,把乡镇节点编号 (n+1+j) 与城市 i 连接
if(dsu.unite(e.u, e.v)){
baseCost += e.w;
}
}
}
ll total = baseCost + sumC;
if(total < answer) answer = total;
}
cout << answer << '\n';
return 0;
}
各种做法时间复杂度
🌟【16分做法】( \(k = 0\) )
思路:
只有城市和道路,不存在乡镇。
→ 直接在 \(n\) 个城市、 \(m\) 条边上跑 Kruskal 最小生成树算法。
步骤复杂度分析:
- 排序所有边:
\(O(m \log m)\) - 并查集合并查找(近似 \(O(1)\) 摊还):
共 \(m\) 次边判断,复杂度约 \(O(m \alpha(n)) \approx O(m)\) - 合并总复杂度:
\(O(m \log m + m) \Rightarrow O(m \log m)\)
✅ 最终复杂度: \(O(m \log m)\)
✅ 适用数据: \(k = 0\) ,或忽略乡镇的情况。
🌟【32分做法】(特殊性质 A)
条件:
- \(c_j = 0\)
- 存在某城市 \(i\) ,使 \(a_{j,i} = 0\)
思路:
每个乡镇都能零代价连上某个城市,因此相当于所有乡镇都已免费接入。
→ 直接将所有乡镇视为新的节点,与城市相连的边加入图后,用 Kruskal 求最小生成树。
边数:
- 原边: \(m\)
- 新增边: \(n \times k\)
步骤复杂度:
- 排序所有边(包括城市间 + 城市到乡镇):\[O((m + nk) \log (m + nk)) \]
- 并查集操作: \(O(m + nk)\)
- 总体复杂度:\[O((m + nk) \log (m + nk)) \]
✅ 由于 \(k \le 10\) , \(nk \ll m\) ,所以可简化为:
🌟【24分做法】( \(k \le 5\) ,枚举子集)
思路:
\(k\) 很小,可以 枚举每个乡镇是否使用( \(2^k\) 种情况)。
对于每种情况:
- 加入该乡镇的所有边;
- 然后对“城市 + 当前选中的乡镇”做一次 Kruskal。
步骤复杂度:
- 枚举所有乡镇组合: \(2^k\)
- 每次 Kruskal:\[O((m + nk) \log (m + nk)) \]
- 总复杂度:\[O(2^k (m + nk) \log (m + nk)) \]
- 因为 \(k \le 5\) ,可近似:\[O(2^k m \log m) = O(32 m \log m) \]
✅ 总结:
🌟【100分做法】(终极优化)
核心优化思想:
前面做法瓶颈在于:
每次枚举都要重新排序 \(m\) 条边!
我们注意到:
- Kruskal 选出的 有效边 只有 \(n - 1\) 条;
- 枚举的边中,大多数在所有情况中都没用。
所以优化思路是:
- 先用 Kruskal 在城市之间跑一遍,找到 \(n - 1\) 条生成树边;
- 只保留这些边 + 所有乡镇边( \(n \times k\) 条);
- 一次性排序这 \(O(nk + n)\) 条边;
- DFS 枚举每种乡镇选法,在 Kruskal 时只考虑标记允许的边。
步骤复杂度:
- 第一次 Kruskal(提取城市生成树):
\(O(m \log m)\) - 一次性排序所有 \(nk + n - 1\) 条边:
\(O((nk + n) \log (nk + n)) \approx O(nk \log (nk))\) - 枚举 \(2^k\) 种乡镇选法,每次 Kruskal 边数 O(nk):\[O(2^k nk) \]
- 总复杂度:\[O(m \log m + 2^k nk) \]
✅ 对于题目范围 \(n = 10^4, m = 10^6, k = 10\) ,这能完美通过。
✅ 最终汇总表
| 分值 | 思路 | 时间复杂度 | 关键优化 |
|---|---|---|---|
| 16分 | 无乡镇,纯 Kruskal | \(O(m \log m)\) | 直接求城市最小生成树 |
| 32分 | 特殊性质 A,所有乡镇 0 成本接入 | \(O((m + nk)\log(m + nk)) \approx O(m \log m)\) | 把乡镇直接加进图中 |
| 24分 | 枚举乡镇使用情况 | \(O(2^k m \log m)\) | DFS 枚举每个乡镇选法 |
| 100分 | 先提取城市树边 + 预排序 | \(O(m \log m + 2^k nk)\) | 只枚举有限边集,提前排序 |
整合前面那个城市联通最小费用的16分 ,32分,24分做法, 不用100分做法,形成一个部分分合并的最优得分解答
整体策略(自适应选择三种方案)
读取输入后判断三种情况的优先级(按最简单/最安全的顺序):
- ** \(k = 0\) **(16分情况)
直接对原有 \(m\) 条边做一次 Kruskal 求最小生成树(MST)。复杂度 \(O(m\log m)\) 。 - 特殊性质 A(32分):对于每个乡镇 \(j\) ,都有 \(c_j = 0\) 且存在某城市 \(i\) 使得 \(a_{j,i} = 0\) 。
既然乡镇免费且至少能以 0 费用接入一个城市,就把所有乡镇当作额外节点,把乡镇到所有城市的边(权为 \(a_{j,i}\) )全部加入图,然后对包含 \(n+k\) 个节点的一次 Kruskal。复杂度近似 \(O((m + nk)\log(m+nk))\) ,通常可视为 \(O(m\log m)\) 。 - ** \(k \le 5\) **(24分)
枚举每个乡镇是否被选择(共 \(2^k\) 种情况)。对于每个子集:加入对应乡镇与城市的边(乡镇编号为 \(n+1..n+k\) ),再与原有边一起做 Kruskal,计算总费用(一次性费用 \(\sum c_j\) 加上边修建/修复费用)。复杂度 \(O(2^k (m + nk)\log(m+nk))\) ,因为 \(k\le5\) 可行。
程序按上述顺序判断并执行:若满足 \(k=0\) 执行 16 分分支;否则若满足特殊 A 执行 32 分分支;否则若 \(k\le5\) 则执行枚举分支;若以上都不满足(例如 \(k>5\) 且不满足 A),程序仍会尝试做枚举分支(可能超时——这是你要求“只用 16/24/32 分做法,不用 100 分做法”的固有限制)。
代码(C++,OI 风格,详注释)
// road_partial.cpp
// 整合 16 分(k=0)、32 分(特殊性质 A)、24 分(k<=5 枚举)三种做法的程序(不含 100 分优化)
// 编译:g++ -O2 -std=c++17 road_partial.cpp -o road_partial
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INFLL = (1LL<<62);
// 并查集(OI 风格)
struct DSU {
int n;
vector<int> fa, sz;
DSU(int n_=0){ init(n_); }
void init(int n_){
n = n_;
fa.resize(n+1);
sz.assign(n+1, 1);
for(int i=1;i<=n;i++){ fa[i]=i; }
}
int find(int x){ return fa[x]==x ? x : fa[x]=find(fa[x]); }
bool unite(int a,int b){
a = find(a); b = find(b);
if(a==b) return false;
if(sz[a] < sz[b]) swap(a,b);
fa[b] = a;
sz[a] += sz[b];
return true;
}
};
// 基本边结构:连接 u-v,权重 w
struct Edge {
int u, v;
ll w;
Edge(){}
Edge(int _u,int _v,ll _w):u(_u),v(_v),w(_w){}
};
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, k;
if(!(cin >> n >> m >> k)) return 0;
// 读取原有 m 条边
vector<Edge> baseEdges;
baseEdges.reserve(m);
for(int i=0;i<m;i++){
int u,v; ll w;
cin >> u >> v >> w;
baseEdges.emplace_back(u,v,w);
}
// 读取 k 个乡镇的数据(如果 k==0 则不会读到下面)
vector<ll> c(k);
vector<vector<ll>> a(k, vector<ll>(n+1));
for(int j=0;j<k;j++){
cin >> c[j];
for(int i=1;i<=n;i++){
cin >> a[j][i];
}
}
// ---------- 判定特殊性质 A(32 分所需) ----------
bool specialA = true;
for(int j=0;j<k;j++){
if(c[j] != 0){ specialA = false; break; }
bool hasZero = false;
for(int i=1;i<=n;i++){
if(a[j][i] == 0){ hasZero = true; break; }
}
if(!hasZero){ specialA = false; break; }
}
// ---------- 分支 1:k == 0(16 分) ----------
if(k == 0){
// 只需对原有 m 条边做一次 Kruskal,连通 n 个城市
sort(baseEdges.begin(), baseEdges.end(), [](const Edge& A, const Edge& B){
return A.w < B.w;
});
DSU dsu(n);
dsu.init(n);
ll ans = 0;
int used = 0;
for(const auto &e: baseEdges){
if(dsu.unite(e.u, e.v)){
ans += e.w;
if(++used == n-1) break;
}
}
cout << ans << "\n";
return 0;
}
// ---------- 分支 2:特殊性质 A(32 分) ----------
if(specialA){
// 将所有乡镇视作额外节点(编号 n+1 .. n+k),把所有乡镇-城市的边加入,再与 baseEdges 一起 Kruskal
// 边数为 m + n*k;并查集节点数为 n + k
vector<Edge> edges = baseEdges;
edges.reserve(m + (ll) n * k);
for(int j=0;j<k;j++){
int town = n + 1 + j;
for(int i=1;i<=n;i++){
edges.emplace_back(town, i, a[j][i]);
}
}
sort(edges.begin(), edges.end(), [](const Edge& A, const Edge& B){
return A.w < B.w;
});
DSU dsu(n + k);
dsu.init(n + k);
ll total = 0;
for(const auto &e: edges){
if(dsu.unite(e.u, e.v)){
total += e.w;
}
}
// 题目目标是保证原有 n 个城市两两连通;在特殊 A 下把所有乡镇免费加入并做 MST 可以得到最优或等价解
cout << total << "\n";
return 0;
}
// ---------- 分支 3:k <= 5(24 分) ----------
if(k <= 5){
// 枚举所有乡镇子集(2^k <= 32),每个子集把被选乡镇加入以及其与城市的边加入,再与 baseEdges 一起 Kruskal
ll best = INFLL;
int maxMask = 1<<k;
// 为减少常量,先把 baseEdges 排序一次;在每个掩码中我们将 baseEdges 复制到局部数组再加入乡镇边并排序
// 注意:这仍然会重新排序大数组,但在 k<=5 情况常被允许(部分分)
sort(baseEdges.begin(), baseEdges.end(), [](const Edge& A, const Edge& B){
return A.w < B.w;
});
for(int mask = 0; mask < maxMask; ++mask){
// 计算一次性费用 sumC
ll sumC = 0;
int addedTowns = 0;
for(int j=0;j<k;j++){
if(mask & (1<<j)){
sumC += c[j];
++addedTowns;
}
}
// 构造边集:把 baseEdges 复制,然后加入被选乡镇的 n 条边
vector<Edge> edges = baseEdges; // 已排序,但后续插入会破坏顺序 -> 下面要重新排序
edges.reserve(edges.size() + (size_t)addedTowns * n);
for(int j=0;j<k;j++){
if(mask & (1<<j)){
int town = n + 1 + j;
for(int i=1;i<=n;i++){
edges.emplace_back(town, i, a[j][i]);
}
}
}
// 重新排序(简洁直观):时间瓶颈在 m 较大时会慢,但属于 24 分的允许实现
sort(edges.begin(), edges.end(), [](const Edge& A, const Edge& B){
return A.w < B.w;
});
// Kruskal:并查集大小使用 n + k(安全)
DSU dsu(n + k);
dsu.init(n + k);
ll cost = 0;
for(const auto &e: edges){
if(dsu.unite(e.u, e.v)){
cost += e.w;
}
}
// 最终总费用 = 修/建道路费用 + 被选乡镇的一次性费用
best = min(best, cost + sumC);
}
cout << best << "\n";
return 0;
}
// ---------- 其它情况(k > 5 且不满足特殊 A) ----------
// 由于本次任务要求不使用 100 分的优化,这里我们仍然尝试用枚举(可能超时),并给出提示性实现。
// 实际比赛中对这种情况应该用 100 分的优化版本(已在之前讨论)。
{
// 尝试枚举(若 k 较大会爆炸),但实现与上面 k<=5 相同(注意可能超时)
ll best = INFLL;
int maxMask = 1<<k;
if(k >= 25){
// 防止位移溢出或枚举不可行,直接退出或返回一个安全上界(但题设要求不这样)
// 这里我们为了完整性,直接使用一种保守做法:只做不选任何乡镇(等同 k=0)
// 这保证程序不会爆炸,但得分可能较低。
sort(baseEdges.begin(), baseEdges.end(), [](const Edge& A, const Edge& B){ return A.w < B.w; });
DSU dsu(n);
dsu.init(n);
ll ans = 0;
int used = 0;
for(const auto &e: baseEdges){
if(dsu.unite(e.u, e.v)){
ans += e.w;
if(++used == n-1) break;
}
}
cout << ans << "\n";
return 0;
}
// 仍尝试完整枚举(可能会超时)
sort(baseEdges.begin(), baseEdges.end(), [](const Edge& A, const Edge& B){
return A.w < B.w;
});
for(int mask = 0; mask < maxMask; ++mask){
ll sumC = 0;
int addedTowns = 0;
for(int j=0;j<k;j++){
if(mask & (1<<j)){
sumC += c[j];
++addedTowns;
}
}
vector<Edge> edges = baseEdges;
edges.reserve(edges.size() + (size_t)addedTowns * n);
for(int j=0;j<k;j++){
if(mask & (1<<j)){
int town = n + 1 + j;
for(int i=1;i<=n;i++){
edges.emplace_back(town, i, a[j][i]);
}
}
}
sort(edges.begin(), edges.end(), [](const Edge& A, const Edge& B){
return A.w < B.w;
});
DSU dsu(n + k);
dsu.init(n + k);
ll cost = 0;
for(const auto &e: edges){
if(dsu.unite(e.u, e.v)) cost += e.w;
}
best = min(best, cost + sumC);
}
cout << best << "\n";
return 0;
}
}
各分支的时间复杂度总结(清晰列出)
- **16 分( \(k=0\) **):排序 \(m\) 条边: \(O(m\log m)\) 。并查集操作摊还 \(O(m)\) 。总体 \(O(m\log m)\) 。
- 32 分(特殊性质 A):共 \(m + nk\) 条边,排序复杂度 \(O((m+nk)\log(m+nk))\) 。通常 \(k\le10\) ,所以可近似 \(O(m\log m)\) 。
- 24 分( \(k\le5\) 枚举):枚举 \(2^k\) 个子集(≤32),每个子集做一次 Kruskal:排序与并查复杂度约 \(O((m+nk)\log(m+nk))\) 。总复杂度 \(O(2^k (m+nk)\log(m+nk))\) ,常数较大但在部分分上传统可接受。
- 其它情况( \(k>5\) 且不满足 A):本程序尝试枚举所有子集(会爆炸),在比赛中应改用 100 分的优化(已在 earlier 讨论中给出)。为防止位移溢出,本程序若 \(k\) 极大会退化为只做 \(k=0\) 的 MST 作为保守输出(可以保证不会崩溃,但得分受限)。

浙公网安备 33010602011771号