[WC2008]游览计划 状压DP,斯坦纳树
题解:
这是一道斯坦纳树的题,用状压+spfa来解决
什么是斯坦纳树?
一开始还以为是数据结构来着,其实跟最小生成树很像,大致就是最小生成树只能在各个点之间直接相连,而斯坦纳树则允许间接相连。
就是允许在图中添加新的点,再通过连接新的点来将指定点联通。这些新点可能是被限定的(给定点),比如这道题。
说到这里,你可能已经发现了,最小生成树其实就是斯坦纳树的一种特殊情况。
不过斯坦纳树其实是一个NPC问题,但是在做题的时候我们可以利用状压来解决小范围内的斯坦纳树问题
那么这道题应该如何解决呢?
其实我感觉这道题不知道斯坦纳树应该也可以做,反正是状压,不过知道斯坦纳树应该对解题有点帮助的吧。
首先我们观察到景点数其实是很小的,因此我们设f[i][j][s],表示当前点为i,j,状态为s时的最小代价,
同时记录path[i][j][s]表示转移到f[i][j][s]的状态
则有:
f[i][j][s]=min(f[i][j][k] + f[i][j][s^k] - a[i][j]);
f[i][j][s]=min(f[x][y][s] + a[i][j]);
现在来解释一下这两个式子:
1,f[i][j][s]=min(f[i][j][l] + f[i][j][s^k] - a[i][j]);
这个实质上是低层状态向高层状态的转移,是同一个点不同状态的合并,但由于是同一个点,因此直接相加的话当前点的权值会被计算两次,因此要减掉一个当前点的权值。
其中k表示s的一个子集
这里注意一个枚举子集的技巧:
for(int k=S & (S - 1); k ;k = (k - 1) & S)
其实本来应该是从k=S开始的,但是由于k=S,那么S^k将为空集,而在本题中,用空集去更新状态是毫无意义的,因此直接就跳到(S - 1) & S了
2,f[i][j][s]=min(f[x][y][s] + a[i][j]);
其实这个的意义我认为是在于向非景点的转移,转移方法是枚举与x,y相邻的点(上下左右),然后更新(式子中i,j就是被更新点)
初始化:
我们在读入数据时顺便存下景点有哪些(新开数组),这样后面处理的时候会方便很多,
然后根据我们存景点的顺序为景点编号,
首先memset(f,127,sizeof(f));
然后将每个景点对应的状态赋为0(就是自己到自己无需代价)
注意到第一个方程我们只需枚举子集即可,但是要求子集已经是最优
同时注意到第二个方程,它的转移方向是不明确的,即a可以到b,b也可以到a,所以就不方便直接枚举了,
这里我们采用spfa来维护它。
但是注意到上一个方程的要求:子集已经是最优,
因此我们需要每到一个状态,就调用一次spfa,来在当前状态下更新整张图,使得子集为最优。
具体细节看代码吧。
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define R register int 4 #define AC 1050 5 #define inf 1061109567//直接用127太大了,会爆 6 #define ac 15 7 int n,m,tot,all,head,tail; 8 int s[ac][ac],f[ac][ac][AC]; 9 int a[6]={0,1,-1,0,0},b[6]={0,0,0,1,-1}; 10 bool z[150][150],vis[150][150]; 11 12 struct node{ 13 int x,y; 14 }sights[ac],q[AC]; 15 16 struct Path{ 17 int x,y,s; 18 }path[ac][ac][AC]; 19 20 /*f[i][j][k],表示在i,j状态为k的最小代价,只能向相邻的转移 , 21 为什么要按层spfa???? 22 观察到按点转移的方程中,枚举的是子集,也就是说,当前状态的子集必须先求出, 23 因此要按层来spfa?*/ 24 void pre() 25 { 26 scanf("%d%d",&n,&m); 27 for(R i=1;i<=n;i++) 28 for(R j=1;j<=m;j++) 29 { 30 scanf("%d",&s[i][j]); 31 if(!s[i][j]) sights[++tot].x=i,sights[tot].y=j; 32 } 33 all=(1 << tot) - 1; 34 memset(f,63,sizeof(f)); 35 for(R i=1;i<=tot;i++) 36 { 37 int x=sights[i].x,y=sights[i].y; 38 f[x][y][1 << (i - 1)]=0;//到自己当然没代价了 39 } 40 } 41 42 void spfa(int S)//DP 43 { 44 int x,y; node now; 45 while(head < tail) 46 { 47 x=q[++head].x,y=q[head].y; 48 vis[x][y]=false; 49 for(R i=1;i<=4;i++) 50 { 51 now.x=x + a[i], now.y=y + b[i];//error!!!now.y是+ b[i]不是+a[i]啊 52 if(now.x < 1 || now.x > n || now.y < 1 || now.y > m) continue; 53 if(f[now.x][now.y][S] > f[x][y][S] + s[now.x][now.y])//error!!!是大于号啊。。。 54 { 55 f[now.x][now.y][S] = f[x][y][S] + s[now.x][now.y]; 56 path[now.x][now.y][S] = (Path){x,y,S}; 57 if(!vis[now.x][now.y]) q[++tail]=now,vis[now.x][now.y]=true; 58 } 59 } 60 } 61 head = tail = 0;//重置一次防止越界 62 } 63 64 void work()//更新 65 { 66 int now; 67 for(R S=1;S<=all;S++) 68 { 69 for(R i=1;i<=n;i++) 70 for(R j=1;j<=m;j++)//枚举每个点 71 { 72 for(R k=S & (S - 1); k ;k = (k - 1) & S)//其实原版是k = S; k ;k=(k - 1) & S 73 {//感性的理解都是相当于从0到S枚举了每一个数,然后&S保证是子集,用k-1这样类似递归的转移来跳过某些重复部分 74 now=f[i][j][k] + f[i][j][S ^ k] - s[i][j];//因为都是从同一个点出发的,而一个点的代价只能算一次,所以-s[i][j] 75 if(now < f[i][j][S]) 76 f[i][j][S] = now , path[i][j][S] = (Path){i,j,k};//记录路径 77 } 78 if(f[i][j][S] != inf) 79 q[++tail]=(node){i,j} , vis[i][j]=true;//是++tail啊 80 } 81 spfa(S); 82 } 83 } 84 85 #define t path[x][y][S] 86 void dfs(int x,int y,int S)//搜索方案 87 { 88 if(!x || !t.s) return ;//没有上一个了就退出 89 z[x][y]=true;//表示有志愿者 90 dfs(t.x,t.y,t.s);//找当前状态的上一个 91 if(t.x == x && t.y == y) dfs(x,y,S ^ t.s);//如果是当前节点,将会有分叉 92 } 93 #undef t 94 95 void getans()//获取答案 + 输出 96 { 97 dfs(sights[1].x,sights[1].y,all); 98 printf("%d\n",f[sights[1].x][sights[1].y][all]); 99 for(R i=1;i<=n;i++) 100 { 101 for(R j=1;j<=m;j++) 102 { 103 if(!s[i][j]) printf("x"); 104 else if(z[i][j]) printf("o"); 105 else printf("_"); 106 } 107 printf("\n"); 108 } 109 } 110 111 int main() 112 { 113 // freopen("in.in","r",stdin); 114 pre(); 115 work(); 116 getans(); 117 // fclose(stdin); 118 return 0; 119 }