diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 39f0ced..a6d3c62 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,6 +52,7 @@ jobs: cd /tmp/adodefont apt update -y apt install -y texlive texlive-xetex texlive-lang-chinese latexmk wget unzip + tlmgr install minted wget -q -O source-code-pro.zip https://github.com/adobe-fonts/source-code-pro/archive/2.030R-ro/1.050R-it.zip wget -q -O source-han-serif.zip https://github.com/adobe-fonts/source-han-serif/archive/1.001R.zip wget -q -O source-han-sans.zip https://github.com/adobe-fonts/source-han-sans/archive/2.001R.zip diff --git "a/5-\345\233\276\350\256\272.md" "b/5-\345\233\276\350\256\272.md" index d06be41..0fe2d58 100644 --- "a/5-\345\233\276\350\256\272.md" +++ "b/5-\345\233\276\350\256\272.md" @@ -1,7 +1,10 @@ + # 5-图论 + --- + ## dijkstra ```cpp diff --git a/template.tex b/template.tex index c5db253..e2dc81a 100644 --- a/template.tex +++ b/template.tex @@ -289,6 +289,49 @@ \subsection{sol.cpp}\label{sol.cpp}} } \end{minted} +\hypertarget{ux4e09ux5206}{% +\subsection{三分}\label{ux4e09ux5206}} + +\begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} +double cal() +{ + double l = 0, r = 1e10; + for (int i = 1; i <= 100; i++) + { + double m1 = l + (r - l) / 3; + double m2 = (r - l) / 3 * 2 + l; + if (f(m1) < f(m2)) + l = m1; + else + r = m2; + } // 求最大值 + return f(l); +} + +int cal() +{ + int l = 0, r = 1e10; + while (l + 2 < r) + { + int m1 = l + (r - l) / 3; + int m2 = (r - l) / 3 * 2 + l; + if (f(m1) < f(m2)) + { + l = m1; + } + else + { + r = m2; + } + } + int ans = f(l); + for (int i = l + 1; i <= r; i++) + { + ans = max(ans, f(i)); + } +} +\end{minted} + \hypertarget{ux5b57ux7b26ux4e32}{% \section{1-字符串}\label{ux5b57ux7b26ux4e32}} @@ -669,6 +712,63 @@ \subsection{树状数组}\label{ux6811ux72b6ux6570ux7ec4}} \end{minted} +\begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} +template +struct Fenwick{ + int n; + vector tr; + + Fenwick(int n) : n(n), tr(n + 1, 0){} + + int lowbit(int x){ + return x & -x; + } + + void modify(int x, T c){//单点添加 + for(int i = x; i <= n; i += lowbit(i)) tr[i] += c; + } + + void modify(int l, int r, T c){//区间添加 + modify(l, c); + if (r + 1 <= n) modify(r + 1, -c); + } + + T query(int x){ + T res = T(); + for(int i = x; i; i -= lowbit(i)) res += tr[i]; + return res; + } + + T query(int l, int r){ + return query(r) - query(l - 1); + } + + int find_first(T sum){//和出现的第一位置 + int ans = 0; T val = 0; + for(int i = __lg(n); i >= 0; i--){ + if ((ans | (1 << i)) <= n && val + tr[ans | (1 << i)] < sum){ + ans |= 1 << i; + val += tr[ans]; + } + } + return ans + 1; + } + + int find_last(T sum){ + int ans = 0; T val = 0; + for(int i = __lg(n); i >= 0; i--){ + if ((ans | (1 << i)) <= n && val + tr[ans | (1 << i)] <= sum){ + ans |= 1 << i; + val += tr[ans]; + } + } + return ans; + } + +}; +using BIT = Fenwick; +\end{minted} + \hypertarget{ux5e76ux67e5ux96c6}{% \subsection{并查集}\label{ux5e76ux67e5ux96c6}} @@ -925,6 +1025,22 @@ \subsection{区间最值线段树}\label{ux533aux95f4ux6700ux503cux7ebfux6bb5ux6 } \end{minted} +\hypertarget{ux5355ux8c03ux961fux5217}{% +\subsection{单调队列}\label{ux5355ux8c03ux961fux5217}} + +\begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} +int p[N]; +int head=1,tail=0; +for(int i=1;i<=n;i++){ + if(head<=tail&&p[head]==i-k){//当前0 区间长度大于k时扔掉头部 + head++; + } + while(head<=tail&&a[p[tail]]<=a[i]) tail--;//此时求最大值 + p[++tail]=i; + //则head记录区间内最值 +} +\end{minted} + \hypertarget{ux52a8ux6001ux89c4ux5212}{% \section{3-动态规划}\label{ux52a8ux6001ux89c4ux5212}} @@ -1017,6 +1133,14 @@ \subsection{状态压缩DP}\label{ux72b6ux6001ux538bux7f29dp}} } \end{minted} +枚举s的二进制真子集 + +\begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} +for(int j=s;j;j=(j-1)&s){ + //... +} +\end{minted} + \hypertarget{ux6570ux4f4ddp}{% \subsection{数位DP}\label{ux6570ux4f4ddp}} @@ -1116,52 +1240,75 @@ \subsection{数位DP}\label{ux6570ux4f4ddp}} } \end{minted} +\hypertarget{ux72b6ux538bdpux89e3ux54c8ux5bc6ux987fux56deux8defux95eeux9898}{% +\subsection{状压DP解哈密顿回路问题}\label{ux72b6ux538bdpux89e3ux54c8ux5bc6ux987fux56deux8defux95eeux9898}} + +\begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} + +int dp[(1 << 21)][20]; + +for (int i = 0; i < (1 << (n + 1)); i++) + for (int j = 1; j <= n; j++) + if (dp[i][j]) + for (int k = 1; k <= n; k++) + if (a[j][k] && (((i >> k) & 1) == 0)) + dp[i | (1 << k)][k] = 1; + +\end{minted} + \hypertarget{ux6570ux5b66}{% \section{4-数学}\label{ux6570ux5b66}} \begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} +\hypertarget{ux4e09ux5206-1}{% +\subsection{三分}\label{ux4e09ux5206-1}} + +\begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} +//寻找cal函数的极小值(极大值则把<改为>) +while(l>=1; - + if (n & 1) + ans = ans * a % mod; + a = a * a % mod; + n >>= 1; } return ans; } \end{minted} -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\setcounter{enumi}{1} -\tightlist -\item - exgcd -\end{enumerate} +\hypertarget{exgcd}{% +\subsection{exgcd}\label{exgcd}} \begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} -int exgcd(int a,int b,int &x,int &y) +int exgcd(int a, int b, int &x, int &y) { - if(b==0) + if (b == 0) { - x=1; - y=0; + x = 1; + y = 0; return a; } - int d=exgcd(b,a%b,x,y),x0=x,y0=y; - x=y0; - y=x0-(a/b)*y0; + int d = exgcd(b, a % b, x, y), x0 = x, y0 = y; + x = y0; + y = x0 - (a / b) * y0; return d; } \end{minted} @@ -1172,10 +1319,10 @@ \subsection{线性inv}\label{ux7ebfux6027inv}} \begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} void getinv(int n) { - inv[1]=1; - for(int i=2;i<=n;i++) + inv[1] = 1; + for (int i = 2; i <= n; i++) { - inv[i]=mod-((mod/i)*inv[mod%i])%mod; + inv[i] = mod - ((mod / i) * inv[mod % i]) % mod; } } \end{minted} @@ -1184,33 +1331,35 @@ \subsection{线性inv}\label{ux7ebfux6027inv}} \subsection{分块}\label{ux5206ux5757}} \begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} -int ans=0; - for(int l=1,r;l<=n;l=r+1) - { - r=n/(n/l); - ans+=(r-l+1)*(n/l); - } - cout<n||m<0||n<0)return 0; - return fac[n]*inv[m]%mod*inv[n-m]%mod; + if (m > n || m < 0 || n < 0) + return 0; + return fac[n] * inv[m] % mod * inv[n - m] % mod; } \end{minted} -\hypertarget{ux6b27ux62c9ux5b9aux7406}{% -\subsection{欧拉定理}\label{ux6b27ux62c9ux5b9aux7406}} +\hypertarget{ux6269ux5c55ux6b27ux62c9ux5b9aux7406}{% +\subsection{扩展欧拉定理}\label{ux6269ux5c55ux6b27ux62c9ux5b9aux7406}} -\(a\) 在 \(\bmod m\)意义下, \(a(bc)\) 与 \(a ^ (b \bmod(eular(m))+m)\) -同余 +\hypertarget{ux5b9aux4e49}{% +\subsubsection{定义}\label{ux5b9aux4e49}} + +\[ +a^b \equiv \begin{cases} + a^{b \bmod \varphi(m)}, &\gcd(a,m) = 1, \\ + a^b, &\gcd(a,m)\ne 1, b < \varphi(m), \\ + a^{(b \bmod \varphi(m)) + \varphi(m)}, &\gcd(a,m)\ne 1, b \ge \varphi(m). +\end{cases} \pmod m +\] + +\hypertarget{ux89e3ux91ca}{% +\subsubsection{解释}\label{ux89e3ux91ca}} + +读者可能对第二行产生疑问,这一行表达的意思是:如果 \(b < \varphi(m)\) +的话,就不能降幂了。 + +主要是因为题目中 \(m\) 不会太大,而如果 +\(b < \varphi(m)\),自然复杂度是可以接受的。而如果 \(b \ge \varphi(m)\) +的话,复杂度可能就超出预期了,这个时候我们才需要降幂来降低复杂度。 \hypertarget{ux5361ux7279ux5170ux6570}{% \subsection{卡特兰数}\label{ux5361ux7279ux5170ux6570}} \begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} -int C(int n,int m) +int C(int n, int m) { - return fac[n]*qpow(fac[n-m],mod-2)%mod*qpow(fac[m],mod-2)%mod; + return fac[n] * qpow(fac[n - m], mod - 2) % mod * qpow(fac[m], mod - 2) % mod; } int cat(int n) { - return C(2*n,n)*qpow(n+1,mod-2)%mod; + return C(2 * n, n) * qpow(n + 1, mod - 2) % mod; } \end{minted} @@ -1294,58 +1464,157 @@ \subsection{卡特兰数}\label{ux5361ux7279ux5170ux6570}} \subsection{矩阵}\label{ux77e9ux9635}} \begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} - class matrix +class matrix { - public: - int x[105][105]; - int sz; - matrix(int n) - {sz=n; - for(int i=1;i<=sz;i++) +public: + int x[105][105]; + int sz; + matrix(int n) + { + sz = n; + for (int i = 1; i <= sz; i++) + { + for (int j = 1; j <= sz; j++) { - for(int j=1;j<=sz;j++) - { - x[i][j]=0; - } + x[i][j] = 0; } } - matrix mul(matrix a,matrix b); - matrix qpow(matrix a,int n); - void tra(matrix a); + } + matrix mul(matrix a, matrix b); + matrix qpow(matrix a, int n); + void tra(matrix a); }; -matrix matrix::mul(matrix a, matrix b) { +matrix matrix::mul(matrix a, matrix b) +{ matrix c(a.sz); - for(int i=1;i<=a.sz;i++) - for(int j=1;j<=a.sz;j++) - for(int k=1;k<=a.sz;k++) - c.x[i][j]=(c.x[i][j]%mod+(a.x[i][k]*b.x[k][j])%mod)%mod; + for (int i = 1; i <= a.sz; i++) + for (int j = 1; j <= a.sz; j++) + for (int k = 1; k <= a.sz; k++) + c.x[i][j] = (c.x[i][j] % mod + (a.x[i][k] * b.x[k][j]) % mod) % mod; return c; } -matrix matrix::qpow(matrix a,int n) +matrix matrix::qpow(matrix a, int n) { matrix res(a.sz); - for(int i=1;i<=a.sz;i++)res.x[i][i]=1; - while(n>0) + for (int i = 1; i <= a.sz; i++) + res.x[i][i] = 1; + while (n > 0) { - if(n&1)res= mul(res,a); - a= mul(a,a); - n>>=1; + if (n & 1) + res = mul(res, a); + a = mul(a, a); + n >>= 1; } return res; } -void matrix::tra(matrix a) { - for(int i=1;i<=a.sz;i++) +void matrix::tra(matrix a) +{ + for (int i = 1; i <= a.sz; i++) + { + for (int j = 1; j <= a.sz; j++) + { + cout << a.x[i][j] << " "; + } + cout << endl; + } +} +\end{minted} + +\hypertarget{ux9ad8ux65afux6d88ux5143}{% +\subsection{高斯消元}\label{ux9ad8ux65afux6d88ux5143}} + +求解线性方程组 + +将各系数合为矩阵,再将其变为上三角矩阵 + +过程中通常要保证选择的主元绝对值最大以保证精度 + +n\^{}3 + +\begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} +const int N = 100; +const double eps = 1e-10; +int n; +double a[N + 1][N + 1], b[N + 1]; + +void gauss() +{ + int l = 1; + for (int i = 1; i <= n; i++) + { // n列 + for (int j = l; j <= n; j++) + { // 找下面所有行中这一列处绝对值最大的 + if (abs(a[j][i]) > abs(a[l][i])) + { + for (int k = i; k <= n; k++) + { + swap(a[l][k], a[j][k]); + } + swap(b[l], b[j]); + } + } + if (abs(a[l][i]) < eps) + continue; + for (int j = 1; j <= n; j++) + { // 对所有其他行,更新值 + if (j != l && abs(a[j][i]) > eps) + { + double delta = a[j][i] / a[l][i]; + for (int k = i; k <= n; k++) + { + a[j][k] -= a[l][k] * delta; + } + b[j] -= b[l] * delta; + } + } + ++l; + } + + for (int i = l; i <= n; i++) + { // 假如有剩下的行且b值不为0则无解 + if (abs(b[i]) > eps) + { + cout << "无解" << endl; + return; + } + } + if (l <= n) { - for(int j=1;j<=a.sz;j++) + cout << "无穷多解" << endl; + } + else + { + for (int i = 1; i <= n; i++) { - cout< using namespace std; -#define int long long -const int inf = 0x3f3f3f3f; -typedef pair PII; -vector mp[100100]; -int n,m,s; -int dis[100100]; -int vis[100100]; -priority_queue,greater > q; - -void dj(int s) -{ - for(int i=1;i<=n;i++)dis[i]=inf; - dis[s]=0ll; +#define ll long long +const ll maxn = 10+1e5; +const ll inf = 0x3f3f3f3f; +typedef pair pll; + vector mp[100100]; + ll n,m,s; + ll dis[100100]; + ll vis[100100]; + +void dij(ll s){ + for(ll i=1;i<=n;i++){ + dis[i]=inf; + } + dis[s]=0; + priority_queue,greater > q; q.push({dis[s],s}); - - while(!q.empty()) - { - int u=q.top().second; + while(!q.empty()){ + ll u=q.top().second; q.pop(); - if(vis[u])continue; + if(vis[u]) continue; vis[u]=1; - for(auto [w,v]:mp[u]) - { - if(dis[u]+w>n>>m>>s; -for(int i=1;i<=m;i++) -{ - int u,v,w; - cin>>u>>v>>w; - mp[u].push_back({w,v}); -} -dj(s); -for(int i=1;i<=n;i++)cout<>n>>m>>s; + for(ll i=1;i<=m;i++){ + ll u,v,w; + cin>>u>>v>>w; + mp[u].push_back({w,v}); + } + dij(s); + for(ll i=1;i<=n;i++) + cout< -using namespace std; -const int N = 5050; -const int inf=0x3f3f; -int g[N][N],dis[200200]; -bool vis[200200]; -int n,m,ans,u,v,w; -void init(){ - memset(g,inf,sizeof(g)); - memset(dis,inf,sizeof(dis)); -} -void addedge(int u,int v,int w) -{ - if (u != v && g[u][v] > w) g[u][v] = g[v][u] = w; + vector edge[maxn]; + ll dis[maxn]; + ll vis[maxn]; +void update(ll u){ + for(auto [v,w]:edge[u]){ + dis[v]=min(dis[v],w); + } + } -void prim(){ - dis[1]=0; - for(int i=1;i<=n;i++){ - int t=0; - for(int j=1;j<=n;j++){ - if(!vis[j]&&dis[j]g[t][j]){ - dis[j]=g[t][j]; +ll prim(ll n){ + for(ll i=1;i<=n;i++){ + vis[i]=0; + dis[i]=inf; + } + vis[1]=1; + update(1); + ll ans=0; + for(ll i=1;i<=n-1;i++){//连接n-1条边 + ll pos,mi=inf; + for(ll j=1;j<=n;j++){ + if(vis[j]) continue; + if(dis[j]>n>>m; -init(); -for(int i=1;i<=m;i++){ - cin>>u>>v>>w; - addedge(u,v,w); - addedge(u,v,w); -} -prim(); -for(int i=1;i<=n;i++){ - if(!vis[i]) { - cout << "orz"; - return 0; +\hypertarget{prim-omlogm-ux5c0fux6839ux5806ux4f18ux5316}{% +\subsubsection{prim O(mlogm) +(小根堆优化)}\label{prim-omlogm-ux5c0fux6839ux5806ux4f18ux5316}} + +\begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} + vector edge[maxn]; + ll dis[maxn]; + ll vis[maxn]; +ll prim(ll n){ + for(ll i=1;i<=n;i++){ + vis[i]=0; + dis[i]=inf; } -} -cout<,greater> pq; + pq.push({0,1}); + while(!pq.empty()){ + auto [d,u]=pq.top(); + pq.pop(); + if(vis[u]) continue; + vis[u]=1; + ans+=d; + for(auto [v,w]:edge[u]){ + if(!vis[v]&&w -using namespace std; -#define int long long -int fa[200100]; -struct edge{ - int u,v,w; -}e[200100],mst[200100]; -int n,m,k; -int ans; -bool cmp(edge a,edge b) -{ +typedef struct{ + ll u,v,w; +}eg; + eg e[maxm]; + vector edge[maxn]; + ll fa[maxn]; +bool cmp(eg a,eg b){ return a.w>n>>m; -init(n); -for(int i=1;i<=m;i++)cin>>e[i].u>>e[i].v>>e[i].w; -sort(e+1,e+m+1,cmp); -kruskal(); -if(k==n-1)cout<>u>>v>>w; + edge[u].push_back({v,w}); + edge[v].push_back({u,w}); + e[i].u=u;e[i].v=v;e[i].w=w; } \end{minted} @@ -1527,95 +1818,69 @@ \subsubsection{kruskal}\label{kruskal}} \subsection{LCA}\label{lca}} \begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} - #include - +#include using namespace std; -#define IOS ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr); -#define int long long -#define ull unsigned long long +#define IOS ios::sync_with_stdio(false),cin.tie(nullptr), cout.tie(nullptr); +#define ll long long +#define ull unint long long #define lowbit(i) ((i) & (-i)) #define ls(p) (p << 1) #define rs(p) (p << 1 | 1) -#define rep(i, a, b) for (int i = a; i <= b; i++) -#define per(i, a, b) for (int i = a; i >= b; i--) - -typedef pair PII; -const int mod = 1e9 + 7; -const int inf = 0x3f3f3f3f; -const int N = 5e5 + 200; - -int qpow(int a, int n) -{ - int ans = 1; - while (n) - { - if (n & 1) - { - ans = ans * a % mod; - } - a = a * a % mod; - n >>= 1; - } - return ans; -} - -int n, q, root; -vector mp[N]; -int lg2[N]; -int dep[N]; -int f[N][20]; -int vis[N]; -void dfs(int u, int fa = 0) -{ - if (vis[u]) - return; +#define rep(i, a, b) for (ll i = a; i <= b; i++) +#define per(i, a, b) for (ll i = a; i >= b; i--) + +typedef pair pll; +const ll mod = 1e9 + 7; +const ll inf = 0x3f3f3f3f; +const ll maxn = 5e5 + 200; + + ll n, q, root; + vector mp[N]; + ll lg2[maxn]; + ll dep[maxn]; + ll f[maxn][20]; + ll vis[maxn]; +void dfs(ll u, ll fa = 0){ + if (vis[u]) return; vis[u] = 1; dep[u] = dep[fa] + 1; f[u][0] = fa; - for (int i = 1; i <= lg2[dep[u]]; i++) - { + for (ll i = 1; i <= lg2[dep[u]]; i++){ f[u][i] = f[f[u][i - 1]][i - 1]; } - for (auto v : mp[u]) - { + for (auto v : mp[u]){ dfs(v, u); } } -int lca(int a, int b) -{ +ll lca(ll a, ll b){ if (dep[a] > dep[b]) swap(a, b); while (dep[a] != dep[b]) b = f[b][lg2[dep[b] - dep[a]]]; if (a == b) return a; - for (int k = lg2[dep[a]]; k >= 0; k--) - { - if (f[a][k] != f[b][k]) - { + for (ll k = lg2[dep[a]]; k >= 0; k--){ + if (f[a][k] != f[b][k]){ a = f[a][k], b = f[b][k]; } } return f[a][0]; } -signed main() -{ - IOS cin >> n >> q >> root; - for (int i = 1; i < n; i++) - { - int u, v; +int main(){ + IOS + cin >> n >> q >> root; + for (ll i = 1; i < n; i++){ + ll u, v; cin >> u >> v; mp[u].push_back(v); mp[v].push_back(u); } - for (int i = 2; i <= n; i++) - { + for (ll i = 2; i <= n; i++){ lg2[i] = lg2[i / 2] + 1; - } + } dfs(root); - while (q--) - { - int u, v; + while (q--){ + ll u, v; cin >> u >> v; cout << lca(u, v) << endl; } @@ -1623,4 +1888,483 @@ \subsection{LCA}\label{lca}} } \end{minted} +\hypertarget{ux6709ux5411ux56feux5f3aux8054ux901aux5206ux91cf}{% +\subsection{有向图强联通分量}\label{ux6709ux5411ux56feux5f3aux8054ux901aux5206ux91cf}} + +有向非强连通图中的\textbf{极大}强连通子图我们称为\textbf{强连通分量} + +如果一个有向图不是强连通图但是将\textbf{所有有向边换成无向边}变成了强连通图,那么该图就是\textbf{弱连通图}。 + +Kosaraju算法 + +首先对原图 𝐺 进行遍历,记录节点访问\textbf{完}的顺序 𝑑𝑖 , 𝑑𝑖 表示第 𝑖 +个访问完的节点编号。 + +我们选择最晚\textbf{访问完}的节点,对 𝐺 +的反向图进行遍历,它能够遍历到的顶点和它组成了一个 +SCC,把该过程所遍历到的节点打标记,接下来继续找最晚\textbf{访问完}且未被打上标记的节点进行遍历操作。 + +\begin{verbatim} +#include +#define ll long long +using namespace std; +const int Maxn=1e5+7; +int n,m; +vectore[Maxn],e1[Maxn]; +// e 存正向边,e1 存反向边 +bool vis[Maxn]; +int d[Maxn],cnt,col[Maxn],cnt1; +void dfs1(int u){ + vis[u]=1; + for(auto v:e[u]) if(!vis[v]) dfs1(v); + d[++cnt]=u; +} +vectorans[Maxn]; +void dfs2(int u){ + col[u]=cnt1; + ans[cnt1].push_back(u); + for(auto v:e1[u]) if(!col[v]) dfs2(v); +} +int main(){ + scanf("%d%d",&n,&m); + for(int i=1,u,v;i<=m;i++){ + scanf("%d%d",&u,&v); + e[u].push_back(v); + e1[v].push_back(u); + } + for(int i=1;i<=n;i++) if(!vis[i]) dfs1(i); + for(int i=n;i;i--) if(!col[d[i]]) ++cnt1,dfs2(d[i]); + for(int i=1;i<=cnt1;i++) sort(ans[i].begin(),ans[i].end()); + printf("%d\n",cnt1); + for(int i=1;i<=n;i++){ + if(ans[col[i]].size()){ + for(auto j:ans[col[i]]) printf("%d ",j); + puts(""); + ans[col[i]].resize(0); + } + } + return 0; +} +\end{verbatim} + +Tarjan算法 + +在 DFS 过程中,我们会遇到如下 4 种边: + +\begin{itemize} +\item + 树枝边:DFS 过程中经过的边,即 DFS 搜索树上的边。 +\item + 前向边:从祖先节点指向后代节点的非树枝边,我们称为前向边。 +\item + 返祖边(后向边):从后代节点指向祖先节点的非树枝边,我们称为返祖边(后向边)。 +\item + 横叉边:两端无祖先关系的非树枝边,我们称为横叉边。 + + 每个强连通分量都是 DFS 树的一颗子树,搜索时,把当前 DFS + 树种未处理的节点加入一个栈,回溯时可以判断栈顶到栈中的节点是否构成一个强连通分量。 +\end{itemize} + +我们不妨定义 𝑑𝑓𝑛(𝑢) 表示节点 𝑢 在 DFS 中的遍历编号(\textbf{时间戳}), +𝑙𝑜𝑤(𝑢) 表示 𝑢 或 𝑢 +的子树能够最多只通过\textbf{一条非树枝边(不包含树边)}回溯的最早的 𝑑𝑓𝑛 +值,用一个栈记录经过的节点,那么我们可以得出: + +\begin{itemize} +\tightlist +\item + 初始情况有 𝑙𝑜𝑤(𝑢)=𝑑𝑓𝑛(𝑢) 。 +\item + 对于边 (𝑢,𝑣) ,如果 𝑢 为 𝑣 的父亲节点,则有 + 𝑙𝑜𝑤(𝑢)=min\{𝑙𝑜𝑤(𝑢),𝑙𝑜𝑤(𝑣)\} 。 +\item + 对于边 (𝑢,𝑣) 为返祖边或者指向非其他强连通的横叉边,则有 + 𝑙𝑜𝑤(𝑢)=min\{𝑙𝑜𝑤(𝑢),𝑑𝑓𝑛(𝑣)\} 。 +\end{itemize} + +在节点 𝑢 搜索完毕之后,如果 𝑙𝑜𝑤(𝑢)=𝑑𝑓𝑛(𝑢) ,那么说明以 𝑢 +为\textbf{根节点}的搜索子树上及栈中在 𝑢 +内的元素组成了一个强连通分量,然后删除栈内的这些元素,不断重复该操作直到找到所有的强连通分量。 + +例 \href{https://www.luogu.com.cn/problem/P3387}{P3387 【模板】缩点 - +洛谷 \textbar{} 计算机科学教育新生态 (luogu.com.cn)} + +\begin{verbatim} +#include +#define ll long long +using namespace std; +const int Maxn=1e5+7; +int n,m; +vectore[Maxn]; +int col[Maxn],dfn[Maxn],low[Maxn],cnt; +int stk[Maxn],top,_cnt; +vectorans[Maxn]; +void Tarjan(int u){ + dfn[u]=low[u]=++cnt; + stk[++top]=u; + for(auto v:e[u]){ + if(!dfn[v]) Tarjan(v),low[u]=min(low[u],low[v]); + else if(!col[v]) low[u]=min(low[u],dfn[v]); + } + if(low[u]==dfn[u]){ + col[u]=++_cnt; + ans[_cnt].push_back(u); + while(stk[top]!=u) + ans[_cnt].push_back(stk[top]),col[stk[top--]]=_cnt; + --top; + } +} +int main(){ + scanf("%d%d",&n,&m); + for(int i=1,u,v;i<=m;i++){ + scanf("%d%d",&u,&v); + e[u].push_back(v); + } + for(int i=1;i<=n;i++) if(!dfn[i]) Tarjan(i); + for(int i=1;i<=_cnt;i++) sort(ans[i].begin(),ans[i].end()); + printf("%d\n",_cnt); + for(int i=1;i<=n;i++){ + if(ans[col[i]].size()){ + for(auto j:ans[col[i]]) printf("%d ",j); + puts(""); + ans[col[i]].resize(0); + } + } + return 0; +} +\end{verbatim} + +\hypertarget{ux5272ux70b9ux5272ux8fb9}{% +\subsection{割点割边}\label{ux5272ux70b9ux5272ux8fb9}} + +\begin{itemize} +\tightlist +\item + 割点:在\textbf{无向图}中,删去该点后使得连通块数增加的结点称为 + \textbf{割点}。 +\item + 割边(桥):在\textbf{无向图}中,删去该边后使得连通块数增加的边称为 + \textbf{割边(桥)}。 +\end{itemize} + +一个图可能会有多个割点或者割边,但是有割点的图不一定存在割边,有割边的图不一定存在割点。 + +\hypertarget{ux5272ux70b9}{% +\paragraph{割点}\label{ux5272ux70b9}} + +和有向图求 SCC 一样,我们会在搜索过程中遇到两种搜索树上的边: + +\begin{itemize} +\tightlist +\item + 树枝边:DFS 过程中经过的边,即 DFS 搜索树上的边。 +\item + 返祖边(后向边):从后代节点指向祖先节点的非树枝边,我们称为返祖边(后向边)。 +\end{itemize} + +不包含横叉边和前向边,因为这是\textbf{无向图}。 + +我们不妨定义 𝑑𝑓𝑛(𝑢) 表示节点 𝑢 在 DFS 中的遍历编号(\textbf{时间戳}), +𝑙𝑜𝑤(𝑢) 表示 𝑢 或 𝑢 +的子树能够最多只通过\textbf{一条非树枝边(不包含树边)}回溯的最早的 𝑑𝑓𝑛 +值,那么我们可以得出: + +\begin{itemize} +\tightlist +\item + 初始情况有 𝑙𝑜𝑤(𝑢)=𝑑𝑓𝑛(𝑢) 。 +\item + 对于边 (𝑢,𝑣) ,如果 𝑣 没有被搜索到,那么这条边就是树枝边,则有 + 𝑙𝑜𝑤(𝑢)=min\{𝑙𝑜𝑤(𝑢),𝑙𝑜𝑤(𝑣)\} 。 +\item + 对于边 (𝑢,𝑣) ,如果 𝑣 被搜索到了,那么这条边就是返祖边,则有 + 𝑙𝑜𝑤(𝑢)=min\{𝑙𝑜𝑤(𝑢),𝑑𝑓𝑛(𝑣)\} 。 +\end{itemize} + +对于一个节点 𝑢 ,它的子节点 𝑣 ,若 𝑙𝑜𝑤(𝑣)≥𝑑𝑓𝑛(𝑢) ,说明 𝑣 +无法通过它子树的节点到达 𝑑𝑓𝑛 更小的节点,也就是 𝑣 无法不经过点 𝑢 到达比 +𝑢 的 𝑑𝑓𝑛 值更小的节点,显然 𝑢 是一个割点,反之 𝑢 就不是一个割点。 + +但是对于根节点 𝑢 ,它的 𝑑𝑓𝑛 +值一定是整个序列的最小值,因此上述方法不管用,如果它存在两个及以上的子节点,那么把 +𝑢 删除后绝对会把 𝑢 子节点的子树分割开来,此时 𝑢 是割点。 + +\begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} +#include +#define ll long long +using namespace std; +const ll Maxn=1e5+7; +struct edge1{ + ll v,Next; +}Edge[Maxn<<1]; +ll n,m,tot,low[Maxn],dfn[Maxn],cnt,root,ans,head[Maxn]; +bool flg[Maxn]; +inline void add(ll u,ll v){ + Edge[++tot]=(edge1){v,head[u]},head[u]=tot; +} +void tarjan(ll u){ + dfn[u]=low[u]=++cnt; + ll ch=0; + for(ll i=head[u];i;i=Edge[i].Next){ + ll v=Edge[i].v; + if(!dfn[v]){ + ch++; + tarjan(v); + low[u]=min(low[u],low[v]); + if(low[v]>=dfn[u]&&u!=root) flg[u]=1; + + } + else low[u]=min(low[u],dfn[v]); + } + if(ch>=2&&root==u) flg[u]=1; +} +int main(){ + scanf("%lld%lld",&n,&m); + for(ll i=1,u,v;i<=m;i++) + scanf("%lld%lld",&u,&v),add(u,v),add(v,u); + for(ll i=1;i<=n;i++) + if(!dfn[i]) + root=i,tarjan(i); + for(ll i=1;i<=n;i++) + if(flg[i]) + ++ans; + printf("%lld\n",ans); + for(ll i=1;i<=n;i++) + if(flg[i]) + printf("%lld ",i); + return 0; +} +\end{minted} + +\hypertarget{ux5272ux8fb9}{% +\paragraph{割边}\label{ux5272ux8fb9}} + +割边的求法和割点的求法类似,我们继续使用相同定义的 𝑙𝑜𝑤 和 𝑑𝑓𝑛 。 + +很显然,树枝边使得整个图连通,而非树边删除后并不影响图的连通性,因此\textbf{割边一定是树枝边}。 + +假设当前节点为 𝑢 ,它有子节点 𝑣 ,那么边 (𝑢,𝑣) 为割边时当且仅当 +𝑙𝑜𝑤(𝑣)\textgreater 𝑑𝑓𝑛(𝑢) ,只要 𝑣 的节点能通过非树边来到比 𝑢 的 𝑑𝑓𝑛 +更小的节点,那么 (𝑢,𝑣) +就不是割边,不取等号的原因是它是一条边而非一个节点。 + +现在唯一的问题就是判断一条边是否为非树边,我们不能直接将 𝑣→𝑢 ( 𝑢 是 𝑣 +的父亲)识别成非树边,这样可能将树边也识别成非树边。 + +tarjan + +\begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} +#include +#define int long long +const int N=1e6+1; +using namespace std; +struct fy +{ + int v,next; +}edge[N]; +struct fy_ +{ + int from,to; +}E[N]; +int dfn[N],low[N],n,m,x,y,idx,head[N],res,cnt,IDX; +bool g[N]; +inline void add(int x,int y) +{ + edge[++cnt].v=y,edge[cnt].next=head[x],head[x]=cnt; +} +inline void dfs(int u,int fa) +{ + dfn[u]=low[u]=++idx; + for(int i=head[u];i;i=edge[i].next) + { + int v=edge[i].v; + if(!dfn[v]) + { + dfs(v,u); + low[u]=min(low[u],low[v]); + if(low[v]>dfn[u]) + E[++IDX].from=min(u,v),E[IDX].to=max(u,v); + } + else if(v!=fa&&dfn[v] +#define ll long long +using namespace std; +const ll Maxn=4e6+7; +struct edge1{ + ll u,v,Next; +}Edge[Maxn<<1]; +ll n,m,tot=1,f[Maxn]; +ll low[Maxn],dfn[Maxn],cnt,head[Maxn]; +ll cl; +vectorans[Maxn]; +bool flg[Maxn],vis[Maxn]; +inline void add(ll u,ll v){ + Edge[++tot]=(edge1){u,v,head[u]},head[u]=tot; +} +void tarjan(ll u){ + dfn[u]=low[u]=++cnt; + for(ll i=head[u];i;i=Edge[i].Next){ + if(i==(f[u]^1)) continue; + ll v=Edge[i].v; + if(!dfn[v]){ + f[v]=i; + tarjan(v); + low[u]=min(low[u],low[v]); + if(low[v]>dfn[u]) flg[i]=flg[i^1]=1; + } + else low[u]=min(low[u],dfn[v]); + } +} +void DFS(ll u){ + vis[u]=1; + ans[cl].push_back(u); + for(ll i=head[u];i;i=Edge[i].Next){ + if(!flg[i]&&!vis[Edge[i].v]){ + DFS(Edge[i].v); + } + } +} +int main(){ + scanf("%lld%lld",&n,&m); + for(ll i=1,u,v;i<=m;i++) + scanf("%lld%lld",&u,&v),add(u,v),add(v,u); + for(ll i=1;i<=n;i++) if(!dfn[i]) tarjan(i); + for(ll i=1;i<=n;i++) if(!vis[i]) ++cl,DFS(i); + printf("%lld\n",cl); + for(ll i=1;i<=cl;i++,puts("")){ + printf("%lld ",ans[i].size()); + for(auto j:ans[i]) printf("%lld ",j); + } + return 0; +} +\end{minted} + +\hypertarget{ux70b9ux53ccux8fdeux901aux5206ux91cf}{% +\subsubsection{点双连通分量}\label{ux70b9ux53ccux8fdeux901aux5206ux91cf}} + +对于一个点双,它在 DFS 搜索树中 𝑑𝑓𝑛\emph{d\textbf{f}n} +值最小的点一定是割点或者树根。 + +当这个点是割点时,它所属的点双必定不可以向它的父亲方向包括更多点,因为一旦回溯,它就成为了新的子图的一个割点,不是点双。所以它应该归到其中一个或多个子树里的点双中。 + +当这个点是树根时,它的 𝑑𝑓𝑛\emph{d\textbf{f}n} +值是整棵树里最小的。它若有两个以上子树,那么它是一个割点;它若只有一个子树,它一定属于它的直系儿子的点双,因为包括它;它若是一个独立点,视作一个单独的点双。 + +换句话说,一个点双一定在这两类点的子树中。 + +我们用栈维护点,当遇到这两类点时,将子树内目前不属于其它点双的非割点或在子树中的割点归到一个新的点双。注意这个点可能还是与其它点双的公共点,所以不能将其出栈。 + +\begin{minted}[fontsize=\footnotesize,breaklines,linenos]{cpp} +#include +using namespace std; +const int N = 5e5 + 5, M = 4e6 + 5; +int cnt = 1, fir[N], nxt[M], to[M]; +int s[M], top, bcc, low[N], dfn[N], idx, n, m; +vector ans[N]; +inline void tarjan(int u, int fa) { + int son = 0; + low[u] = dfn[u] = ++idx; + s[++top] = u; + for(int i = fir[u]; i; i = nxt[i]) { + int v = to[i]; + if(!dfn[v]) { + son++; + tarjan(v, u); + low[u] = min(low[u], low[v]); + if(low[v] >= dfn[u]) { + bcc++; + while(s[top + 1] != v) ans[bcc].push_back(s[top--]);//将子树出栈 + ans[bcc].push_back(u);//把割点/树根也丢到点双里 + } + } else if(v != fa) low[u] = min(low[u], dfn[v]); + } + if(fa == 0 && son == 0) ans[++bcc].push_back(u);//特判独立点 +} +inline void add(int u, int v) { + to[++cnt] = v; + nxt[cnt] = fir[u]; + fir[u] = cnt; +} +int main() { + scanf("%d%d", &n, &m); + for(int i = 1; i <= m; i++) { + int u, v; + scanf("%d%d", &u, &v); + add(u, v), add(v, u); + } + for(int i = 1; i <= n; i++) { + if(dfn[i]) continue; + top = 0; + tarjan(i, 0); + } + printf("%d\n", bcc); + for(int i = 1; i <= bcc; i++) { + printf("%d ", ans[i].size()); + for(int j : ans[i]) printf("%d ", j); + printf("\n"); + } + return 0; +} +\end{minted} + \end{document}