算法刷题记录(Day 28)

本文探讨了两种解决边双连通分量问题的算法:一种基于Tarjan算法和并查集,另一种利用LCA(最近公共祖先)和并查集。第一种方法在原图上进行操作,遇到割边时更新边的状态;第二种方法在缩点后的图上计算LCA,通过调整连接关系减少边的数量。这两种方法在不同的场景下各有优势,对于理解图论算法和实际问题的解决具有重要意义。

Network(poj 3694)

原题链接
题目类型:缩点+边双连通分量
思路一:
存在的问题:
1.可能存在重边,又无向图中要求子节点不能通过反边访问父亲节点,因此需要将边的序号作为条件传入参数中。
2.加入了一条新的边后如何求得割边的个数,dfs遍历到目的节点,然后列出路径上的割边的个数,并在dfs的过程中做出标记标记为不是割边。需要注意的是,需要设置dfs的返回值,以标志该条路径是否为到达目的块的路径。
3.那么应该遍历的是缩点后的图还是原先的图呢?由于本题时间限制较少,因此可以直接在原图上进行遍历。

具体的实现代码如下所示:

//WA代码
#include<iostream>
#include<vector>
#include<cstring>
#include<stack>
#define NMAX 100005

using namespace std;
int F, R;
struct edge {
	int from, to, next;
	int is;//是否为割边
};
vector<edge> E;
int h[NMAX];
int a, b;
void addline(int x, int y) {
	edge cur;
	cur.from = x, cur.to = y, cur.next = h[x], cur.is = 0;
	h[x] = E.size();
	E.push_back(cur);

	cur.from = y, cur.to = x, cur.next = h[y], cur.is = 0;
	h[y] = E.size();
	E.push_back(cur);
}

int dfn[NMAX], low[NMAX], belong[NMAX];
stack<int > S;
int tot = 0;
int connect = 0;
void tarjan(int x, int fa, int side) {
	dfn[x] = low[x] = ++tot;
	S.push(x);
	for (int i = h[x]; i != -1; i = E[i].next) {
		int to = E[i].to;
		if (!dfn[to]) {
			tarjan(to, x, i);
			low[x] = min(low[x], low[to]);
			//用来判断是否为割边
			if (low[to] == dfn[to]) {
				E[i].is = 1;
				E[i ^ 1].is = 1;//其反向边也应该添加
			}
		}
		else if (i!= (side^1) ) low[x] = min(low[x], dfn[to]); //修改条件,避免存在重边
	}
	if (dfn[x] == low[x]) {
		int cur = -1;
		while (cur!=x) {
			cur = S.top();
			S.pop();
			belong[cur] = connect;
		}
		connect++;
	}

}

int num = 0;
int vis[NMAX];
int dfs(int x, int dst,int & res) {
	int ret = 0;
	vis[x] = 1;
	if (belong[x] == dst) return 1;
	for (int i = h[x]; i != -1; i = E[i].next) {
		if (!vis[E[i].to]) {
			int j = dfs(E[i].to, dst, res);
			if (j) ret = j;
			if (j && E[i].is) {
				res--;
				E[i].is = 0;
				E[i ^ 1].is = 0;
			}
			
		}
	}
	return ret;

}
int main() {
	cin >> F >> R;
	while (!(F == 0 && R == 0)) {
		memset(h, -1, sizeof(h));
		E.clear();
		while (!S.empty()) S.pop();
		tot = 0;
		connect = 0;
		for (int i = 0; i < R; i++) {
			cin >> a >> b;
			addline(a, b);
		}
		//for (int i = 0; i < E.size(); i++) cout << E[i].from << " " << E[i].to << endl;
		//for(int i=h[4];i!=-1;i=E[i].next) cout << E[i].from << " " << E[i].to << endl;
		memset(dfn, 0, sizeof(dfn));
		for (int i = 1; i <= F; i++) {
			if (!dfn[i]) tarjan(i, 0, -1);
		}

		int Q;
		cin >> Q;
		int res = connect-1;
		num++;
		cout << "Case " << num << ":" << endl;
		for (int i = 0; i < Q; i++) {
			int a, b;
			cin >> a >> b;
			int A = belong[a];
			int B = belong[b];
			if (A != B) {
				//进行原图的dfs并进行相应的修改
				memset(vis, 0, sizeof(vis));
				dfs(a, B,res);//引用会一直传递下去吗?
			}
			cout << res << endl;
		}
		cout << endl;
		cin >> F >> R;
	}
}

仍然没有找到哪里存在错误!!!

思路二:
学习别人的思路:lca+并查集

LCA(最近公共祖先)是指在一棵树上,两个节点深度最大的公共祖先节点
用于解决两个节点仅有唯一的一条确定的路径的问题。
在本例中,使用的是暴力的算法,逐步向上求取lca。对于边的删除,利用的是并查集的方式来实现,即每次通过边移动一个点,然后便直接查找其父亲节点,以回避同一个集合中的边。

tarjan求最近公共祖先
数组的引用

//AC代码
#include<iostream>
#include<vector>
#include<cstring>
#include<stack>
#define NMAX 100005

using namespace std;
int F, R;
struct edge {
	int from, to, next;
	int is;//是否为割边
};
vector<edge> E;
vector<edge> Es;
int h[NMAX];
int hs[NMAX];
int a, b;
void addline(int x, int y,vector<edge> & E,int  (& h) [NMAX] ) {
	edge cur;
	cur.from = x, cur.to = y, cur.next = h[x], cur.is = 0;
	h[x] = E.size();
	E.push_back(cur);

	cur.from = y, cur.to = x, cur.next = h[y], cur.is = 0;
	h[y] = E.size();
	E.push_back(cur);
}

int dfn[NMAX], low[NMAX], belong[NMAX];
stack<int > S;
int tot = 0;
int connect = 0;
void tarjan(int x, int fa, int side) {
	dfn[x] = low[x] = ++tot;
	S.push(x);
	for (int i = h[x]; i != -1; i = E[i].next) {
		int to = E[i].to;
		if (!dfn[to]) {
			tarjan(to, x, i);
			low[x] = min(low[x], low[to]);
			//用来判断是否为割边
			if (low[to] == dfn[to]) {
				E[i].is = 1;
				E[i ^ 1].is = 1;//其反向边也应该添加
			}
		}
		else if (i!= (side^1) ) low[x] = min(low[x], dfn[to]); //修改条件,避免存在重边
	}
	if (dfn[x] == low[x]) {
		int cur = -1;
		while (cur!=x) {
			cur = S.top();
			S.pop();
			belong[cur] = connect;
		}
		connect++;
	}

}

int num = 0;
int vis[NMAX];
int deep[NMAX], fa[NMAX], bf[NMAX];
int dfs(int x, int dst, int& res) {
	int ret = 0;
	vis[x] = 1;
	if (belong[x] == dst) return 1;
	for (int i = h[x]; i != -1; i = E[i].next) {
		if (!vis[E[i].to]) {
			int j = dfs(E[i].to, dst, res);
			if (j) ret = j;
			if (j && E[i].is) {
				res--;
				E[i].is = 0;
				E[i ^ 1].is = 0;
			}
			
		}
	}
	return ret;

}
void dfs(int x) {
	for (int i = hs[x]; i != -1; i = Es[i].next) {
		if (!deep[Es[i].to]) {
			deep[Es[i].to] = deep[x] + 1;
			fa[Es[i].to] = x;
			dfs(Es[i].to);
		}
	}
}
int find(int x) {
	if (x != bf[x]) bf[x] = find(bf[x]);
	return bf[x];
}
int LCA(int u, int v) {
	while (u != v) {
		if (deep[u] > deep[v]) u = fa[u];
		else if (deep[u] < deep[v]) v = fa[v];
		else u = fa[u], v = fa[v];
		u = find(u), v = find(v);
	}
	return u;
}
int main() {
	cin >> F >> R;
	while (!(F == 0 && R == 0)) {
		memset(h, -1, sizeof(h));
		memset(hs, -1, sizeof(hs));
		E.clear();
		Es.clear();
		while (!S.empty()) S.pop();
		tot = 0;
		connect = 0;
		for (int i = 0; i < R; i++) {
			cin >> a >> b;
			addline(a, b, E, h);
		}
		//for (int i = 0; i < E.size(); i++) cout << E[i].from << " " << E[i].to << endl;
		//for(int i=h[4];i!=-1;i=E[i].next) cout << E[i].from << " " << E[i].to << endl;
		memset(dfn, 0, sizeof(dfn));
		for (int i = 1; i <= F; i++) {
			if (!dfn[i]) tarjan(i, 0, -1);
		}

		//在缩点后的图上来处理
		//首先建立缩点后的图
		for (int i = 0; i < E.size(); i++) {
			if (E[i].is) {
				addline(belong[E[i].from], belong[E[i].to], Es, hs );
			}
		}
		
		//lca一定需要deep和fa的关系吗?->lca是基于树的,因此需要fa的信息,而在暴力搜索lca的过程中,需要向上寻找父节点
		memset(deep, 0, sizeof(deep));
		for (int i = 0; i < connect; i++) {
			bf[i] = i;
			if (!deep[i]) deep[i] = 1, fa[i] = -1, dfs(i);
		}
		int Q;
		cin >> Q;
		int res = connect-1;
		num++;
		cout << "Case " << num << ":" << endl;
		for (int i = 0; i < Q; i++) {
			int a, b;
			cin >> a >> b;
			int A = find(belong[a]);
			int B = find(belong[b]);
			if (A != B) {
				//首先找公共祖先
				int lca = LCA(A, B);
				//计算路径上的边的个数
				while (A != lca) {
					res--;
					bf[A] = lca; //A一定是代表元

					A = fa[A];
					A = find(A);
				}
				while (B != lca) {
					res--;
					bf[B] = lca;

					B = fa[B];
					B = find(B);
				}
			}
			cout << res << endl;
		}
		cout << endl;
		cin >> F >> R;
	}
}

对于思路一进行验证,将思路一应用到缩点后的图中,是成立的。
满脸疑惑

#include<iostream>
#include<vector>
#include<cstring>
#include<stack>
#define NMAX 100005

using namespace std;
int F, R;
struct edge {
	int from, to, next;
	int is;//是否为割边
};
vector<edge> E;
vector<edge> Es;
int h[NMAX];
int hs[NMAX];
int a, b;
void addline(int x, int y,vector<edge> & E,int  (& h) [NMAX] ) {
	edge cur;
	cur.from = x, cur.to = y, cur.next = h[x], cur.is = 0;
	h[x] = E.size();
	E.push_back(cur);

	cur.from = y, cur.to = x, cur.next = h[y], cur.is = 0;
	h[y] = E.size();
	E.push_back(cur);
}

int dfn[NMAX], low[NMAX], belong[NMAX];
stack<int > S;
int tot = 0;
int connect = 0;
void tarjan(int x, int fa, int side) {
	dfn[x] = low[x] = ++tot;
	S.push(x);
	for (int i = h[x]; i != -1; i = E[i].next) {
		int to = E[i].to;
		if (!dfn[to]) {
			tarjan(to, x, i);
			low[x] = min(low[x], low[to]);
			//用来判断是否为割边
			if (low[to] == dfn[to]) {
				E[i].is = 1;
				E[i ^ 1].is = 1;//其反向边也应该添加
			}
		}
		else if (i!= (side^1) ) low[x] = min(low[x], dfn[to]); //修改条件,避免存在重边
	}
	if (dfn[x] == low[x]) {
		int cur = -1;
		while (cur!=x) {
			cur = S.top();
			S.pop();
			belong[cur] = connect;
		}
		connect++;
	}

}

int num = 0;
int vis[NMAX];
int deep[NMAX], fa[NMAX], bf[NMAX];
int hdfs(int x, int dst, int& res, vector<edge>& E) {
	int ret = 0;
	vis[x] = 1;
	if (x == dst) return 1;
	for (int i = hs[x]; i != -1; i = E[i].next) {
		if (!vis[E[i].to]) {
			int j = hdfs(E[i].to, dst, res, E);
			if (j) ret = j;
			if (j && !E[i].is) {
				res--;
				//标记该边已经被删除
				E[i].is = 1;
				E[i ^ 1].is = 1;
			}
		}
	}
	return ret;

}
void dfs(int x) {
	for (int i = hs[x]; i != -1; i = Es[i].next) {
		if (!deep[Es[i].to]) {
			deep[Es[i].to] = deep[x] + 1;
			fa[Es[i].to] = x;
			dfs(Es[i].to);
		}
	}
}
int find(int x) {
	if (x != bf[x]) bf[x] = find(bf[x]);
	return bf[x];
}
int LCA(int u, int v) {
	while (u != v) {
		if (deep[u] > deep[v]) u = fa[u];
		else if (deep[u] < deep[v]) v = fa[v];
		else u = fa[u], v = fa[v];
		u = find(u), v = find(v);
	}
	return u;
}
int main() {
	cin >> F >> R;
	while (!(F == 0 && R == 0)) {
		memset(h, -1, sizeof(h));
		memset(hs, -1, sizeof(hs));
		E.clear();
		Es.clear();
		while (!S.empty()) S.pop();
		tot = 0;
		connect = 0;
		for (int i = 0; i < R; i++) {
			cin >> a >> b;
			addline(a, b, E, h);
		}
		//for (int i = 0; i < E.size(); i++) cout << E[i].from << " " << E[i].to << endl;
		//for(int i=h[4];i!=-1;i=E[i].next) cout << E[i].from << " " << E[i].to << endl;
		memset(dfn, 0, sizeof(dfn));
		for (int i = 1; i <= F; i++) {
			if (!dfn[i]) tarjan(i, 0, -1);
		}

		//在缩点后的图上来处理
		//首先建立缩点后的图
		for (int i = 0; i < E.size(); i++) {
			if (E[i].is) {
				addline(belong[E[i].from], belong[E[i].to], Es, hs );
				i++;//反向边就不要再建立了
			}
		}
		//cout << "bian" << endl;
		//for (int i = 0; i < Es.size(); i++) {
			//cout << Es[i].from << " " << Es[i].to << " "<< Es[i].next<< endl;
		//}
		//lca一定需要deep和fa的关系吗?->lca是基于树的,因此需要fa的信息,而在暴力搜索lca的过程中,需要向上寻找父节点
		memset(deep, 0, sizeof(deep));
		for (int i = 0; i < connect; i++) {
			bf[i] = i;
			if (!deep[i]) deep[i] = 1, fa[i] = -1, dfs(i);
		}
		int Q;
		cin >> Q;
		int res = connect-1;
		num++;
		cout << "Case " << num << ":" << endl;
		for (int i = 0; i < Q; i++) {
			int a, b;
			cin >> a >> b;
			int A = belong[a];
			int B = belong[b];
			if (A != B) {
				//dfs来搜索到达那里的路径
				memset(vis, 0, sizeof(vis));
				hdfs(A, B, res,Es);
			}
			cout << res << endl;
		}
		cout << endl;
		cin >> F >> R;
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值