连通图
这里可能需要说一下SCC是在有向图里的,BCC是在无向图里的,割边割点倒是都可以#
Tajan求强连通分量(Strong Connected Component, SCC)#
给个比较好的视频链接吧:https://www.youtube.com/watch?v=wUgWX0nc4NY&feature=youtu.be
上面的链接需要梯子和较好的英语水平。耐下心来,真的是一份很好的教程。
存个板子:

#include <bits/stdc++.h> using namespace std; const int maxn = 1e5+100; int n,m; int cnt;//记录强联通分量的个数 int id;//遍历的步数 int dfn[maxn];//记录元素第一次被访问的步数 int low[maxn];//包含i的强联通分量最早被访问的步数 int num[maxn];//记录强联通分量里的点的个数 int belong[maxn];//i从属的强联通分量的序号 int top;//栈中元素的个数 int sta[maxn];//手打栈 int instack[maxn];//判断元素是否在栈中 int head[maxn]; struct node{ int to,next; }edge[maxn];//链式前向星存边 inline int read() {//读入优化 int x=0,w=1;char ch=getchar(); while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();} while(ch<='9'&&ch>='0') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*w; } void tarjan(int u) { int v; dfn[u] = low[u] = ++id; sta[++top] = u, instack[u] = 1; for (int i = head[u]; ~i; i = edge[i].next) { v = edge[i].to; if (!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]);//判断u是否为v的子节点 } else if (instack[v]) low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) { cnt++; do//退栈 { num[cnt]++; v = sta[top--]; belong[v] = cnt; instack[v] = 0; } while (u != v); } } int main() { int ans = 0; n = read(), m = read(); memset(head, -1, sizeof(head)); for (int i = 1; i <= m; i++) { int u = read(), v=read(); edge[i] = {v,head[u]}; head[u] = i; } for (int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i); int du[cnt+1]={0}; for (int u = 1; u <= n; u ++) { for (int j = head[u]; ~j; j = edge[j].next) { int v = edge[j].to; if (belong[u] != belong[v]) ++ du[belong[u]]; } } for (int i = 1; i <= cnt; ++ i) { if (!du[i]) { if (ans) {puts("0");return 0;} ans = num[i]; } } printf("%d\n", ans); return 0; }
给几道裸题:
P2863 [USACO06JAN]The Cow Prom S
这道题就是需要统计强连通分量里的点数,大于1则 ++ans

#include <bits/stdc++.h> using namespace std; const int maxn = 1e5+100; int n,m; int cnt;//记录强联通分量的个数 int id;//遍历的步数 int dfn[maxn];//记录元素第一次被访问的步数 int low[maxn];//包含i的强联通分量最早被访问的步数 int num[maxn];//记录强联通分量里的点的个数 int belong[maxn];//i从属的强联通分量的序号 int top;//栈中元素的个数 int sta[maxn];//手打栈 int instack[maxn];//判断元素是否在栈中 int head[maxn]; struct node{ int to,next; }edge[maxn];//链式前向星存边 inline int read() {//读入优化 int x=0,w=1;char ch=getchar(); while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();} while(ch<='9'&&ch>='0') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*w; } void tarjan(int u) { int v; dfn[u] = low[u] = ++id; sta[++top] = u, instack[u] = 1; for (int i = head[u]; ~i; i = edge[i].next) { v = edge[i].to; if (!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]);//判断u是否为v的子节点 } else if (instack[v]) low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) { cnt++; do//退栈 { num[cnt]++; v = sta[top--]; belong[v] = cnt; instack[v] = 0; } while (u != v); } } int main() { int ans = 0; n = read(), m = read(); memset(head, -1, sizeof(head)); for (int i = 1; i <= m; i++) { int u = read(), v=read(); edge[i] = {v,head[u]}; head[u] = i; } for (int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i); for (int i = 1; i <= cnt; i ++) ans += (num[i]>1); printf("%d\n", ans); return 0; }
P2341 [USACO03FALL][HAOI2006]受欢迎的牛 G
这道题需要稍微想一想。肯定是出度为0的强连通分量可以被所有奶牛崇拜。
并且一定只有一个出度为0的,不然就出现了独立点,那么答案就是0.
遍历的时候记得要判断是否指向不同的SCC

#include <bits/stdc++.h> using namespace std; const int maxn = 1e5+100; int n,m; int cnt;//记录强联通分量的个数 int id;//遍历的步数 int dfn[maxn];//记录元素第一次被访问的步数 int low[maxn];//包含i的强联通分量最早被访问的步数 int num[maxn];//记录强联通分量里的点的个数 int belong[maxn];//i从属的强联通分量的序号 int top;//栈中元素的个数 int sta[maxn];//手打栈 int instack[maxn];//判断元素是否在栈中 int head[maxn]; struct node{ int to,next; }edge[maxn];//链式前向星存边 inline int read() {//读入优化 int x=0,w=1;char ch=getchar(); while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();} while(ch<='9'&&ch>='0') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*w; } void tarjan(int u) { int v; dfn[u] = low[u] = ++id; sta[++top] = u, instack[u] = 1; for (int i = head[u]; ~i; i = edge[i].next) { v = edge[i].to; if (!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]);//判断u是否为v的子节点 } else if (instack[v]) low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) { cnt++; do//退栈 { num[cnt]++; v = sta[top--]; belong[v] = cnt; instack[v] = 0; } while (u != v); } } int main() { int ans = 0; n = read(), m = read(); memset(head, -1, sizeof(head)); for (int i = 1; i <= m; i++) { int u = read(), v=read(); edge[i] = {v,head[u]}; head[u] = i; } for (int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i); int du[cnt+1]={0}; for (int u = 1; u <= n; u ++) { for (int j = head[u]; ~j; j = edge[j].next) { int v = edge[j].to; if (belong[u] != belong[v]) ++ du[belong[u]]; } } for (int i = 1; i <= cnt; ++ i) { if (!du[i]) { if (ans) {puts("0");return 0;} ans = num[i]; } } printf("%d\n", ans); return 0; }
Tajan求割点#
相较于求强连通分量,求割点只是在其基础上加了根节点统计子节点个数
给道模板题吧:P3388 【模板】割点(割顶)

#include <cstring> #include <cstdio> #include <iostream> using namespace std; const int maxn = 2e5+100; int n,m; int cnt;//记录强联通分量的个数 int id;//遍历的步数 int dfn[maxn];//记录元素第一次被访问的步数 int low[maxn];//包含i的强联通分量最早被访问的步数 int num[maxn];//记录强联通分量里的点的个数 int belong[maxn];//i从属的强联通分量的序号 int top;//栈中元素的个数 int sta[maxn];//手打栈 int instack[maxn];//判断元素是否在栈中 int head[maxn]; int iscut[maxn]; int tot; int in[maxn], out[maxn]; struct node{ int to,next; }edge[maxn];//链式前向星存边 inline int read() {//读入优化 int x=0,w=1;char ch=getchar(); while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();} while(ch<='9'&&ch>='0') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*w; } void tarjan(int u, int fa) { int v, child = 0; dfn[u] = low[u] = ++id; sta[++top] = u, instack[u] = 1; for (int i = head[u]; ~i; i = edge[i].next) { v = edge[i].to; if (!dfn[v]) { tarjan(v, fa); low[u] = min(low[u], low[v]); if (low[v]>=dfn[u] && u!=fa) iscut[u] = 1; if (u == fa) ++ child; } else if (instack[v]) low[u] = min(low[u], dfn[v]); } if (child >= 2 && fa == u) iscut[fa] = 1; if (dfn[u] == low[u]) { cnt++; do//退栈 { num[cnt]++; v = sta[top--]; belong[v] = cnt; instack[v] = 0; } while (u != v); } } inline void add(int u, int v) { edge[++tot] = {v, head[u]}; head[u] = tot; } int main() { int ans = 0; memset(head, -1, sizeof(head)); scanf("%d%d",&n,&m); for (int i=1;i<=m;i++){ int x,y; scanf("%d%d",&x,&y); add(x,y); add(y,x); } for (int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i, i); for (int i=1;i<=n;i++) if (iscut[i]) ans++; printf("%d\n",ans); for (int i=1;i<=n;i++) if (iscut[i]) printf("%d ",i); return 0; }
还有道:B - Network UVA - 315

#include <cstring> #include <cstdio> #include <iostream> using namespace std; const int maxn = 1e3+100; int n,m; int cnt;//记录强联通分量的个数 int id;//遍历的步数 int dfn[maxn];//记录元素第一次被访问的步数 int low[maxn];//包含i的强联通分量最早被访问的步数 int num[maxn];//记录强联通分量里的点的个数 int belong[maxn];//i从属的强联通分量的序号 int top;//栈中元素的个数 int sta[maxn];//手打栈 int instack[maxn];//判断元素是否在栈中 int head[maxn]; int iscut[maxn]; int tot; struct node{ int to,next; }edge[maxn];//链式前向星存边 inline int read() {//读入优化 int x=0,w=1;char ch=getchar(); while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();} while(ch<='9'&&ch>='0') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*w; } void tarjan(int u, int fa) { int v, child = 0; dfn[u] = low[u] = ++id; sta[++top] = u, instack[u] = 1; for (int i = head[u]; ~i; i = edge[i].next) { v = edge[i].to; if (!dfn[v]) { tarjan(v, fa); low[u] = min(low[u], low[v]); if (low[v]>=dfn[u] && u!=fa) iscut[u] = 1; if (u == fa) ++ child; } else if (instack[v]) low[u] = min(low[u], dfn[v]); } if (child >= 2 && fa == u) iscut[fa] = 1; if (dfn[u] == low[u]) { cnt++; do//退栈 { num[cnt]++; v = sta[top--]; belong[v] = cnt; instack[v] = 0; } while (u != v); } } inline void add(int u, int v) { edge[++tot] = {v, head[u]}; head[u] = tot; } int main() { while (~scanf("%d", &n) && n) { int ans = 0; tot = 0; memset(head, -1, sizeof(head)); memset(low, 0, sizeof(low)); memset(dfn, 0, sizeof(dfn)); memset(sta, 0, sizeof(sta)); memset(dfn, 0, sizeof(dfn)); memset(num, 0, sizeof(num)); memset(iscut, 0, sizeof(iscut)); int st, ed; while(~scanf("%d",&st)&&st) { while(getchar()!='\n') { scanf("%d",&ed); add(st, ed); add(ed, st); } } for (int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i, i); for (int i=1;i<=n;i++) if (iscut[i]) ans++; printf("%d\n",ans); } return 0; }
Tajan求割边#
UPD 2020.8.17: 突然发现我的板子里并没有判断是否存在重边的情况。
但是众所周知,如果桥是有重边的话,那么他是不算桥的。
这里说要在建图前需要给重边打上标记,让他们不能成为桥。

#include <cstring> #include <cstdio> #include <iostream> #include <vector> #include <algorithm> using namespace std; const int maxn = 1e5+100; int id;//遍历的步数 int dfn[maxn];//记录元素第一次被访问的步数 int low[maxn];//包含i的强联通分量最早被访问的步数 int head[maxn]; int bridge; int tot; struct node{ int to,next,flag; }edge[maxn];//链式前向星存边 inline int read() {//读入优化 int x=0,w=1;char ch=getchar(); while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();} while(ch<='9'&&ch>='0') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*w; } void tarjan(int u, int pre) { int v; dfn[u] = low[u] = ++id; for (int i = head[u]; ~i; i = edge[i].next) { v = edge[i].to; if (v == pre) continue; if (!dfn[v]) { tarjan(v, u); low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) edge[i].flag = edge[i^1].flag = 1, bridge++; } low[u] = min(low[u], dfn[v]); } } inline void add(int u, int v) { edge[tot] = {v, head[u], 0}; head[u] = tot++; } int main() { int n; while (~scanf("%d", &n)) { id = tot = bridge = 0; memset(head, -1, sizeof(head)); memset(low, 0, sizeof(low)); memset(dfn, 0, sizeof(dfn)); int st, ed; for(int i = 1,u,num,v; i <= n; i++) { scanf("%d (%d)",&u,&num); while (num--) { scanf("%d",&v); v++; if(v <= u)continue; add(u+1,v), add(v,u+1); } } for (int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i, i); printf("%d critical links\n", bridge); vector<pair<int,int> >ans; for(int u = 1; u <= n; u++) for(int i = head[u]; ~i; i = edge[i].next) if (edge[i].flag && edge[i].to > u) ans.push_back(make_pair(u, edge[i].to)); sort(ans.begin(),ans.end()); //按顺序输出桥 for(int i = 0; i < ans.size(); i++) printf("%d - %d\n",ans[i].first-1,ans[i].second-1); printf("\n"); } return 0; }
Tajan SCC 好题:#
UPD 2020.8.17: 这道题好像数据比较水,并没有重边出现的情况。然而正常情况下是需要考虑的。
这题真的很好呀。
题意是给你一张图,之后给出q条边,依次加到原图上,之后输出每次加上这条边之后的剩余的割边数。
首先我们先把原先的图用Tajan缩点。那么缩完点之后,剩余的图就是一棵树。树的割边数就是树的结点数-1
那么每次加上新的边,就只需要减去这条边两端点到它们LCA路径上的割边数就好了。(因为加上这一条边之后树上会成环,那么路径上的割边就会消失)

#include <cstring> #include <cstdio> #include <iostream> #include <vector> #include <algorithm> #include <queue> using namespace std; const int maxn = 1e5+100; const int maxm = 4e5+100; int id;//遍历的步数 int dfn[maxn];//记录元素第一次被访问的步数 int low[maxn];//包含i的强联通分量最早被访问的步数 int head[maxn]; int fa[maxn]; int depth[maxn]; int iscut[maxn]; int belong[maxn]; int instack[maxn]; int sta[maxn]; int num[maxn]; int bridge; int cnt; int tot; int top; int ans; int n, m; struct node{ int to,next,flag; }edge[maxm];//链式前向星存边 inline int read() {//读入优化 int x=0,w=1;char ch=getchar(); while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();} while(ch<='9'&&ch>='0') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*w; } inline void add(int u, int v) { edge[tot] = {v, head[u], 0}; head[u] = tot++; } vector<int> G[maxn]; void BFS(int x) { memset(depth, 0, sizeof(depth)); memset(iscut, 0, sizeof(iscut)); queue<int> q; q.push(x); depth[x] = 1, fa[x] = -1; while (!q.empty()) { int u = q.front(); q.pop(); for (int i = 0; i < G[u].size(); ++ i) { int v = G[u][i]; if (depth[v]) continue; iscut[v] = 1; depth[v] = depth[u] + 1; fa[v] = u; q.push(v); } } } void LCA(int u, int v) { //朴素LCA if (depth[u] < depth[v]) swap(u, v); while (depth[u] > depth[v]) { if (iscut[u]) --ans, iscut[u] = 0; u = fa[u]; } while (u != v) { if (iscut[u]) --ans, iscut[u] = 0; if (iscut[v]) --ans, iscut[v] = 0; u = fa[u], v = fa[v]; } } void tarjan(int u, int pre) { int v; dfn[u] = low[u] = ++id; sta[++top] = u, instack[u] = 1; for (int i = head[u]; ~i; i = edge[i].next) { v = edge[i].to; if (v == pre) continue; if (!dfn[v]) { tarjan(v, u); low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) edge[i].flag = edge[i^1].flag = 1, bridge++; } else if (instack[v]) low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) { cnt++; do { num[cnt]++, v = sta[top--]; belong[v] = cnt, instack[v] = 0; } while (u != v); } } void solve() { id = bridge = top = cnt = 0; memset(low, 0, sizeof(low)); memset(dfn, 0, sizeof(dfn)); memset(num, 0, sizeof(num)); memset(instack, 0, sizeof(instack)); tarjan(1, 1); for (int i = 0; i <= cnt; ++ i) G[i].clear(); for (int u = 1; u <= n; ++ u) { for (int i = head[u]; ~i; i = edge[i].next) { if (edge[i].flag) { int v = edge[i].to; G[belong[u]].push_back(belong[v]); G[belong[v]].push_back(belong[u]); } } } BFS(1); ans = cnt - 1; int q; scanf("%d", &q); while (q--) { int u, v; scanf("%d%d", &u, &v); LCA(belong[u], belong[v]); printf("%d\n", ans); } printf("\n"); } int main() { int icase = 0; while (~scanf("%d %d", &n, &m)) { if (!n && !m) break; memset(head, -1, sizeof(head)); tot = 0; for (int i = 1,u,v; i <= m; ++ i) { scanf("%d%d",&u,&v); add(u, v), add(v, u); } printf("Case %d:\n", ++icase); solve(); } return 0; }
这道题就比较好了。需要判断重边的情况。
给边再开个flag来标记是否被走过,就可以达到去重的目的了。
桥的记录还是一样的地方。

#include <cstring> #include <cstdio> #include <iostream> #include <vector> #include <algorithm> #include <queue> using namespace std; const int maxn = 2e5+100; const int maxm = 2e6+100; int id;//遍历的步数 int dfn[maxn];//记录元素第一次被访问的步数 int low[maxn];//包含i的强联通分量最早被访问的步数 int head[maxn]; int fa[maxn]; int depth[maxn]; int iscut[maxn]; int belong[maxn]; int instack[maxn]; int sta[maxn]; int num[maxn]; int vis[maxn]; int d[maxn]; int bridge; int cnt; int tot; int top; int ans; int n, m; int ans_max; struct node{ int to,next; bool flag, iscut; }edge[maxm];//链式前向星存边 inline int read() {//读入优化 int x=0,w=1;char ch=getchar(); while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();} while(ch<='9'&&ch>='0') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*w; } inline void add(int u, int v) { edge[tot] = {v, head[u], 0, 0}; head[u] = tot++; } vector<int> G[maxn]; void tarjan(int u) { int v; dfn[u] = low[u] = ++id; sta[++top] = u, instack[u] = 1; for (int i = head[u]; ~i; i = edge[i].next) { v = edge[i].to; if (edge[i].flag) continue; edge[i].flag = edge[i^1].flag = 1; if (!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) edge[i].iscut = edge[i^1].iscut = 1, bridge++; } else if (instack[v]) low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) { cnt++; do { num[cnt]++, v = sta[top--]; belong[v] = cnt, instack[v] = 0; } while (u != v); } } void dp(int st) { vis[st] = 1; //当前结点已访问 for(int i = 0; i < G[st].size(); ++ i){ int Eiv = G[st][i]; if(vis[Eiv]) continue; //不走回头路 dp(Eiv); ans_max = max(ans_max, d[st] + d[Eiv] + 1); //更新树的直径(由当前结点两段之和更新) d[st] = max(d[st], d[Eiv]+1); //更新当前结点所能走的最长路径(保留较长的那边) } } void solve() { id = bridge = top = cnt = ans_max = 0; memset(low, 0, sizeof(low)); memset(dfn, 0, sizeof(dfn)); memset(num, 0, sizeof(num)); memset(vis, 0, sizeof(vis)); memset(d, 0, sizeof(d)); memset(instack, 0, sizeof(instack)); tarjan(1); for (int i = 0; i <= cnt; ++ i) G[i].clear(); for (int u = 1; u <= n; ++ u) { for (int i = head[u]; ~i; i = edge[i].next) { if (edge[i].iscut) { int v = edge[i].to; G[belong[u]].push_back(belong[v]); G[belong[v]].push_back(belong[u]); } } } dp(belong[1]); cout << cnt - 1 - ans_max << endl; } int main() { int icase = 0; while (~scanf("%d %d", &n, &m)) { if (!n && !m) break; memset(head, -1, sizeof(head)); tot = 0; for (int i = 1,u,v; i <= m; ++ i) { scanf("%d%d",&u,&v); add(u, v), add(v, u); } solve(); } return 0; } /* 5 4 1 2 1 3 1 4 1 5 4 4 1 2 1 3 1 4 2 3 */
A - Network of Schools POJ - 1236
这道题主要考察的是如何构造scc
先缩点,重构图。
重构之后的图若有多个点,则考虑这些点中入度为0的点:入度为0的点不能被其他点到达,
而一个入度不为0的点可以从某个入度为0的点到达,那么只需要向这些入度为0的点分发软件,就可以使得所有的点均能获得软件。
重构之后的图中有出度为0的点,在图中,入度为0的点(设为m个)无法从其他点到达,那么为了使得所有的点连通,
需要m条路径连接到这m个入度为0的点;而出度为0的点(设为n个)无法到达其他点,那么为了使得所有的点连通,需要n条路径从这n个出度为0的点连出。
于是,至少需要添加
max(m, n)条边,使得图中所有的点的入度和出度不为0.
同时,在一个有向无环图中,如果该图的所有点均可连接到一块,且每个点的出度和入度均不为0,则该图肯定强连通。于是,结果为 max(m,n)
构造策略举例: n = 2 , n :{1, 2} m = 4, m : {3, 4, 5, 6}
1 - 3, 2 - 4, 之后1跟2任意取将剩下的5, 6 相连即可, 这样是4条边,也就是max(m,n);
不懂的可以自己画画图

G - Strongly connected HDU - 4635
这道题挺好的。
题意:给定一个有向图,求最大可以增加多少条边使得这个仍然不是强连通。
题解:首先我们需要知道的是,在一个有n个节点的有向图当中,最多只会有n * (n - 1) 条边
那么我们就可以反着来做了,就是假设当前图是最大的完全SCC了,之后删去最少数量的边使它成为DAG。
因为图中已经有m条边了,所以要先减去这些边
之后就是先缩点,并且统计每个SCC里点的个数。之后我们枚举每个出度或者入度为0 的SCC,求出 min (sccnum[i] * ( n - sccnum[i]) )
Note: 这里是让每个在scc[i]中的点都连向其他的点, 这样就必然是一个scc(可以画图看看)
那么我们最后只需要 n * (n - 1) - m - min (sccnum[i] * ( n - sccnum[i]) ) 就好啦。

#include <cstring> #include <cstdio> #include <iostream> #include <vector> #include <algorithm> #include <queue> using namespace std; const int maxn = 2e5+100; const int maxm = 2e6+100; typedef long long ll; const ll inf = 1e17; int id;//遍历的步数 int dfn[maxn];//记录元素第一次被访问的步数 int low[maxn];//包含i的强联通分量最早被访问的步数 int head[maxn]; int fa[maxn]; int depth[maxn]; int iscut[maxn]; int belong[maxn]; int instack[maxn]; int sta[maxn]; int num[maxn]; int vis[maxn]; int d[maxn]; int bridge; int cnt; int tot; int top; int ans; int n, m; int ans_max; struct node{ int to,next; bool flag, iscut; }edge[maxm];//链式前向星存边 inline int read() {//读入优化 int x=0,w=1;char ch=getchar(); while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();} while(ch<='9'&&ch>='0') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*w; } inline void add(int u, int v) { edge[tot] = {v, head[u], 0, 0}; head[u] = tot++; } vector<int> G[maxn]; void tarjan(int u) { int v; dfn[u] = low[u] = ++id; sta[++top] = u, instack[u] = 1; for (int i = head[u]; ~i; i = edge[i].next) { v = edge[i].to; // if (edge[i].flag) continue; // edge[i].flag = edge[i^1].flag = 1; if (!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]); // if (low[v] > dfn[u]) // edge[i].iscut = edge[i^1].iscut = 1, bridge++; } else if (instack[v]) low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) { cnt++; do { num[cnt]++, v = sta[top--]; belong[v] = cnt, instack[v] = 0; } while (u != v); } } void solve() { id = bridge = top = cnt = ans_max = 0; memset(low, 0, sizeof(low)); memset(dfn, 0, sizeof(dfn)); memset(num, 0, sizeof(num)); memset(vis, 0, sizeof(vis)); memset(d, 0, sizeof(d)); memset(instack, 0, sizeof(instack)); for (int i = 1; i <= n; ++ i) if (!dfn[i]) tarjan(i); int in[cnt+1]={0}, out[cnt+1]={0}; if (cnt == 1) printf("-1\n"); else { for (int i = 1; i < n; ++ i) { for (int j = head[i]; ~j; j = edge[j].next) { int v = edge[j].to; if (belong[v] != belong[i]) ++in[belong[v]], ++out[belong[i]]; } } ll base = 1ll*n*(n-1)-m; ll ans = inf; for (int i = 1; i <= cnt; ++ i) { if (!in[i] || !out[i]) ans = min(1ll*num[i]*(n-num[i]), ans); } printf("%lld\n", base-ans); } } int main() { int icase = 0; int T; scanf("%d", &T); while (T--) { scanf("%d%d", &n,&m); memset(head, -1, sizeof(head)); tot = 0; for (int i = 1,u,v; i <= m; ++ i) scanf("%d%d",&u,&v), add(u, v); printf("Case %d: ", ++icase); solve(); } return 0; }
首先赞一下这道题,的确是个好题。
题目大意:一个国王有n个王子,同时有n个女孩。每个王子都有自己喜欢的若干个女孩,
现给定一个合法的完备匹配(也就是一个王子娶其中一个自己喜欢女孩),求每个王子可以选择哪些女孩可以让剩下的每个王子依旧能够选择到自己喜欢的一个女孩。
题目有点绕。。大概就是求完美匹配的增广路。
如果给定的完备匹配中xi->yi,现在我们想让xi指向yj。
如果让xi->yj的话,那么yi显然对于xi已经没用了,又因为最终的匹配必然是一个完备匹配,所以假如寻找到了增广链,那么增广链的最后一条边所指向的节点必然是yi。
假如我们由yi向xi再连一条边会发生什么呢? 很明显,形成了一个环!也就是说,形成了一个强连通分量。所以说,如果xi可以选择yj,那么xi和yj必然属于同一个强连通分量。
所以此题可用强连通分量来求解。
具体解法:对每个王子向他喜欢的女孩连一条边,对于给定的完备匹配,由女孩向对应的男孩连一条匹配边。
然后求这个二分图的强连通分量,如果某个xi和他喜欢的某个yj属于同一个强连通分量,那么yj对于xi是可选的。
Note: 读入输出挂yyds! 没用前9000ms+ ,用之后450ms+

#include <cstring> #include <cstdio> #include <iostream> #include <vector> #include <algorithm> #include <queue> using namespace std; const int maxn = 4e3+100; const int maxm = 2e6+100; typedef long long ll; const ll inf = 1e17; int id;//遍历的步数 int dfn[maxn];//记录元素第一次被访问的步数 int low[maxn];//包含i的强联通分量最早被访问的步数 int head[maxn]; int fa[maxn]; int depth[maxn]; int iscut[maxn]; int belong[maxn]; int instack[maxn]; int sta[maxn]; int num[maxn]; int vis[maxn]; int d[maxn]; int bridge; int cnt; int tot; int top; int n, m; int ans_max; struct node{ int to,next; bool flag, iscut; }edge[maxm];//链式前向星存边 vector<int> ans; //仅适合纯数字输入 int Scan() //输入外挂 { int res=0,ch,flag=0; if((ch=getchar())=='-') flag=1; else if(ch>='0'&&ch<='9') res=ch-'0'; while((ch=getchar())>='0'&&ch<='9') res=res*10+ch-'0'; return flag?-res:res; } void Out(int a) //输出外挂 { if(a>9) Out(a/10); putchar(a%10+'0'); } inline int read() {//读入优化 int x=0,w=1;char ch=getchar(); while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();} while(ch<='9'&&ch>='0') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*w; } inline void add(int u, int v) { edge[tot] = {v, head[u], 0, 0}; head[u] = tot++; } void tarjan(int u, int fa) { int v; dfn[u] = low[u] = ++id; sta[++top] = u, instack[u] = 1; for (int i = head[u]; ~i; i = edge[i].next) { v = edge[i].to; //if (fa == v) continue; // if (edge[i].flag) continue; //edge[i].flag = edge[i^1].flag = 1; if (!dfn[v]) { tarjan(v, u); low[u] = min(low[u], low[v]); //if (low[v] > dfn[u]) //edge[i].iscut = edge[i^1].iscut = 1, bridge++; } else if (instack[v]) low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) { cnt++; do { num[cnt]++, v = sta[top--]; belong[v] = cnt, instack[v] = 0; } while (u != v); } } void solve() { id = bridge = top = cnt = ans_max = 0; memset(low, 0, sizeof(low)); memset(dfn, 0, sizeof(dfn)); memset(num, 0, sizeof(num)); memset(vis, 0, sizeof(vis)); memset(instack, 0, sizeof(instack)); for (int i = 1; i <= 2*n; ++ i) if (!dfn[i]) tarjan(i, -1); for (int u = 1; u <= n; ++ u) { ans.clear(); for (int j = head[u]; ~j; j = edge[j].next) { int v = edge[j].to; if (belong[u] == belong[v]) ans.push_back(v-n); } Out(ans.size()); sort(ans.begin(), ans.end()); for (int j = 0; j < ans.size(); j++) putchar(' '), Out(ans[j]); putchar('\n'); } } int main() { while (~scanf("%d", &n)) { memset(head, -1, sizeof(head)); tot = 0; for (int u = 1,v,num; u <= n; ++ u) { num = Scan(); while (num--) v = Scan(), add(u, v+n); } for (int u = 1, v; u <= n; ++ u) v = Scan(), add(v+n, u); solve(); } return 0; }
H - Prince and Princess HDU - 4685
这题是上一题的增强版,仍然是求完美匹配的增广路。
分析:这题是poj 1904的加强版,poj 1904的王子和公主数是相等的,
这里可以不等,且poj 1904给出了一个初始完美匹配,但是这题就要自己求。
对于上一道题我们将完美匹配的建边,再将偏好建边。这样就会出现好多个SCC,这个SCC的代表的就是增广路。(SCC的目的就是将王子能匹配的公主都放在一个scc里)
这道题也是一样的做法。只不过我们需要造出完美匹配。具体做法就是先最大匹配之后加入虚拟点。之后连边缩点,输出原图中的点即可。

#include <cstring> #include <cstdio> #include <iostream> #include <vector> #include <algorithm> #include <queue> using namespace std; const int maxn = 2e3+100; const int maxm = 2e7+100; typedef long long ll; const ll inf = 1e17; int id;//遍历的步数 int dfn[maxn];//记录元素第一次被访问的步数 int low[maxn];//包含i的强联通分量最早被访问的步数 int head[maxn]; int belong[maxn]; int instack[maxn]; int sta[maxn]; int num[maxn]; int mx[maxn], my[maxn], vis[maxn]; int cnt; int tot; int top; int n, m; int nn, mm; int mp[maxn][maxn]; struct node{ int to,next; bool flag, iscut; }edge[maxm];//链式前向星存边 vector<int> ans; //仅适合纯数字输入 int Scan() //输入外挂 { int res=0,ch,flag=0; if((ch=getchar())=='-') flag=1; else if(ch>='0'&&ch<='9') res=ch-'0'; while((ch=getchar())>='0'&&ch<='9') res=res*10+ch-'0'; return flag?-res:res; } void Out(int a) //输出外挂 { if(a>9) Out(a/10); putchar(a%10+'0'); } inline int read() {//读入优化 int x=0,w=1;char ch=getchar(); while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();} while(ch<='9'&&ch>='0') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*w; } inline void add(int u, int v) { edge[tot] = {v, head[u], 0, 0}; head[u] = tot++; } void tarjan(int u, int fa) { int v; dfn[u] = low[u] = ++id; sta[++top] = u, instack[u] = 1; for (int i = head[u]; ~i; i = edge[i].next) { v = edge[i].to; if (!dfn[v]) { tarjan(v, u); low[u] = min(low[u], low[v]); } else if (instack[v]) low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) { cnt++; do { num[cnt]++, v = sta[top--]; belong[v] = cnt, instack[v] = 0; } while (u != v); } } bool DFS(int u) { for (int v = 1; v <= mm; v++) { if (!mp[u][v]) continue; if (!vis[v]) { vis[v] = 1; if (!my[v] || DFS(my[v])) { my[v] = u, mx[u] = v; return 1; } } } return 0; } int hungary() { memset(mx,0,sizeof(mx)), memset(my,0,sizeof(my)); int res = 0; for (int i = 1; i <= nn; i++) { if (!mx[i]) { memset(vis, 0, sizeof(vis)); if(DFS(i)) ++ res; } } return res; } void solve() { id = top = cnt = 0; memset(low, 0, sizeof(low)); memset(dfn, 0, sizeof(dfn)); memset(num, 0, sizeof(num)); memset(belong, 0, sizeof(belong)); memset(instack, 0, sizeof(instack)); nn=n,mm=m; int temp=hungary(); nn=mm=n+m-temp; for(int u=n+1;u<=nn;u++) for(int v=1;v<=m;v++) mp[u][v]=true; for(int u=1;u<=n;u++) for(int v=m+1;v<=mm;v++) mp[u][v]=true; hungary(); for(int u=1;u<=nn;u++) for(int v=1;v<=mm;v++) if(mp[u][v]) add(u,v+nn); for(int v=1;v<=mm;v++) add(v+nn,my[v]); for (int i = 1; i <= n; ++ i) if (!dfn[i]) tarjan(i, -1); for (int u = 1; u <= n; ++ u) { ans.clear(); for(int v=1;v<=m;v++) if(mp[u][v]&&belong[u]==belong[v+nn]) ans.push_back(v); Out(ans.size()); sort(ans.begin(), ans.end()); for (int j = 0; j < ans.size(); j++) putchar(' '), Out(ans[j]); putchar('\n'); } } int main() { int T; scanf("%d", &T); int kase = 0; while (T--) { scanf("%d%d", &n, &m); memset(head, -1, sizeof(head)); memset(mp, 0, sizeof(mp)); tot = 0; for (int u = 1,v,num; u <= n; ++ u) { num = Scan(); while (num--) v = Scan(), mp[u][v] = 1; } printf("Case #%d:\n", ++kase); solve(); } return 0; }
定义:
对于一个连通图,如果任意两点至少存在两条点不重复路径,则称这个图为点双连通的(简称双连通);
如果任意两点至少存在两条边不重复路径,则称该图为边双连通的。
点双连通图的定义等价于任意两条边都同在一个简单环中,而边双连通图的定义等价于任意一条边至少在一个简单环中。
对一个无向图,点双连通的极大子图称为点双连通分量(简称双连通分量),边双连通的极大子图称为边双连通分量。
Tajan求边双连通分量#
如点双联通分量的定义:边双联通分量就是不存在桥的最大边双联通子图。
其实求边双连通分量和求强连通分量差不多,每次访问点的时候将其入栈,当low[u]==dfn[u]时就说明找到了一个连通的块,
则栈内的所有点都属于同一个边双连通分量,因为无向图要见反向边,所以在求边双连通分量的时候,遇到反向边跳过就行了。
这里就比点双联通分量要简单的多了——直接去掉所有的桥就行了(就是统计连通块的数量就好了。
Note:不要忘记判断重边!!不过这道题也没有重边的样子。
E - Redundant Paths POJ - 3177
这题主要是考构造边双
一个有桥的连通图,如何把它通过加边变成边双连通图?方法为首先求出所有的桥,然后删除这些桥边,剩下的每个连通块都是一个双连通子图。
把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为1。统计出树中度为1的节点的个数,即为叶节点的个数,记为leaf。
则至少在树上添加(leaf+1)/2条边,就能使树达到边二连通,所以至少添加的边数就是(leaf+1)/2。
具体方法为,首先把两个最近公共祖先最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为一个形成的环一定是双连通的。
然后再找两个最近公共祖先最远的两个叶节点,这样一对一对找完,恰好是(leaf+1)/2次,把所有点收缩到了一起。

#include <cstring> #include <cstdio> #include <iostream> using namespace std; const int maxn = 1e5+100; int n,m; int sccnum;//记录强联通分量的个数 int id;//遍历的步数 int dfn[maxn];//记录元素第一次被访问的步数 int low[maxn];//包含i的强联通分量最早被访问的步数 int num[maxn];//记录强联通分量里的点的个数 int belong[maxn];//i从属的强联通分量的序号 int top;//栈中元素的个数 int sta[maxn];//手打栈 int instack[maxn];//判断元素是否在栈中 int head[maxn]; int tot; int in[maxn], out[maxn]; struct node{ int to,next; }edge[maxn];//链式前向星存边 inline int read() {//读入优化 int x=0,w=1;char ch=getchar(); while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();} while(ch<='9'&&ch>='0') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*w; } void tarjan(int u, int fa) { int v; dfn[u] = low[u] = ++id; sta[++top] = u, instack[u] = 1; for (int i = head[u]; ~i; i = edge[i].next) { v = edge[i].to; if (i == (fa^1)) continue; //反向边就跳过 if (!dfn[v]) { tarjan(v, i); low[u] = min(low[u], low[v]);//判断u是否为v的子节点 } else if (instack[v]) low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) { sccnum++; do//退栈 { num[sccnum]++; v = sta[top--]; belong[v] = sccnum; instack[v] = 0; } while (u != v); } } inline void add(int u, int v) { edge[tot] = {v, head[u]}; head[u] = tot++; } int main() { int ans = 0; scanf("%d%d", &n,&m); memset(head, -1, sizeof(head)); for (int i = 1,u,v; i <= m; i++) scanf("%d%d", &u,&v), add(u, v), add(v, u); for (int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i, -1); for (int u = 1; u <= n; u ++) { for (int j = head[u]; ~j; j = edge[j].next) { int v = edge[j].to; if (belong[u] != belong[v]) ++ out[belong[u]], ++ in[belong[v]]; } } int cnt1 = 0; for (int i = 1; i <= sccnum; ++ i) { if (out[i] == 1) ++cnt1; } printf("%d\n", (cnt1+1)/2); return 0; }
I - Caocao's Bridges HDU - 4738
这道题就是求出桥最小权值,但是有几个坑点需要注意:
1、有重边
2、最小权值为0的时候要输出1,因为至少要有一个人
3、如果整个图是SCC就输出-1
4、如果有孤立点那么就输出0

#include <cstring> #include <cstdio> #include <iostream> #include <vector> #include <algorithm> #include <queue> using namespace std; const int maxn = 2e3+100; const int maxm = 4e6+100; int id;//遍历的步数 int dfn[maxn];//记录元素第一次被访问的步数 int low[maxn];//包含i的强联通分量最早被访问的步数 int head[maxn]; int fa[maxn]; int depth[maxn]; int iscut[maxn]; int belong[maxn]; int instack[maxn]; int sta[maxn]; int num[maxn]; int vis[maxn]; int d[maxn]; int bridge; int cnt; int tot; int top; int ans; int n, m; int ans_max; struct node{ int to,next; bool flag, iscut; int w; }edge[maxm];//链式前向星存边 inline int read() {//读入优化 int x=0,w=1;char ch=getchar(); while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();} while(ch<='9'&&ch>='0') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*w; } inline void add(int u, int v, int w) { edge[tot] = {v, head[u], 0, 0, w}; head[u] = tot++; } vector<int> G[maxn]; void tarjan(int u) { int v; dfn[u] = low[u] = ++id; sta[++top] = u, instack[u] = 1; for (int i = head[u]; ~i; i = edge[i].next) { v = edge[i].to; if (edge[i].flag) continue; edge[i].flag = edge[i^1].flag = 1; if (!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) edge[i].iscut = edge[i^1].iscut = 1, bridge++; } else if (instack[v]) low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) { cnt++; do { num[cnt]++, v = sta[top--]; belong[v] = cnt, instack[v] = 0; } while (u != v); } } void solve() { id = bridge = top = cnt = ans_max = 0; memset(low, 0, sizeof(low)); memset(dfn, 0, sizeof(dfn)); memset(num, 0, sizeof(num)); memset(vis, 0, sizeof(vis)); memset(belong, 0, sizeof(belong)); memset(instack, 0, sizeof(instack)); tarjan(1); int flag = 0; for (int i = 1; i <= n && !flag; ++ i) { if (!belong[i]) flag = 1; } //cout << bridge << " " << flag << endl; if (flag) cout << 0 << endl; else if (!bridge) cout << -1 << endl; else { int ans = 10000; for (int i = 0; i <= cnt; ++ i) G[i].clear(); for (int u = 1; u <= n; ++ u) { for (int i = head[u]; ~i; i = edge[i].next) { if (edge[i].iscut) ans = min(edge[i].w, ans); } } if (ans == 0) ans = 1; cout << ans << endl; } } int main() { while (~scanf("%d %d", &n, &m)) { if (!n && !m) break; memset(head, -1, sizeof(head)); tot = 0; for (int i = 1,u,v,w; i <= m; ++ i) { scanf("%d%d%d",&u,&v,&w); add(u, v, w), add(v, u, w); } solve(); } return 0; } /* 3 3 1 2 0 1 3 2 2 3 1 3 2 1 2 0 2 3 4 3 2 1 2 0 2 1 0 */
Tajan求点双连通分量#
1.点双联通分量
若一张无向连通图不存在割点, 则称它为”点双连通图”.无向图的极大点双连通子图被称为”点双连通分量”, 简记为”v-DCC”。
一张无向连通图是”点双连通分量”, 当且仅当满足下列两个条件之一:
1) 图的顶点数不超过2。
2) 图中任意两点都同时包含在至少一个”简单环”指的是不自交的环,也就是我们通常画出的环。
这样就可以保证图不存在割点。具体证明方法这里就不解释了:)
若某个节点为孤立点,则它自己单独构成一个v-DCC. 除了孤立点以外,点双连通分量的大小至少为2,根据v-DCC定义中的”极大”性,虽然桥不属于任何eDCC,但是割点可能属于多个v-DCC。
为了求出”点双连通分量”,需要在Tarjan算法的过程中维护一个栈,。也就类似于我们前文所说的割点的求法。是不是和前文的缩点很类似呢!!!!!它们真的就长得挺像。但从某种程度上说,还是要复杂一些。
我好像没有找到比较好的模板题。
倒是找到了一道比较综合的题目:HDU - 3394

#include <bits/stdc++.h> using namespace std; const int M = 200010; const int N = 10010; struct Edge { int from, to; int next; } edge[M]; int head[N]; int cnt_edge; void add_edge(int u, int v) { edge[cnt_edge].from = u; edge[cnt_edge].to = v; edge[cnt_edge].next = head[u]; head[u] = cnt_edge++; } int dfn[N]; int idx; int low[N]; stack<Edge> stk; set<int> bcc; int cut; // 桥的数量 int ans; // 冲突边数量 int m, n; void dfs(int u, int pre) { dfn[u] = low[u] = ++idx; for (int i = head[u]; i != -1; i = edge[i].next) { int v = edge[i].to; if (v == pre) continue; if (!dfn[v]) { stk.push(edge[i]); dfs(v, u); low[u] = min(low[u], low[v]); if (low[v] >= dfn[u]) // 割点 { Edge tmp; int cnt = 0; bcc.clear(); do { cnt++; tmp = stk.top(); stk.pop(); bcc.insert(tmp.from); bcc.insert(tmp.to); } while (tmp.from != u || tmp.to != v); if (cnt > bcc.size()) ans += cnt; } if (low[v] > dfn[u]) ++cut; } else if (dfn[v] < dfn[u]) { stk.push(edge[i]); low[u] = min(low[u], dfn[v]); } } } void init() { memset(head, -1, sizeof head); memset(dfn, 0, sizeof dfn); ans = cut = cnt_edge = idx = 0; } int main() { while (~scanf("%d%d", &n, &m)) { if (n == 0 && m == 0) break; int u, v; init(); for (int i = 0; i < m; ++i) { scanf("%d%d", &u, &v); add_edge(u, v); add_edge(v, u); } for (int i = 1; i <= n; ++i) if (!dfn[i]) dfs(i, -1); printf("%d %d\n", cut, ans); } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人