题目链接
很好的一道题,真的捋清了什么是真正的期望,然后这道题有很多人拿的是位运算做的…… (或许我觉得unsigned long long比较的简单吧,但是由于它容易被hack,所以我打了双哈希)。
那么好的题,好多人都是一笔带过,但是这道题是真的有点东西的,我们知道题目中给到的,一共有N个点,并且有M条边已经是连好的,但是由于题目中说的那样,我们还有可能会链接到已经连上的边,所以,我们得考虑它的期望应当是如何去算。
先是状态的处理,我用的是双哈希加上map<>用以存储,然后,哈希表示的状态表示我们已经放进去的状态方程,就是譬如有一棵拥有x个节点的树,那么对应的hash[x]上就会放进去了。(相当于把2进制的状态压缩,看作哈希了)
那么,先考虑到它们的状态,既然是期望就要从最后一个状态开始返回来推回第一个状态,所以用的是搜索+回溯的思想,我们从状态1开始,往后推所有的状态,状态1就是题中给予我们的状态,就是存在着几棵树,并且,每棵树有一定的节点数,我们存储这样的节点数即可,因为处理也就是处理这样的节点数的。接下来,就是状态往后推了,我们看到对于每个状态,我们每次链接一条边,然后查询所有的可能状态会是怎样的,然后就是往下推这个状态,看到它到满状态需要多少期望步数,然后满状态的步数是0,以此为基础,我们回溯。那么推回到上一状态,应当是多少的值?
此时就要涉及到期望DP的方程了,我们从后往前推,那么我们知晓了后面的状态,然后回到前面,知道目前的状态S,与下个的状态new_S,“dp[S] = ( K1 * dp[new_S[1]] + K2 * dp[new_S[2]] + ……+ Kn * dp[new_S[N]] ) / ( N * (N - 1) / 2 ) + 1;”,在这里,每个对应的K表示的是到下一个对应的状态能有多少的路可以走,这里的路指的是把两棵树连在一起的时候,可行的方案数,然后后面的dp[new_S[i]]表示的是下一个状态的期望,然后,我们可以将这个dp方程转化为:知道所有后面的状态,推至目前状态,那么相当于反了一下,可以看作是后面的(期望乘以总的有效链接边的对应总数 + 所有的边)/(有效连接边的总数)。
剩下的,我写了注释……
#include <iostream>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <limits>
#include <vector>
#include <stack>
#include <queue>
#include <set>
#include <map>
#define lowbit(x) ( x&(-x) )
#define pi 3.141592653589793
#define e 2.718281828459045
#define INF 0x3f3f3f3f
#define SonG_y main
#define MP(x, y) make_pair(x, y)
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const int maxN = 40;
//const ull Hash_1 = 37, Hash_2 = 31; //避免hack,双哈希
const ull Hash_1 = 1e9 + 7, Hash_2 = 1e9 + 9; //避免hack,双哈希
int N, M, root[maxN], sum[maxN]; //加一个带权值的并查集,用以知晓已经有多少答案在里面了
int tot; //记下总的方案数,两两链接一条边的话,我们会发现是(N-1,N-2,…… ,1)的总和————(N-1)*N/2
ull num1[maxN], num2[maxN], BiH_1[maxN], BiH_2[maxN];
map<pair<ull, ull>, int> match;
int cnt;
struct node
{
int to[maxN]; //用以记录对应索取边的数量;其中to[0]用作记忆化使用,是否已经遍历过就由其决定
double val; //记录期望之用……
node()
{
memset(to, 0, sizeof(to));
val = 0.;
}
}edge[10007];
int fid(int x) { return x == root[x]?x:(root[x] = fid(root[x])); }
void mix(int x, int y)
{
int u = fid(x), v = fid(y);
if(u != v)
{
root[u] = v;
sum[v] += sum[u];
}
}
/*--开始处理期望--*/
double solve(ull S1, ull S2) //期望得倒过来求,所以用初始状态逐一推到末状态(完全状态),然后从后往前走
{
int pos = match[MP(S1, S2)];
if(edge[pos].to[0] == 1) return edge[pos].val; //记忆化的过程,我们用to[0]用作记忆化,如果的的确确是来过的,就直接返回此时的概率,因为,此时对应的pos是状态位
int tmp = 0; //记录总共的处理数量
double ret = 0.;
ull new_S_1 = 0, new_S_2 = 0;
edge[pos].to[0] = 1; //记忆化
if(S1 == BiH_1[N] && S2 == BiH_2[N]) //此时就是满状态,已经有了所有的边
{
edge[pos].val = 0.;
return 0.;
}
for(int i=1; i<=N; i++)
{
if(edge[pos].to[i] > 0) //内部的点有i个了,去找寻外面的世界。但是只能向上去寻找跟多节点的树,因为少节点的树都已经遍历过了,此时向外找即可
{
for(int j=i+1; j<=N-i; j++) //补成一颗完整的树
{
if(edge[pos].to[j] > 0) //数目为j的树存在,我们把他们两个链接在一起,得到一颗新的树
{
new_S_1 = S1 - BiH_1[i] - BiH_1[j] + BiH_1[i+j]; //得到的是下一刻的状态,我们链接了一些边,达到一个更大的联通图,并且,这个时候,我们会多出来一棵(i+j)数量节点的树
new_S_2 = S2 - BiH_2[i] - BiH_2[j] + BiH_2[i+j];
if(match[MP(new_S_1, new_S_2)] == 0) //得到一个新的状态,此时我们就要处理这样的一种状态了
{
match[MP(new_S_1, new_S_2)] = ++cnt;
for(int k=1; k<=N; k++)
{
edge[cnt].to[k] = edge[pos].to[k]; //新状态的继承前一刻的状态,并且将会在前一个状态下继续修改每个对应树中的点的状态
}
edge[cnt].to[i]--; //前两个状态已经和在了一起,内部节点数为i的点少了一棵树
edge[cnt].to[j]--; //两个状态和加,内部节点数为j的点也少了一棵树
edge[cnt].to[i+j]++; //得到更多的边了,我们的状态也大了
}
int bibibi = edge[pos].to[i] * i * edge[pos].to[j] * j;
ret += 1.*bibibi * solve(new_S_1, new_S_2);
tmp += bibibi;
}
}
}
}
for(int i=1; i<=N; i++) //处理内部
{
if(edge[pos].to[i] >= 2) //值为i的树的量大于1的话,就说明它们两个也可以链接在一起的状态
{
new_S_1 = S1 - 2*BiH_1[i] + BiH_1[i<<1]; //新树的节点翻倍了,多出来的是新的树(是两树之和)
new_S_2 = S2 - 2*BiH_2[i] + BiH_2[i<<1];
if(match[MP(new_S_1, new_S_2)] == 0)
{
match[MP(new_S_1, new_S_2)] = ++cnt;
for(int k=1; k<=N; k++)
{
edge[cnt].to[k] = edge[pos].to[k]; //与上面一样,赋值,并且更新
}
edge[cnt].to[i] -= 2; //两个等量节点的和
edge[cnt].to[i*2]++;
}
int bibibi = edge[pos].to[i] * i; //内部的边相互走过
int lalala = bibibi*(bibibi - 1)/2 - i*(i - 1)/2*edge[pos].to[i];
ret += 1.*lalala * solve(new_S_1, new_S_2);
tmp += lalala;
}
}
edge[pos].val = 1.*(ret + tot)/(1.*tmp);
return edge[pos].val;
}
/*--------------*/
inline void pre_did()
{
BiH_1[0] = BiH_2[0] = 1;
for(int i=1; i<maxN; i++)
{
BiH_1[i] = BiH_1[i-1] * Hash_1; //这里的哈希值的是读取位数时候使用,用以“<<i”等效使用,或者向前移动固定的位数
BiH_2[i] = BiH_2[i-1] * Hash_2; //打双哈希保证稳定
}
}
inline void init()
{
match.clear(); cnt = 0;
memset(edge, 0, sizeof(edge));
tot = N*(N-1)/2;
for(int i=1; i<=N; i++) //带权值的并查集,记录有多少条边,记忆化……
{
root[i] = i;
sum[i] = 1;
}
}
int SonG_y()
{
pre_did();
while(scanf("%d%d", &N, &M)!=EOF)
{
init();
for(int i=1; i<=M; i++)
{
int e1, e2;
scanf("%d%d", &e1, &e2);
mix(e1, e2);
}
ull State_1 = 0, State_2 = 0;
for(int i=1; i<=N; i++)
{
if(root[i] == i) //对于所有的树,我们对森林的根进行处理,把每棵树的树下的点放进去,接下来链接的时候,也就是那么多的点,找上对应的关系,这棵树的点,连上对面的树的点,与旗下节点数目有关
{
edge[1].to[sum[i]]++;
}
}
if(edge[1].to[N]) { printf("%.6lf\n", 0.); continue; } //第N位有值,说明已经全部放进来了,都在一起了,就不需要再额外开空间了
for(int i=1; i<=N; i++)
{
State_1 += edge[1].to[i] * BiH_1[i]; //取到了几位,将边放入其中
State_2 += edge[1].to[i] * BiH_2[i]; //一样的,做hash
}
match[MP(State_1, State_2)] = ++cnt;
printf("%.10lf\n", solve(State_1, State_2));
}
return 0;
}
/*
5 3
3 4
3 5
5 4
ans:3.8095238095
*/
对了,贴一组测试样例:
5 3
3 4
3 5
5 4
ans:3.8095238095