专栏文章

题解:P11834 [省选联考 2025] 岁月(补充题解)

P11834题解参与者 2已保存评论 1

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
1 条
当前快照
1 份
快照标识符
@miq18asl
此快照首次捕获于
2025/12/03 21:17
3 个月前
此快照最后确认于
2025/12/03 21:17
3 个月前
查看原文
upd on 2025.03.08:修改了一处笔误并补充了一个内容。
此篇文章是对另一篇题解从性质 C 扩展到正解的“细枝末节的地方”的补充,下文将默认读者看过另一篇题解。
在看完那篇题解之后,如果读者已经知晓此题的解法,那么请退出此文章,因为这篇文章已经没用了。如果读者还不清楚性质 C 怎么做或者不知道如何判定一张图合法,那么请回去再仔细阅读。如果读者已经知晓上述内容,但不知道如何将性质 C 的解法套用到正解上(即那篇文章的倒数第二段不清楚),那么请往下看。
枚举边权,将所有边权相同的边的两端所在的连通块合并。注意,在不加说明的情况下,下面所称的连通块为合并之前的连通块。假设将 S1,S2,,SkS_1,S_2,\cdots,S_k 合并成了一个,我们已经求出了原来的每个连通块中的每一个集合 ss 成为该连通块的合法点集的概率 ressres_s,考虑如何通过它来求出新的 resres。类比性质 C,ss 合法当且仅当:
  1. 对于 ss 中的每个元素,其在原来的连通块中合法。
  2. ss 中的点强连通。
  3. 若将每个连通块 SiS_i 看作一个点,将在不同的连通块之间且终点合法的边加入后,每个和 ss 有交的连通块 SiS_i 均可以到达其它所有点。
  4. 按照条件 3 处理后,每个和 ss 不交的连通块 SiS_i 均不完全能到达其它点。
下文将分部分讨论上述条件。

Part 2

大体和 C 性质的部分相同,但由于我们已知 ss 中的所有点合法,那么对于任意的 usu\in s,其可以到达其所在连通块的所有点。在 DP 的过程中我们会对缩点后有多个点的 DAG 进行计数。那么这张 DAG 一定满足以下条件:
  • 由于在原来的连通块中的合法点集组成一个强连通分量,故对于 DAG 中的任意两个点,它们所代表的强连通分量所在的连通块不交。
  • 对于其中的任意两个强连通分量 u,vu,v,记 extuext_u 表示和 uu 中任意一个点在同一个连通块的点组成的集合,若不存在边 uvu\to v,则原图中不存在边 pqp\to q 满足 pextu,qvp\in ext_u,q\in v
ok(s,t)=[extsextt=]ok(s,t)=[ext_s\cap ext_t=\emptyset]anssans_sss 在所有点在原来合法的前提下为强连通图的概率,则有如下转移方程: anss=1(gsanss)tsok(st,t)gt2cross(extst,t)ans_s=1-(g_s-ans_s)-\sum_{\emptyset \neq t\sub s}ok(s\setminus t,t) g_t2^{-cross(ext_{s\setminus t},t)} gs=ansslowbit(s)tsok(st,t)anstgst2(cross(extst,t)+cross(extt,st))g_s=ans_s-\sum_{lowbit(s)\in t\subseteq s}ok(s\setminus t,t)ans_tg_{s\setminus t}2^{-(cross(ext_{s\setminus t},t)+cross(ext_{t},s\setminus t))} 解释一下上面的式子,由于需要保证一个连通块不涉及到两个强连通分量,则需要计算 okok 函数。在计算 gsg_s 时需要在集合 sts\setminus t 的基础上并上 tt,且 tt 和之前的所有点都没有边,则有 cross(extst,t)+cross(extt,st)cross(ext_{s\setminus t},t)+cross(ext_{t},s\setminus t) 条边不能存在。而在计算 anssans_s 的时候需要保证 sts\setminus t 没有连向 tt 的边,故有 cross(extst,t)cross(ext_{s\setminus t},t) 条边不能存在,故有以上转移系数。如果有其它部分不能看懂请看此题

Part 4

最简单的一个,枚举所有和集合无交的连通块,统计起点在该连通块内,终点在 ss 中的边的数量。在 ss 中的所有点在原来合法的前提下,ss 符合该条件当且仅当这些边不存在。设 cntscnt_s 为这些边的数量,则在该前提下 ss 符合该条件的概率为 2cnts2^{-cnt_s}

Part 1&3

类似于性质 C,先考虑一个较为暴力的算法。枚举一个集合 rr 并分别计算其符合条件的概率。设 fsf_s 表示 ss 中的点在原来全部为合法点且 rr 可以到达所有与 ss 有交的连通块的概率。
Q:为什么要这样设计状态而不是定义为直接从 rsr\to s 的概率?
A:因为 Part 3 中的点是原来的一整个连通块,如果 ss 是指的原图中的点集,那么这样的状态是没意义的。如果 ss 指的是可达的连通块组成的集合,那么由于要求每条边的终点在原来合法,这就意味着原来的合法点会影响转移系数,但是我们不可能在每次转移 ftfsf_t\to f_s 时都枚举哪些点合法(存疑,如果有人有思路的话请踢我),所以就这样设计状态了。
本质上 fsf_s 实际上记录了两个状态:哪些连通块可达和已经可达的连通块中有哪些点合法,但是由于从后面的信息可以推出前面的信息,所以就只要记录后面的东西。
由于一个连通块以前的合法点现在要么全不合法,要么全合法,故只有 rsr\subseteq s 且不存在一个连通块同时包含 rrsrs\setminus rfsf_s 才有意义。则考虑枚举 tst\sub s,计算 rr 只能到达 tt 的概率并从总概率中减去。设 cofscof_s 表示 ss 中的点在之前全部为关键点的概率,则: fs=cofsrtsok(t,st)ft2cross(extt,st)cofstf_s=cof_s-\sum_{r\subseteq t\sub s}ok(t,s\setminus t)f_t2^{-cross(ext_t,s\setminus t)}cof_{s\setminus t} 由于我们已经确定了 tt 中和其相交的每一个连通块中的合法点,不可以对其再作修改,所以仍然要计算 ok(t,st)ok(t,s\setminus t)
但是按上述转移方程暴力计算是不优的,联想到在 C 性质中的优化,上述 DP 本质上相当于建立一张边权为 ft2cross(extt,st)cofst-f_t2^{-cross(ext_t,s\setminus t)}cof_{s\setminus t} 的边,求每条起点为 prp\supe rok(r,pr)=1ok(r,p\setminus r)=1,终点为全集的路径经过的边权之积乘上 cofpcof_p 的和。考虑倒着跑 DP,求出每个点到全集的路径权值和,再乘上 cofpcof_p,最后再计算 tfs=tsok(s,ts)fttf_s=\sum_{t\supe s} ok(s,t\setminus s) f_t 即可。
在计算完上述概率后,由于它们之间互相独立,则 ress=anss×2cnts×tfsres_s=ans_s\times 2^{-cnt_s}\times tf_s,最终的答案为 ress\sum res_s
附一份加上了注释的部分上述题解的代码:
CPP
int fa[25],bel[2][25],_msk[25],ext[2][1 << 15],con[1 << 15];
//_msk: 连通块的点集
//bel: 上一时刻和这一时刻和该点同连通块的点
//ext: 两时刻该集合同连通块的点
//con:这一时刻集合内部是否为同一连通块
int fnd(int u){
    if(fa[u] == u) return u;
    return fa[u] = fnd(fa[u]);
}

int mrg(int u,int v){
    u = fnd(u);v = fnd(v);
    if(u == v) return 0;
    fa[u] = v;
    return 1;
}

void init(){
    rep(u,0,n - 1) _msk[u] = 0;
    rep(u,0,n - 1) _msk[fnd(u)] |= 1 << u;
    fill(con,con + (1 << n),0);
    rep(u,0,n - 1) bel[1][u] = _msk[fnd(u)];
    con[0] = 1;
    rep(u,0,n - 1){
        for(int S = _msk[u];S;S = (S - 1) & _msk[u]) con[S] = 1;
    }
    rep(S,1,(1 << n) - 1){
        int u = ctz(S);
        ext[1][S] = ext[1][S - (1 << u)] | bel[1][u];
    }
}

int cross(int S,int T){
    assert((S & T) == 0);
    return sum[S + T] - sum[S] - sum[T];
}

int ok(int S,int T){
    //上一时刻两集合所属块是否不交
    return (ext[0][S] & ext[0][T]) == 0;
}
Z f[1 << 15],g[1 << 15],tf[1 << 15];

Z ans[1 << 15],cof[1 << 15],dp[1 << 15];
Z res[1 << 15];
void solve(){
    scanf("%d%d",&n,&m);
    P[0] = 1;
    rep(i,1,n * n) P[i] = P[i - 1] * 2;
    rep(i,0,n * n) iP[i] = Z(1) / P[i];
    fill(msk,msk + n,0);
    rep(i,1,m){
        scanf("%d%d%d",&E[i].u,&E[i].v,&E[i].w);
        E[i].u--;E[i].v--;
    }
    sort(E + 1,E + m + 1,cmp);
    rep(u,0,n - 1) fa[u] = u;
    init();
    rep(S,0,(1 << n) - 1) res[S] = 0;
    rep(i,0,n - 1) res[1 << i] = 1;
    for(int l = 1,r;l <= m;l = r + 1){
        r = l;
        while(r < m && E[r].w == E[r + 1].w) r++;
        rep(u,0,n - 1){
            swap(bel[0][u],bel[1][u]);
            msk[u] = 0;
        }
        rep(S,0,(1 << n) - 1) swap(ext[0][S],ext[1][S]);
        //保存上一时刻的连通性
        int succ = 0;
        rep(i,l,r){
            if(mrg(E[i].u,E[i].v)) succ = 1;
            msk[E[i].u] |= 1 << E[i].v;msk[E[i].v] |= 1 << E[i].u;
        }  
        init();
        //处理这一时刻的连通性
        if(!succ) continue;
        rep(S,1,(1 << n) - 1){
            int u = ctz(S);
            sum[S] = sum[S - (1 << u)] + popcnt(msk[u] & S);
        }
        //处理集合内的边,sum[S] 为两端均在 S 中的边的数量
        int stable = 0;
        rep(u,0,n - 1) if(bel[0][u] == bel[1][u]) stable |= 1 << u;
        rep(S,0,(1 << n) - 1) ans[S] = cof[S] = dp[S] = 0;
        cof[0] = -1;dp[0] = 1;
        //cof[0] 没有影响
        rep(S,1,(1 << n) - 1){
            if(!con[S] || (S & stable)) continue;
            for(int T = S;T;T = (T - 1) & S){
                if(lowbit(S) == lowbit(T) && ok(S - T,T)) cof[S] -= cof[S - T] * ans[T] * iP[cross(ext[0][S - T],T) + cross(ext[0][T],S - T)]; 
            }
            for(int T = S;T;T = (T - 1) & S){
                if(ok(S - T,T)) dp[S] += dp[S - T] * cof[T] * iP[cross(ext[0][S - T],T)];
                //S - T 必须合法,即 dp[S - T]=1,但实测删去不会出问题
            }
            //由于每个连通块只能有一个点,故需特判 ok(S - T,T)
            //若 u,v 包含在 S 中但不是同一个强连通分量,且不存在边 u->v,则不能存在 u->w->v,
            //其中 w 和 u 为同一连通块,且必有 u->w,故有以上转移
            ans[S] = 1 - dp[S];
            cof[S] += ans[S];
            dp[S] += ans[S];
            //dp[S] ∈ {0,1}
        } 
        //ans[S]:S 在每一个连通块只包含一个强连通分量,且缩点后为孤点的概率
        rep(S,0,(1 << n) - 1){
            g[S] = 0;
            if(!con[S] || (S & stable)) continue;
            int ssum = 0;
            rep(u,0,n - 1){
                if((ext[0][S] >> u) & 1) continue;
                if((ext[1][S] >> u) & 1) ssum += popcnt(msk[u] & S);      
            }       
            g[S] = iP[ssum] * ans[S];
            //g[S]:同时满足 Part 2&4 的概率
        }
        cof[0] = 1;
        rep(S,1,(1 << n) - 1){
            int u = ctz(S);
            cof[S] = cof[S - (bel[0][u] & S)] * res[(bel[0][u] & S)];
        }
        //cof与之前的含义不同,为 S 中的点均在之前为关键点的概率
        rep(S,0,(1 << n) - 1){
            f[S] = 0;
            if(!con[S] || (S & stable)) continue;
            int SS = ext[1][S],fail = 0;
            rep(u,0,n - 1) if(((SS >> u) & 1) && (S & bel[0][u]) == 0) fail = 1;
            f[S] = 1 - fail;
        }
        // S 是否包含合并上的每一个连通块
        per(S,(1 << n) - 1,0){
            if(S & stable) continue;
            for(int T = S - lowbit(S);T;T = (T - 1) & S) if(ok(S - T,T)) f[T] -= iP[cross(S - T,ext[0][T])] * f[S] * cof[S - T];
        }
        //统计路径,每次加入一个新的连通块
        rep(S,0,(1 << n) - 1) f[S] *= cof[S];
        //路径的起始点的权值也要乘上去
        rep(S,0,(1 << n) - 1) tf[S] = 0;
        rep(S,0,(1 << n) - 1){
            if((S & stable)) continue;
            for(int T = S;T;T = (T - 1) & S) if(ok(S - T,T)) tf[T] += f[S];
        }
        rep(S,0,(1 << n) - 1) if((S & stable) == 0) res[S] = tf[S] * g[S];
    }
    Z answer = 0;
    rep(S,1,(1 << n) - 1) answer += res[S];
    printf("%d\n",answer.val());
}

评论

1 条评论,欢迎与作者交流。

正在加载评论...