题解 P3343 【[ZJOI2015]地震后的幻想乡】

shadowice1984

2018-02-02 22:02:47

Solution

期望神题…… 这道题和今年NOIPD2T2极度神似,不知道NOIP2018要毒瘤到哪里去了 我们首先见到这道题,第一反应是——不会 所以我们要写暴力。 其实,如果知道边的排名的话,跑kruskal就好了对吧 但是我们不知道排名 所以暴力枚举边的全排列,跑kruskal,用题目中给的结论更新答案即可 复杂度n\*m! 显然太暴力了,但是如果你记得NOIP2017D2T2的话,会发现它也有一个类似的全排列枚举+贪心的做法 然后优化呢? 子集DP,是子集DP,全排列之所以复杂度高,是因为它给了我们许多不需要的信息,对于这道题,我们只需两个信息,一是状态,二是联通性 从而把期望问题转化为计数问题 那么这个是可以状压的,当然,你会发现设f\[s]\[i]表示点集s全部联通,最大边排名为i是不可转移的 所以这里有个操作叫正难则反 我们设dp\[k]\[i]\[s](k为0/1变量)(s为二进制串)为加入前i条边时 点集s联通/不连通的概率。 先解释一下为什么变成了前i条边,因为这里要求的代价是**最大边最小**,而不是总和最小,所以平常做kruskal中被“跳过”的边,此时是无需付出代价的,所以也可以记为一种方案 转移的话,我们可以枚举其中的一个顶点,哪一个无所谓,只是在求dp[0][s][i]的时候这个点p要不变,枚举s的子集,该子集必须包含p 那么dp[0][s][i]=sigma(dp[1][s'][j]\*c[i-j][size[s^s']]) 其中size为一个点集中边的数目 那么,这是什么意思呢?,相当于我们依照点p将这个集合剖为两半了 而我们是在枚举这个不联通集合联通块的大小 由于s^s'只能在本身内部连边,所以,s^s'一定和s'不联通,所以s一定不连通,而且因为枚举所有的“分割线”所以不重不漏的枚举了所有方案 另一个性质dp[0][i][j]+dp[1][i][j]=c[i][size[j]] 这个东西应该是显然?好吧还是解释下:因为已经把问题转化为了一个边可以全取问题,也就是说已经不是树了,因此边可以想怎么取就怎么取,除了联通不联通就没有了别的限制,因此dp[0]与dp[1]覆盖了所有的组合数 那么我们就可以愉快的递推了,这里有一个操作是枚举子集的,如果要枚举s的子集,那么可以用这样的语句: ```C for(int k=(s-1)&s;k>0;k=(k-1)&s); ``` 就可以愉快的枚举子集了 最后,是如何生成答案,很遗憾,答案并不能由dp[1]得到,为什么? 因为把原问题反了一下,我们求出来的是成功联通的方案,而不是什么 最小生成树,想必联通图和树之间的差别还是极大的 那么为了得出答案,我们可能也要反一下原问题,想一下,现在尽管是无条件加边,但是如果在加第X条边的时候刚好联通了,这时的联通图,对应的最小生成树最大边也是x. 也是说,连到第X条边时刚好联通的**联通图**,与最大边为X的最小**生成树**,**一一对应** 那么我们现在要求最大边的期望: 1.设最大边一种可能的排名取值为xi 2.设某一取值xi出现的概率为P(xi) 3.设x的取值不比x0小的概率为p(x>=x0) 4.设点集的全集为U 那么最大边的排名的期望就是: sigma(xi\*p(xi)) \[i∈(1~m)] 对上述式子求一个前缀和,上式等价于(不能理解就记成结论好咯) sigma(p(x\>=i))\[i∈(1~m)] 反一下,等价于 sigma(dp\[0]\[i]\[U]/c\[i]\[m]) \[i∈(0~m)] ok,毕竟取到**i不连通**的概率是等价于**最大边大于i**概率的 这样就求出了边排名的期望,除个m+1就行了 上代码~ ```C #include<cstdio> #include<algorithm> #include<cmath> using namespace std; typedef long long ll; ll dp[2][60][2200];ll size[2200];bool book[2200]; int n;int m;ll c[60][60];int up; int map[20][20];double res; int main() { scanf("%d%d",&n,&m);up=pow(2,n)-1; for(int i=1;i<=m;i++) { int u;int v; scanf("%d%d",&u,&v); map[u-1][v-1]=1;map[v-1][u-1]=1;//用邻接矩阵存 } for(int i=1;i<=up;i++) { for(int p1=1;p1<n;p1++) { for(int p2=0;p2<p1;p2++) { size[i]+=((i>>p1)&1)&&((i>>p2)&1)&&map[p1][p2];//暴力计算size } } } c[0][0]=1; for(int j=1;j<=m+1;j++)//递推杨辉三角 { c[0][j]=1; for(int i=1;i<j;i++) { c[i][j]=c[i][j-1]+c[i-1][j-1]; } c[j][j]=1; } for(int i=0;i<=up;i++){dp[0][0][i]=1;} for(int i=0;i<n;i++){dp[1][0][1<<i]=1;dp[0][0][1<<i]=0;} //边界条件,显然一个点是联通的 for(int i=1;i<=m;i++) { for(int j=1;j<=up;j++) { int p1=0;for(;((j>>p1)&1)==0;p1++);//随便选一个剖分点 for(int k=(j-1)&j;k>0;k=(k-1)&j)//枚举子集 { if(((k>>p1)&1)==0)continue;//不包含就算了 for(int rk=0;rk<=i;rk++)//枚举补集可用的边数 { dp[0][i][j]+=dp[1][rk][k]*c[i-rk][size[j^k]]; } } dp[1][i][j]=c[i][size[j]]-dp[0][i][j];//同时求出dp[1] } } for(int i=0;i<=m;i++){res+=(double)dp[0][i][up]/(double)(c[i][m]);}//处理出概率 printf("%lf",(double)res/(double)(m+1));return 0;//拜拜程序~ } ```