[CSP-S 2025] 道路修复 road
T2 道路修复(road)
如果不加乡镇,也就是第一档部分分,这就是一个裸的最小生成树模板,kruskal 直接做。
发现乡镇的范围很小只有 \(5-10\),考虑 \(2^k\) 枚举哪些乡镇要用,直接把启用乡镇的代价加到边权和里然后把带来的新边全加进去重新跑最小生成树,时间复杂度 \(O(2^k(m+kn) \log{(m+kn)})\) ,这样子时间复杂度就很炸,场上我以为接下来的优化和 \(prim\) 相关,但 \(prim\) 是 \(n^2\) 的其实毫无用处,在费劲心思想不熟悉的东西之前,也应该想想如何对现有的做法进一步优化,哪怕不优化复杂度,剪枝一下也可以。
发现瓶颈在于 \(m\) 很大,有 \(1e6\) ,但实际上需要用到只有 \(n\) 级别,考虑一开始先跑一次最小生成树得到的边才是有效的,其他边在后续的选择中一定没有竞争力,这样就直接把边数将到 \(n\) 级别,现在复杂度是 \(O(2^kkn \log{kn})\) 的,这东西跑满也是能到 \(1e9\) 的尝试去 \(log\) 。
我们可以把所有边直接加进来后排序,用 \(vis\) 数组记一下每次都有哪些乡镇要被启用,跑最小生成树的时候判一下,这样就能把 \(log\) 去掉。
甚至还可以再剪枝一下,如果现在跑的答案已经不优了,就可以直接跳过这轮最小生成树的计算,当然这也只是一个小剪枝。。。
赛后发现这题本身好像思维上没有太大限制吧,但是场上的思路走错了后面就什么都没想出来,适时复盘一下成果再重新选择方向是正确的,这题只打了最暴力的分真是蛮可惜的。
const int N=2e4+5;
const int M=2e6+5;
int n,m,k;
struct edge{
int u,v,w;
}g[M],e[M];
int c[15];
int a[15][N];
int fa[N],ans;
bool vis[15];
bool cmp(edge i,edge j){
return i.w<j.w;
}
int find(int x){
return x==fa[x]?x:fa[x]=find(fa[x]);
}
int solve1(){
for(int i=1;i<=n;i++){
fa[i]=i;
}
int cnt=0,res=0;
for(int i=1;i<=m;i++){
int x=find(g[i].u);
int y=find(g[i].v);
int w=g[i].w;
if(x==y) continue;
fa[x]=y;
res+=w;
e[++cnt]=g[i];
if(cnt==n-1) break;
}
return res;
}
void solve(int s){
int num=n,res=0,cnt=0;
for(int i=1;i<=n+k;i++){
fa[i]=i;
}
for(int i=0;i<k;i++){
int ni=i+1;
if((s>>i)&1^1){
vis[ni]=0;
continue;
}
num++;
vis[ni]=1;
res+=c[ni];
}
for(int i=1;i<=m;i++){
if(e[i].u>n && vis[e[i].u-n]==0) continue;
int x=find(e[i].u);
int y=find(e[i].v);
int w=e[i].w;
if(x==y) continue;
res+=w;
cnt++;
fa[x]=y;
if(res>=ans) break;
if(cnt==num-1) break;
}
ans=min(ans,res);
}
void xpigeon(){
cin>>n>>m>>k;
for(int i=1;i<=m;i++){
cin>>g[i].u>>g[i].v>>g[i].w;
}
sort(g+1,g+m+1,cmp);
ans=solve1();
m=n-1;
for(int i=1;i<=k;i++){
cin>>c[i];
for(int j=1;j<=n;j++){
cin>>a[i][j];
e[++m]={n+i,j,a[i][j]};
}
}
sort(e+1,e+m+1,cmp);
for(int s=1;s<(1<<k);s++){
solve(s);
}
cout<<ans<<'\n';
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
xpigeon();
return 0;
}

浙公网安备 33010602011771号