题解:[NOIP 2017 提高组] 宝藏
状压 DP
看到数据范围,因为 \(1\leq n\leq12\),因此可以设计指数级算法(一般是搜索或状压 DP),考虑状压 DP。
状态设计
设 \(\textit{dp}_s\) 表示打通的宝藏屋的集合(宝藏屋的状态,且 \(0\sim n-1\) 编号)为 \(s\) 时的最小代价,\(w_{i,j}\) 表示 \((i,j)\) 的长度。
那么转移就可以枚举 \(s\) 中的每一个点 \(i\),找到其连出的一条边 \((i,j)=w_{i,j}\),则有:
当然,也有直接得出答案的形式(枚举从 \(j\) 打通到 \(i\)):
但是很显然,无论如何都绕不开 \(\textit{depth}_i\) 或 \(\textit{depth}_j\),而这在当前的 DP 状态下无法得知。
因此,对于这种不会的 DP,我们可以再加一维。
设 \(\textit{dp}_{i,s}\) 表示当前打通的宝藏屋的集合为 \(s\),\(s\) 中的宝藏屋最大深度为 \(i\) 时的最小代价,答案即:
状态转移
考虑到最后打通的肯定是一棵树,将这棵树按照深度分层,则递推 \(\textit{dp}_{i,j}\) 即用第 \(i-1\) 层的节点打通第 \(i\) 层的节点。
枚举 \(j\) 的非空真子集 \(k\),设 \(f_{k,j}\) 表示从状态为 \(k\) 打通到 \(j\) 的最小边权和,\(\textit{can}_{k,j}\) 表示能否从 \(k\) 打通到 \(j\),有:
考虑如何求出 \(can_{k,j},f_{k,j}\)。
显然,\(\textit{can}_{k,j}\) 不会总共 \(2^{2n}\) 种状态都会用到(当然,你这样写也不会 TLE 就是了),因此可以在计算 \(f_{k,j}\) 时计算。
枚举 \(j\) 的真子集 \(k\) 的时间复杂度是 \(\mathcal O\left(3^n\right)\) 的,详见时间复杂度。
可以从枚举 \(k\) 中的点 \(l\),再枚举边 \((l,l')\),从而得出 \(k\) 能够到达的宝藏屋集合 \(k'\),则 \(can_{k,j}=\left[j\subseteq k'\right]\)。其中的中括号为艾弗森括号,条件成立时为 \(1\),否则为 \(0\)。
但是这样的总时间复杂度是 \(\mathcal O\left(n^23^n\right)\) 的,可以优化。
记 \(\textit{edge}_k\) 表示宝藏屋集合 \(k\) 能打通的所有点的集合(包含 \(k\)),其实也就是上面的 \(k'\),而求出所有的 \(\textit{edge}_k\) 只需要 \(\mathcal O\left(n^22^n\right)\)。
这样其实是避免了重复求解 \(\textit{edge}_k\),而 \(\textit{can}_{k,j}=[j\subseteq edge_k]\)。
得到 \(can_{k,j}\) 后,若 \(can_{k,j}=1\),就需要求解 \(f_{k,j}\)。
记 \(v_x\) 表示节点 \(x\) 能够到达的点的集合,有:
即枚举 \((x,y)\),尝试从 \(k\) 中的 \(x\) 打通到在 \(j\) 中且不在 \(k\) 中的 \(y\),且对于每一个 \(y\),只需要一条边打通到它,因此要取所有边的最小值。
边界条件
其中,\(i\) 满足 \(i\in \mathbb N\cap[0,n-1]\)。
即题面中的“赞助商”打通的那个宝藏屋。
时间复杂度
枚举所有可能的宝藏屋集合 \(s\) 的数量为 \(2^n\),称全集 \(U=\lbrace1,2,3,\cdots,n\rbrace\),\(s\) 为 \(U\) 的子集。
那么对于 \(U\) 的子集,想要求得 \(U\) 的子集的子集数,不妨先将其按元素个数分组。
有 \(\dbinom n0\) 个大小为 \(0\) 的集合(空集),有 \(\dbinom n1\) 个大小为 \(1\) 的集合,有 \(\dbinom n2\) 个大小为 \(2\) 的集合……有 \(\dbinom ni\) 个大小为 \(i\) 的集合。
由二项式定理,总子集数为:
还有一种分析方式。
记 \(A\subseteq U,B\subseteq A\),答案即二元组 \((A,B)\) 的数量。对于元素 \(x\in U\):
- \(x\notin A,x\notin B\)。
- \(x\in A,x\notin B\)。
- \(x\in A,x\in B\)。
故,总共有 \(3^n\) 个子集。
那么,总时间复杂度为 \(\mathcal O\left(n^23^n\right)\)。
AC 代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int N=12,inf=0x3f3f3f3f;
int n,g[N+1][N+1],dp[N+1][1<<N|1],f[1<<N|1][1<<N|1],can[1<<N|1][1<<N|1],edge[1<<N|1];
string binary(int x){
string ans;
while(x){
ans+=(x&1)^'0';
x>>=1;
}
reverse(ans.begin(),ans.end());
return ans;
}
main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int m;
cin>>n>>m;
memset(g,0x3f,sizeof(g));
while(m--){
int u,v,w;
cin>>u>>v>>w;
u--,v--;
g[u][v]=g[v][u]=min(g[u][v],w);
}
for(int s=0;s<(1<<n);s++){
edge[s]=s;
for(int i=0;i<n;i++){
if(s&(1<<i)){
for(int j=0;j<n;j++){
if(g[i][j]<inf){
edge[s]|=1<<j;
}
}
}
}
}
//从 j 转移到 i 的最小边权和
for(int i=0;i<(1<<n);i++){
for(int j=(i-1)&i;j;j=(j-1)&i){
//j 转移能到 i 的所有点
if((i&edge[j])==i){
can[j][i]=true;
static int tmp[N+1];
fill(tmp,tmp+n,inf);
for(int k=0;k<n;k++){
if(j&(1<<k)){
for(int l=0;l<n;l++){
if((i&(1<<l)) && !(j&(1<<l))){
tmp[l]=min(tmp[l],g[k][l]);
}
}
}
}
for(int k=0;k<n;k++){
if((i&(1<<k)) && !(j&(1<<k))){
if(tmp[k]==inf){
can[j][i]=false;
break;
}
f[j][i]+=tmp[k];
}
}
}else{
can[j][i]=false;
}
}
}
memset(dp,0x3f,sizeof(dp));
for(int i=0;i<n;i++){
dp[0][1<<i]=0;
}
for(int i=1;i<=n;i++){
for(int j=0;j<(1<<n);j++){
for(int k=(j-1)&j;k;k=(k-1)&j){
if(can[k][j]){
dp[i][j]=min(dp[i][j],dp[i-1][k]+i*f[k][j]);
}
}
}
}
int ans=2147483647;
for(int i=0;i<=n;i++){
ans=min(ans,dp[i][(1<<n)-1]);
}
cout<<ans<<'\n';
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}