题解:P11834 [省选联考 2025] 岁月(补充题解)
xzf_200906 · · 题解
upd on 2025.03.08:修改了一处笔误并补充了一个内容。
此篇文章是对另一篇题解从性质 C 扩展到正解的“细枝末节的地方”的补充,下文将默认读者看过另一篇题解。
在看完那篇题解之后,如果读者已经知晓此题的解法,那么请退出此文章,因为这篇文章已经没用了。如果读者还不清楚性质 C 怎么做或者不知道如何判定一张图合法,那么请回去再仔细阅读。如果读者已经知晓上述内容,但不知道如何将性质 C 的解法套用到正解上(即那篇文章的倒数第二段不清楚),那么请往下看。
枚举边权,将所有边权相同的边的两端所在的连通块合并。注意,在不加说明的情况下,下面所称的连通块为合并之前的连通块。假设将
- 对于
s 中的每个元素,其在原来的连通块中合法。 -
- 若将每个连通块
S_i 看作一个点,将在不同的连通块之间且终点合法的边加入后,每个和s 有交的连通块S_i 均可以到达其它所有点。 - 按照条件 3 处理后,每个和
s 不交的连通块S_i 均不完全能到达其它点。
下文将分部分讨论上述条件。
Part 2
大体和 C 性质的部分相同,但由于我们已知
- 由于在原来的连通块中的合法点集组成一个强连通分量,故对于 DAG 中的任意两个点,它们所代表的强连通分量所在的连通块不交。
- 对于其中的任意两个强连通分量
u,v ,记ext_u 表示和u 中任意一个点在同一个连通块的点组成的集合,若不存在边u\to v ,则原图中不存在边p\to q 满足p\in ext_u,q\in v 。
记
解释一下上面的式子,由于需要保证一个连通块不涉及到两个强连通分量,则需要计算
Part 4
最简单的一个,枚举所有和集合无交的连通块,统计起点在该连通块内,终点在
Part 1&3
类似于性质 C,先考虑一个较为暴力的算法。枚举一个集合
Q:为什么要这样设计状态而不是定义为直接从
r\to s 的概率?A:因为 Part 3 中的点是原来的一整个连通块,如果
s 是指的原图中的点集,那么这样的状态是没意义的。如果s 指的是可达的连通块组成的集合,那么由于要求每条边的终点在原来合法,这就意味着原来的合法点会影响转移系数,但是我们不可能在每次转移f_t\to f_s 时都枚举哪些点合法(存疑,如果有人有思路的话请踢我),所以就这样设计状态了。本质上
f_s 实际上记录了两个状态:哪些连通块可达和已经可达的连通块中有哪些点合法,但是由于从后面的信息可以推出前面的信息,所以就只要记录后面的东西。
由于一个连通块以前的合法点现在要么全不合法,要么全合法,故只有
由于我们已经确定了
但是按上述转移方程暴力计算是不优的,联想到在 C 性质中的优化,上述 DP 本质上相当于建立一张边权为
在计算完上述概率后,由于它们之间互相独立,则
附一份加上了注释的部分上述题解的代码:
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());
}