题解:[NOIP 2017 提高组] 宝藏

洛谷P3959

Posted by TH911 on June 1, 2025

题目传送门

状压 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}$,则有:

\[\textit{dp}_{s\cup\lbrace j\rbrace}\leftarrow\min(\textit{dp}_{s\cup\lbrace j\rbrace},\textit{dp}_s+w_{i,j}\cdot\textit{depth}_j)\]

当然,也有直接得出答案的形式(枚举从 $j$ 打通到 $i$):

\[\textit{dp}_s=\min_{i\in s}(\textit{dp}_{s\setminus\set i}+\min_{j\in s\setminus\set i}w_{i,j}\cdot \textit{depth}_{i})\]

但是很显然,无论如何都绕不开 $\textit{depth}_i$ 或 $\textit{depth}_j$,而这在当前的 DP 状态下无法得知。

因此,对于这种不会的 DP,我们可以再加一维

设 $\textit{dp}_{i,s}$ 表示当前打通的宝藏屋的集合为 $s$,$s$ 中的宝藏屋最大深度为 $i$ 时的最小代价,答案即:

\[\min\limits_{i=0}^n\textit{dp}_{i,\mathbb N\cap[0,n-1]}\]

状态转移

考虑到最后打通的肯定是一棵树,将这棵树按照深度分层,则递推 $\textit{dp}_{i,j}$ 即用第 $i-1$ 层的节点打通第 $i$ 层的节点。

枚举 $j$ 的非空真子集 $k$,设 $f_{k,j}$ 表示从状态为 $k$ 打通到 $j$ 的最小边权和,$\textit{can}_{k,j}$ 表示能否从 $k$ 打通到 $j$,有:

\[\textit{dp}_{i,j}=\min_{k\subsetneq j,\textit{can}_{k,j}=1}(\textit{dp}_{i-1,k}+i\cdot f_{k,j})\]

考虑如何求出 $\textit{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’$,则 $\textit{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 \textit{edge}_k]$。

得到 $\textit{can}{k,j}$ 后,若 $\textit{can}{k,j}=1$,就需要求解 $f_{k,j}$。

记 $v_x$ 表示节点 $x$ 能够到达的点的集合,有:

\[f_{k,j}=\sum_{y\in j\setminus k}\min_{x\in v_y,x\in k,x\notin j}w_{x,y}\]

即枚举 $(x,y)$,尝试从 $k$ 中的 $x$ 打通到在 $j$ 中且不在 $k$ 中的 $y$,且对于每一个 $y$,只需要一条边打通到它,因此要取所有边的最小值。

边界条件

\[\textit{dp}_{0,\lbrace i\rbrace}=0\]

其中,$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$ 的集合。

二项式定理,总子集数为:

\[\sum_{i=0}^n \binom ni2^i=\sum_{i=0}^n\binom ni1^{n-i}2^i=(1+2)^n=3^n\]

还有一种分析方式。

记 $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)$。

模拟退火

答案较为连续,考虑模拟退火。

枚举起点,DFS 找到任意一种初始情况,再进行 $100$ 次模拟退火。

AC 代码

状压 DP ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include 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]; 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; } ``` </details>
模拟退火 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=12,inf=0x3f3f3f3f; constexpr const double eps=1e-6; mt19937 Rand(time(0)); int n,g[N+1][N+1],father[N+1]; int Min=2147483647; bool vis[N+1]; void dfs(int x,int fx){ father[x]=fx; vis[x]=true; for(int i=1;i<=n;i++){ if(g[x][i]>=inf||i==fx||vis[i]){ continue; } dfs(i,x); } } void count(int x,int &cnt){ cnt++; for(int i=1;i<=n;i++){ if(father[i]==x){ count(i,cnt); } } } int calc(int x,int depth=1){ int ans=0; for(int i=1;i<=n;i++){ if(father[i]==x){ ans+=calc(i,depth+1); ans+=g[x][i]*depth; } } return ans; } void solve(int root){ for(int i=1;i<=100;i++){ memset(vis,0,sizeof(vis)); dfs(root,0); int ans=calc(root); Min=min(Min,ans); double t=100000,d=0.985; while(t>=eps){ t*=d; int x=Rand()%n+1,y; while(x==root){ x=Rand()%n+1; } int backupFatherX=father[x]; int plCnt=0; do{ y=Rand()%n+1; while(x==y||g[x][y]==inf){ y=Rand()%n+1; } father[x]=y; }while(plCnt=0,count(root,plCnt),plCnt<n); int pl=calc(root); if(pl<ans){ ans=pl; Min=min(Min,ans); }else{ if(Rand()/((1ll<<32)-1.0)<=exp(-1.0*(pl-ans)/t)){ continue; }else{ father[x]=backupFatherX; } } } } } int 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; if(n==1){ cout<<"0"<<endl; return 0; } memset(g,0x3f,sizeof(g)); for(int i=1;i<=m;i++){ int u,v,w; cin>>u>>v>>w; g[u][v]=g[v][u]=min(g[u][v],w); } for(int i=1;i<=n;i++){ solve(i); } cout<<Min<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } /* 4 3 1 2 1 1 4 1 3 4 1 */ ``` </details>