《算法竞赛进阶指南》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; }