《算法竞赛进阶指南》0x56状态压缩DP AcWing529 宝藏
题目链接:https://www.acwing.com/problem/content/531/
题目给出不超过12个点,和一些边,第一个点不用花费,其余的点都要根据深度和扩展的边长来确定花费,通过dp,将层数作为阶段,每个阶段用状态压缩记录12个点中已经走过的点,转移的过程是从j状态转移到k,这里两个状态的转移一定要合法,在深度i-1转移到i时,对每个扩展的点到j集合的距离之和乘上(i-1).
这里有一点十分重要,从j转移到k如何保证一定是从深度i-1的点扩展的呢?
证明:
①、假设全部是从深度为i-1的点转移的,那么由于i-1层的状态都是最优的,加上新扩展的点到集合的距离也是最短的,无疑转移的结果是最优的。
②、假设原状态是S,加入一些新的点之后,新状态是S1,扩展的过程中包括一些点,这些点是从小于i-1层的状态扩展过来的,那么一定存在一个集合,包括S1中除了由i-1层扩展过来的结点以外的所有结点,这个状态在前i-1层中一定是最优的,把这个集合记为S2,那么显然从S2扩展到S1就是①中的情况。最优结果不会遗漏。
证毕。
代码:
#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
const int N = 15,M=1<<12;
int f[N][M];
int expand[M],road[M][N];
vector<int> valid[M],cost[M];//每个状态的下一个合法状态以及状态扩展的最小代价和
int a[N][N];
int n,m;
void pre(){
memset(road,0x3f,sizeof road);
for(int k=0;k<1<<n;k++){
expand[k]=k;
for(int x=1;x<=n;x++){
if(k>>(x-1) & 1){//集合中存在点x
road[k][x]=0;
for(int y=1;y<=n;y++){//枚举x可达的点
if(a[x][y]==0x3f3f3f3f)continue;
expand[k]|=1<<(y-1);//将这个点加到集合中去,注意这个点可能已经在k中
road[k][y]=min(road[k][y],a[x][y]);//到集合k的最小距离
}
}
}
}
for(int j=0;j<1<<n;j++){
for(int k=0;k<j;k++){//枚举j的子集,由于数量小,所以暴力枚举
if((k&j)==k && (expand[k]&j)==j){//k是j的子集并且j是expand[k]的子集
valid[j].push_back(k);//(k,j)合法
int sum=0;
for(int i=1;i<=n;i++){
if((j^k)>>(i-1) & 1)sum+=road[k][i];//差集的最短路之和
}
cost[j].push_back(sum);
}
}
}
}
int main(){
cin>>n>>m;
memset(a,0x3f,sizeof(a));
for(int i=1;i<=m;i++){
int x,y,z;
cin>>x>>y>>z;
a[x][y]=a[y][x]=min(a[x][y],z);
}
pre();
memset(f,0x3f,sizeof f);
for(int i=1;i<=n;i++)
f[1][1<<(i-1)]=0;//扩展第一层不需要资金
int ans=f[1][(1<<n)-1];
for(int i=2;i<=n;i++){
for(int j=1;j<1<<n;j++){
for(int p=0;p<valid[j].size();p++){
int k=valid[j][p];
f[i][j]=min(f[i][j],f[i-1][k]+(i-1)*cost[j][p]);
}
}
ans=min(ans,f[i][(1<<n)-1]);
}
cout<<ans<<endl;
}

![《算法竞赛进阶指南》0x56状态压缩DP AcWing529 宝藏
[编程语言教程]](https://www.zixueka.com/wp-content/uploads/2024/01/1706714844-58f0ae21174d16e.jpg)
