写一道题码一篇题解系列,足以证明我有多菜
因为写每道题都有没注意的或者刚学会的东西【欲哭无泪】
题面:https://www.luogu.org/problem/P3959
这里的做法是状压DP。
把原图分成一层一层的点集,假设下一层可以由上一层拓展到。设dp[i][j]为在第i层转移到j状态的代价,那么转移到dp[i][j]的贡献就是dp[i-1][k]+num*(i-1),其中k是j的子集,num为k中的点扩展出j中有但k中没有的点的最小代价。
转移的时候要确保k能拓展到j,这个可以预处理g[k][j]。同时还要预处理每个状态i拓展到某个点j的最小代价dis[j][i],用来计算num。
初始化是把每个单独点在第一层的代价设为0,表示它作为地面。答案是在每一层转移到全集的最小值。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; int n,m; int maxn; long long e[1<<12],dp[13][1<<12],ans,dis[13][1<<12],num,d[13][13],g[1<<12][1<<12]; void work(){ for(int i=1;i<=maxn;i++){ for(int j=2;j<=n;j++){ for(int k=(i-1)&i;k;k=(k-1)&i){ if(g[k][i]){ num=0; for(int l=1;l<=n;l++){ if((i&(1<<(l-1)))&&!(k&(1<<(l-1)))&&dis[l][k]<=500000){ num+=dis[l][k]; } } dp[j][i]=min(dp[j][i],dp[j-1][k]+(j-1)*num); } } } } } int main() { scanf("%d%d",&n,&m); maxn=(1<<n); for(int i=1,x,y;i<=m;i++){ long long z; scanf("%d%d%lld",&x,&y,&z); if(x==y)continue; if(!d[x][y])d[x][y]=d[y][x]=z; else d[x][y]=d[y][x]=min(d[x][y],z); } memset(dis,0x3f3f3f3f,sizeof(dis)); for(int i=0;i<maxn;i++){ for(int j=1;j<=n;j++){ for(int k=1;k<=n;k++){ if((i&(1<<(k-1)))&&!(i&(1<<(j-1)))){ if(d[k][j]){ dis[j][i]=min(dis[j][i],d[k][j]); } } } } } int flag,flag1; for(int i=1;i<maxn;i++){ for(int j=(i-1)&i;j;j=(j-1)&i){ flag=0; for(int k=1;k<=n;k++){ flag1=0; if((i&(1<<(k-1)))&&!(j&(1<<(k-1)))){ for(int l=1;l<=n;l++){ if(j&(1<<(l-1))){ if(d[l][k]){ flag1=1; break; } } } if(!flag1){ flag=1; break; } } } if(!flag)g[j][i]=1; } } ans=0x3f3f3f3f; memset(dp,0x3f3f3f3f,sizeof(dp)); for(int i=1;i<=n;i++){ dp[1][(1<<(i-1))]=0; } work(); for(int j=1;j<=n;j++)ans=min(ans,dp[j][maxn-1]); printf("%lld ",ans); return 0; }
复杂度是O(3n*n2)。
一开始我是处理点集对应的边来判断转移是否合法,显然会出锅…就算边集是包含关系也不一定能转移,中间可能有断层。然后空间卡着开…最后还有个坑就是重边取min,其实这个根据数据范围应该就能看出来的orz
我太菜了,这题写了一年:(还是不太会做状压DP
洛谷题解里还有模拟退火以及搜索+剪枝的做法。