link
题目要求任选图中一点为根,通过拓展道路最终形成一棵树,使得代价总和最小,代价受深度和边权两个因素影响。
容易想到一种爆搜,任选一点为根,每次扫描已选点来不断尝试拓展道路,但这样做太蛋疼了,我们尝试优化。
此时一看数据范围 \(n\le12\),考虑把思路往状压 \(dp\)上引导或寻找类似做法,我们发现:
- 在最终形成的树中,同一深度的点不受拓展先后顺序的影响,也就是说我们可以尝试一次性拓展同一深度的点。
- 对于已连通的点集 \(s\),我们不关心它的具体形态,只需保证其代价最小,因为在后续拓展中的代价与 \(s\) 的形态无关。
这样的条件启发我们可以 “深度” 为阶段,“点集” 为状态尝试设计 \(dp\)。
设 \(dp\) 状态 \(f_{i,s}\) 为当前拓展到深度 \(i\),状态为 \(s\) 的点均连通时的最小代价,转移方程如下:
\[f_{i,s}=\min_{s \in z 且新点均与 z连通} \{f_{i-1,z} +(i-1)\times cost(z,s) \}
\]
其中转移条件 \(s\in z\) 意味着新的一层只能打通原来没有的点,这时就需要用到子集枚举的手段,这样做时间复杂度是 \(O(3^n)\) 的。
\(cost(z,s)\) 表示从旧状态到新状态的代价,这个可以通过预处求得。
最终一层枚举起点,一层枚举深度,再加上子集枚举 \(dp\) 总时间复杂度是 \(O(n^2 3^n)\) 的,可以通过本题。
点击查看代码
const int N=15;
const int M=1<<12;
const int inf=0x3f3f3f3f;
int cost[M][M],n,m,e[N][N];
ll f[N][M],ans=inf;
void init(){for(int s=1;s<(1<<n);s++){ //当前层for(int z=s;z;z=(z-1)&s){//上一层为其子集,保证都是新点转移过来if(s==z) continue;for(int i=0,tmp=inf;i<n;i++,tmp=inf){ //枚举新点if(!( ((s^z)>>i) &1)) continue;for(int j=0;j<n;j++) if((z>>j)&1) tmp=min(tmp,e[j][i]); //选择最短边if(tmp>=inf){cost[z][s]=inf;break;}else cost[z][s]+=tmp; }}}
}
ll DP(int x){memset(f,0x3f,sizeof(f));f[0][1<<x]=0;//初始免费边ll res=inf;for(int i=1;i<n;i++){for(int s=1;s<(1<<n);s++){for(int z=s;z;z=(z-1)&s){if(s==z) continue;f[i][s]=min(f[i][s],f[i-1][z]+1ll*i*cost[z][s]);}if(s==(1<<n)-1) res=min(res,f[i][s]);}}return res;
}
void xpigeon(){rd(n,m);if(n==1){cout<<0<<'\n';return ;}memset(e,0x3f,sizeof(e));for(int i=0;i<n;i++){e[i][i]=0;}for(int i=1,u,v,w;i<=m;i++){rd(u,v,w);u--,v--;e[u][v]=e[v][u]=min(e[u][v],w);}init();for(int i=0;i<n;i++) ans=min(ans,DP(i));cout<<ans<<'\n';
}
signed main() {ios::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);xpigeon();return 0;
}