洛谷P17461 [GESP202609 八级] 生成树计数 题解

PART 1:题目描述

题目传送门:P17461 GESP202609 八级 生成树计数

题目描述

给定一张有 \(n\) 个顶点 \(m\) 条边的无向连通图 \(G\),顶点依次以 \(1,2,\ldots,n\) 编号。\(G\) 有以下特殊的性质:

  • \(G\) 中的每条边至多属于一个简单环。
  • \(G\) 中没有重边与自环。

简单环是指环中顶点互不相同,且不经过重复边的回路。

请你求出 \(G\) 的不同生成树的数量。两棵生成树不同,当且仅当存在一条边在其中一棵生成树中出现,而不在另一棵生成树中出现。

由于答案可能很大,你只要求出答案对 \(998244353\) 取模的结果。

输入格式

第一行,两个正整数 \(n,m\),分别表示 \(G\) 的顶点数与边数。

接下来 \(m\) 行,每行两个整数 \(u_i,v_i\),表示一条连接顶点 \(u_i,v_i\) 的无向边。

输出格式

输出一行,一个整数,表示 \(G\) 的不同生成树的数量对 \(998244353\) 取模的结果。

输入输出样例 #1

输入 #1

复制代码
7 8
1 2
2 3
3 1
3 4
4 5
5 6
6 7
7 4

输出 #1

复制代码
12

输入输出样例 #2

输入 #2

复制代码
5 4
1 2
1 3
2 4
2 5

输出 #2

复制代码
1

说明/提示

对于 \(40\%\) 的测试点,保证 \(1\le n\le 8\),\(1\le m\le 10\)。

对于 \(60\%\) 的测试点,保证 \(1\le n\le 2000\),\(1\le m\le 2000\)。

对于所有测试点,保证 \(1\le n\le 10^5\),\(1\le m\le 10^5\),\(1\le u_i,v_i\le n\)。

PART 2:解题思路

2.1题目大意

给定一张 \(n\) 个点、\(m\) 条边的无向连通图,保证每条边至多属于一个简单环(即仙人掌图)。求这张图的不同生成树数量,对 998244353 取模。

数据范围:\(1\le n,m\le 10^5\)

2.2什么是仙人掌图

仙人掌图就是一种长得像仙人掌的连通图:

图里所有的环(封闭的圈)互相绝不共用边,最多只在一个顶点处碰头,其余没成环的边就像是连接这些环的"刺"或"茎"。

2.3核心结论

结论:

\\boxed{ \\text{生成树数量} = \\prod_{\\text{每个简单环}}(\\text{环的长度})\\pmod{998244353}}

证明:

1. 桥边必须连通,如果桥边去掉,图就不连通了,无法形成生成树。

2. 每个环只能恰好删去一条边:

  • 如果一条边都不删,生成树中就会包含环,不再是树。

  • 如果删去\(≥2\)条边,就会有节点与图断开,无法形成生成树。

验证:

样例一:

图中存在两个环:

  • 环\(1-2-3\),环的长度为3

  • 环\(4-5-6-7\),环的长度为4

  • 答案为\(3×4=12\)。

样例二:

图中无环,答案为1.

PART 3:算法实现找环

使用 \(DFS\) 遍历图,记录每个点的访问时间 vis\[u\] 和在 \(DFS\) 树中的深度 \(depu\)(根节点深度为 \(0\))。

如何判断是否找到了环?

对于节点\(u\):

  • 如果遇到来时的边,跳过。

  • 如果遇到未访问过的点,继续\(DFS\)。

  • 如果遇到访问时间比当前节点早的节点,说明此时已经发现了一个环。

如何计算环的长度?

\(dep\) 记录的是该点在 DFS 树上的层数。回边 \(u−v\) 与 DFS \\(树上\\) u 到 \(v\) 的路径恰好构成了这个环。环上的边数就是它们在树上的深度差加上回边本身。用树的深度差值 dep[u] - dep[v] + 1

PART 4:代码实现

AC CODE

复制代码
#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
vector<pair<int,int> >e[100005];//存储节点编号与无向边编号
int n,m,cnt=0;//cnt记录访问时间
long long ans=1;//ans记录答案,初始为1
int vis[100005],dep[100005];//vis记录访问时间,dep记录节点深度
void dfs(int now,int fa,int d) {//now当前节点,fa来时边的编号,d当前深度
    cnt++;//访问时间+1
    vis[now]=cnt;//记录访问时间
    dep[now]=d;//记录节点深度
    for(auto i:e[now]) {//枚举当前节点可达的节点
        if(i.second==fa) continue;//如果遇到来时的边,跳过。
        if(!vis[i.first]) dfs(i.first,i.second,d+1);//如果遇到未访问的节点,继续DFS
        else if(vis[i.first]<vis[now]){//如果发现环
            int l=dep[now]-dep[i.first]+1;//计算环的长度
            ans=ans*l%mod;//ans累乘
        }
    }
}
int main() {
    cin>>n>>m;//输入节点数与边数
    for(int i=1;i<=m;i++) {
        int u,v;cin>>u>>v;//输入无向边
        e[u].push_back({v,i});//无向边,双向存储 
        e[v].push_back({u,i});//i为边的编号
    }
    dfs(1,0,0);//初始在节点1
    cout<<ans;//输出答案,记得mod 998244353
    return 0;
}

PART 5:生成树练习题

相关推荐
dear_bi_MyOnly2 小时前
栈和队列:从原理到应用全解析
c语言·开发语言·c++·学习·分类
代钦塔拉3 小时前
【C++/Qt 进阶】设计模式系列(4):原型模式——用“克隆“代替“重新构造“
c++·qt·设计模式
无忧.芙桃4 小时前
C++语言原理与实践(十三):stack、queue 和 priority_queue的介绍与使用
开发语言·c++·栈·队列
TDTC_XQ5 小时前
模板类的写法
linux·c++
CCCCCCCCharlie6 小时前
深入解析I/O底层:文件描述符与缓冲区真相
linux·开发语言·c++·算法
自信150413057596 小时前
重生之从0开始学习c++之string(下)
c++·学习·stl
wuminyu7 小时前
Continuation::freeze与thaw的x86_64汇编Stack Chunk拆解源码分析
java·linux·c语言·汇编·jvm·c++
kunicoding7 小时前
C++ 策略模式实战:写一个可以“一键换输出“的日志系统
开发语言·c++·策略模式