Interconnect【POJ 3156】【哈希+期望dp详解】

版权声明:https://blog.csdn.net/qq_41730082 https://blog.csdn.net/qq_41730082/article/details/86288922

题目链接


  很好的一道题,真的捋清了什么是真正的期望,然后这道题有很多人拿的是位运算做的…… (或许我觉得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

猜你喜欢

转载自blog.csdn.net/qq_41730082/article/details/86288922