二分图、拓扑与欧拉

1、二分图匹配问题(判断是否二分图可以使用并查集)
`
int n1, n2, m;
int h[500], e[100010],ne[100010], idx = 0;
int st[510], match[510];//st 标记是否递归找过, match[x]:和 x 编号的男生的编号

void add(int a, int b){
e[idx] = b, ne[idx] = h[a]; h[a] = idx++;
}

bool find(int x){//递归找可以匹配的点
for(int i = h[x]; i != -1; i = ne[i]){
int b = e[i];// 和各个点尝试能否匹配
if(!st[b]){
st[b] = 1;
// 当前尝试点没有被匹配或者和当前尝试点匹配的那个点可以换另一个匹配
if(match[b] == 0 || find(match[b])){
// 和当前尝试点匹配在一起
match[b] = x;
return true;
}
}
}
return false;
}

int main(){
memset(h, -1, sizeof h);
cin >> n1 >> n2 >> m;
// 保存图,因为只从一遍找另一边,所以该无向图只需要存储一个方向
for(int i = 0; i < m; i++){
int a, b;
cin >> a >> b;
add(a, b);
}
int res = 0;
//为各个点找匹配
for(int i = 1; i <= n1; i++){
memset(st, 0, sizeof st);
//找到匹配
if(find(i)) res++;
}
cout << res;
return 0;
}`

2、拓扑排序(核心在出度和入度,相当于bfs思路,常用于有多重先后顺序的点排列)(下面的例题用于求取所有起点出发到所有终点的总路径,拓扑在此并非用于‘求’排列,而是‘维护’题意的排列并求路径)
`const int MOD = 80112002;
int n, m; // n: 节点数, m: 边数
int rudu[5010];int chudu[5010];// 每个点的出度入度
int sg[5010]; // 标记数组,记录是否已入队
int dp[5010]; // dp[i]: 从任意起点到达i的路径数
int ans = 0;

// cb[x]: 从x出发的所有“反向边”目标(即图的反向边存储)
// rb[x]: 正向边的邻接表(可选)
unordered_map<int, vector> cb;
unordered_map<int, vector> rb;

void tppx() {
queue q;
for (int i = 1; i <= n; i++) {
if (rudu[i] == 0) {
q.push(i);// 初始化:所有入度为0的点是起点
dp[i] = 1;// 起点到自身路径数 = 1
sg[i] = 1;
}
}

// BFS式拓扑排序
while (!q.empty()) {
	int up = q.front(); q.pop();

	// 遍历up的所有后继节点(反向存储时要注意方向)
	for (int i = 0; i < cb[up].size(); i++) {
		int v = cb[up][i];
		rudu[v]--;  // 消去一条入度

		// 动态规划转移:dp[v] += dp[up]
		dp[v] = (dp[v] + dp[up]) % MOD;
	}

	for (int i = 1; i <= n; i++) {
		if (rudu[i] == 0 && !sg[i]) {
			q.push(i);
			sg[i] = 1;// 入度归0的点入队
		}
	}
}

//如果要判断可否拓扑排序,这里可以加一个if,判断是否所有点sg都记录为已经入队过

}

int main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int A, B; cin >> A >> B;
// 注意这里边是 B -> A (存反向图) 因为作者的dp计算方向是反向的
rudu[A]++; // A 的入度 +1
cb[B].push_back(A); // 记录反向边 B -> A
rb[A].push_back(B); // 记录正向边 A -> B
chudu[B]++; // B 的出度 +1
}

tppx();
for (int i = 1; i <= n; i++) {
	if (chudu[i] == 0) {
		ans = (ans + dp[i]) % MOD;// 所有出度为0的节点都是“终点”,记录到它们的所有路径
	}
}

cout << ans << endl;
return 0;

}`

3、欧拉路径与欧拉回路
第一步:使用dfs或并查集判断是否一个连通图
第二步:分有向图和无向图,使用不同方法判断。

例题:给定一张图,请你找出欧拉回路,即在图中找一个环使得每条边都在环上出现恰好一次。
`const int N = 100100, M = 400100;

int h[N],e[M],ne[M],idx;
int ans[N*2],cnt;
bool used[M];
int din[N],dout[N];
int n,m,ver;

void add(int a,int b){
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

void dfs(int u){
for(int &i = h[u]; ~i; ){
if(used[i]){ //如果这条边用过了
i = ne[i]; //删除这条边
continue;
}

    used[i] = true;  //标记这条边已使用
    if(ver == 1) used[i^1] = true;   //如果是无向图,那么这条边的反向边也要标记使用过了

    int t;
    if(ver == 1){
        t = i/2 + 1;
        if(i&1) t = -t;  //(0,1) (2,3) (4,5) 奇数编号是返回的边

    }else t = i+1;

    int j = e[i];
    i = ne[i];
    dfs(j);
    ans[cnt++] = t;
}

}
int main()
{
scanf("%d%d%d",&ver,&n,&m);
memset(h,-1,sizeof h);

for(int i = 0; i<m; i++){
    int a,b;
    scanf("%d%d",&a,&b);
    add(a,b);
    if(ver == 1) add(b,a);  //无向边
    din[b]++, dout[a]++;   
}

if(ver == 1){
    for(int i = 1; i<=n; i++){
        if(din[i]+dout[i] &1){
            //无向图含欧拉回路的充要条件是每个点的度都为偶数
            puts("NO");
            return 0;
        }
    }
}else{
    for(int i = 1; i<=n; i++){
        if(din[i] != dout[i]){
            //有向图含欧拉回路的充要条件是每个点的入度等于出度
            puts("NO");
            return 0;
        }
    }
}

for(int i = 1; i<=n; i++){
    if(~h[i]) {
        dfs(i);
        break;
    }
}

if(cnt < m){
    puts("NO");
    return 0;
}

puts("YES");
for(int i = cnt-1; i>=0; --i){
    cout<<ans[i]<<" ";
}
return 0;

}`
寻找欧拉回路:以无向图为例,因为每个点的度都为偶数,所以我们从任意一个点出发,假设所有点的度数都为2,那么dfs一定会回到起点形成一个回路.假设度数不全为2,有4,6,8...那么在dfs过程中,当走到这些点(假设走到点u)上时,可能会走到其他环上,但是由于度数是偶数,所以如果走到其他环上,最后也会回到点u,在dfs过后,一定会形成许多环,环与环之间会有交点(交边),在回溯过程中将这些点添加到答案中,就是其中一个欧拉回路。

posted @ 2025-10-15 20:55  yubai111  阅读(4)  评论(0)    收藏  举报