[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-\lbrace i\rbrace}+\min_{j\in s-\lbrace i\rbrace}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})\]

考虑如何求出 $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$ 能够到达的点的集合,有:

\[f_{k,j}=\sum_{y\in j-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)$。

AC 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
//#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;
}