Luogu P3959 [NOIP 2017 提高组] 宝藏 题解
不难发现最后的路径一定会构成一棵树。由于每条路的贡献与与其深度有关,故考虑设计与深度有关的状态。由于数据范围很小,考虑状态压缩 DP,每次扩展一层。
设状态 \(f[i][j]\) 表示目前扩展到第 \(i\) 层,所有节点的状态为 \(j\)。若第 \(k\) 位为 \(1\),则表示节点 \(k\) 已经被打通;若第 \(k\) 位为 \(0\),则表示节点 \(k\) 未被打通。记状态 \(k\) 转移到状态 \(j\) 经过的边权之和为 \(cost(k,j)\),不难得到如下转移方程:
接下来,考虑预处理出两个数组 \(cost(k,j)\) 与 \(pos(k,j)\),表示状态 \(k\) 转移到状态 \(j\) 经过的边权之和为 \(cost(k,j)\) 与可行性。再预处理一个数组 \(ex(i)\),表示状态 \(i\) 可以扩展的所有点(包括原有点)的状态集合。若第 \(k\) 位为 \(1\),则表示节点 \(k\) 可以被扩展;若第 \(k\) 位为 \(0\),则表示节点 \(k\) 不可以被扩展。
\(ex(i)\) 并不难预处理,只需要枚举已有节点的每一条边,能扩展到的标记为 \(1\) 即可。有了 \(ex(i)\) 后,我们发现 \(pos(k,j)\) 也不难预处理。当且仅当 \(k\) 是 \(j\) 的子集且 \(j\) 是 \(ex(k)\) 的子集时,\(pos(k,j)=1\)。判断子集可以用位运算来实现,若 \(j\&k=k\),则 \(k\) 是 \(j\) 的子集。
接下来,考虑求出满足 \(pos(k,j)\) 的 \(cost(k,j)\)。考虑枚举状态 \(j\) 中未扩展的点,枚举从 \(k\) 中的点到这一个点的边,取边权最小值为贡献。最后,将每一个未扩展的点的贡献加和,即为 \(cost(k,j)\)。预处理之后,转移也比较显然。
时间复杂度为 \(O(m2^{n}+n3^n)\),其中 \(3^n\) 是枚举子集的时间复杂度。
#include <bits/stdc++.h>
using namespace std;
int n,m,u,v,d,h[30000],ex[5000],cst[5000][5000],f[20][5000],ans=1e9;
vector<int>to[20],dis[20];
bool pos[5000][5000];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&d);
to[u].push_back(v),dis[u].push_back(d);
to[v].push_back(u),dis[v].push_back(d);
}
for(int i=0;i<=(1<<n)-1;i++)
{
ex[i]=i;
for(int j=0;j<n;j++)
if(i&(1<<j))
{
int s=to[j+1].size();
for(int k=0;k<s;k++)ex[i]|=(1<<(to[j+1][k]-1));
}
}
for(int i=0;i<=(1<<n)-1;i++)
for(int j=0;j<=(1<<n)-1;j++)
if((j|ex[i])==ex[i]&&(i|j)==j&&i!=j)
{
pos[i][j]=1;
for(int k=0;k<n;k++)
if(!(i&(1<<k))&&(j&(1<<k)))
{
int mi=1e9,s=to[k+1].size();
for(int l=0;l<s;l++)
if((i&(1<<(to[k+1][l]-1))))mi=min(mi,dis[k+1][l]);
cst[i][j]+=mi;
}
}
for(int i=0;i<=n;i++)
for(int j=0;j<=(1<<n)-1;j++)
f[i][j]=1e9;
for(int i=1;i<=n;i++)
f[0][(1<<(i-1))]=0;
for(int i=1;i<=n;i++)
for(int j=0;j<=(1<<n)-1;j++)
for(int k=0;k<=(1<<n)-1;k++)
if(pos[k][j]&&f[i-1][k]!=1e9)f[i][j]=min(f[i][j],f[i-1][k]+cst[k][j]*i);
for(int i=0;i<=n;i++)ans=min(ans,f[i][(1<<n)-1]);
printf("%d\n",ans);
return 0;
}

浙公网安备 33010602011771号