tarjan几题

发布时间 2024-01-07 15:58:10作者: cloud_eve

SB tarjan趋势!!!

友情提示:点击题目直接传送

[bzoj1123][POI2008]BLO

Description

Byteotia 城市有 n 个 towns m条双向roads. 每条 road 连接两个不同的 towns ,没有重复的road. 所有towns连通。

Input Format

输入 \(n\le100000, m\le500000\) 及 m 条边

Output Format

输出 n 个数,代表如果把第 i 个点去掉,将有多少对点不能互通。

Sample

样例输入

5 5
1 2
2 3
1 3
3 4
4 5

样例输出

8
8
16
14
8

题解

本题要求求去掉第 i 个点后消失了多少对节点。(关于加减节点的问题就容易想到点双连通分量)
拿到这道题,首先想到的就是每去掉一个点,就会少(n-1)对点。
后来看样例发现:好像这个节点对是有顺序的,那就是 2(n-1)对点。
后来转念一想,这不是 tarjan 专题吗?不可能出个这么水的题。。。
于是又手模了几个样例,才发现刚才想的也对:(但太片面了)
1、若现在这个节点为 x ,且它并不是割点,那么它只能是环里的一个不是割点的点,答案就是 2(n-1)(割点是指强联通分量中的一个特殊的点,不在环中的单个点也为割点)
2、若现在这个节点 \(x\) 是割点,那么它的消失一定会构造出森林,那么消失的节点对也就不止 2(n-1)对 。答案应该是每一棵树的大小乘上原图上其余部分的大小,最后再乘2。(不同顺序也算)

ans+=2*siz[to]*(n-siz[to])*1ll;   (错解)

  于是便高高兴兴地开始打代码 —— 0分。。。
为什么呢?
上面那个式子将我们枚举的那个断点算重了好多次。不可以乘二。。。
我们尝试换一种思路:

ans+=siz[to]*(n-siz[to])*1ll;

(虽然就少了'×2' ,但至少不会算重啊。。。)

其实 tarjan 就是基于 dfs 而衍生出来的算法。dfs有一个特性,它在遍历一张图时,不可遍历重复的点(遍历树时,不可遍历其父节点),否则就会死循环。而这道题,我们需要知道这个节点 x 以上部分的大小,从而将这以上部分也作为一棵子树进行统计答案。而 tarjan(dfs) 却无法访问这棵“子树”,这就造成我们少计算了一种情况。统计答案:

ans+=(sum+1)*(n-sum-1) + (n-1)

其中的sum为真正的子树和(其实特别好维护,就是dfs求),我用(n-sum)就间接地求出了这个节点上面的“子树”。为什么sum要减一加一呢?我们再回过头去看,在统计每一个真正的子树所做的贡献时,我们将枚举的断点归到了其余的部分,而不是这棵子树,所以这种情况也需要同样处理。

那‘+(n-1)’是怎么回事呢?我们再看一下这个算法,所有的子树都考虑了节点对 顺序的情况(每两个子树或直接或间接地乘了两遍),但相对我们枚举的切点,只乘了一遍,所以要补救一下。

最后一步就是求割点了。(板子)

opt=0;
    if(dfn[to]>=low[x]){
        tarjan(to);
        low[x]=min(low[x],low[to]);
        if(low[x]>=dfn[to]) opt++;
        if(x!=root||opt>1)  cut[x]=1;
    }
    else low[x]=min(low[x],dfn[to]);

(注意一下,针对有向图的tarjan,需要判断是否入队;而无向图就不用)

贴代码

#include <bits/stdc++.h>
#define N 100010
#define ll long long
using namespace std;
int m, n, tot, tar;
int first[N],  dfn[N], low[N], siz[N];
ll ans[N];
bool cut[N];
struct tree {
	int to, next;
} a[N * 10];
inline int min(int x, int y) {
	return x < y ? x : y;
}
inline void add(int x, int y) {
	if (x == y)
		return ;
	a[++tot] = (tree) {
		y, first[x]
	};
	first[x] = tot;
	a[++tot] = (tree) {
		x, first[y]
	};
	first[y] = tot;
}
inline void tarjan(int x) {
	low[x] = dfn[x] = ++tar;
	siz[x] = 1;
	int opt = 0, sum = 0;
	for (register int i = first[x]; i; i = a[i].next) {
		int to = a[i].to;
		if (!dfn[to]) {
			tarjan(to);
			siz[x] += siz[to];
			low[x] = min(low[x], low[to]);
			if (low[to] >= dfn[x]) {
				opt++;
				ans[x] += 1ll * siz[to] * (n - siz[to]);
				sum += siz[to];
			}
			if (x != 1 || opt > 1)
				cut[x] = 1;
		} else
			low[x] = min(low[x], dfn[to]);
	}
	if (cut[x])
		ans[x] += 1ll * (n - sum - 1) * (sum + 1) + 1ll * (n - 1);
	else
		ans[x] = 1ll * 2 * (n - 1);
}
signed main() {
	scanf("%d%d", &n, &m);
	for (int i = 1, x, y; i <= m; ++i)
		scanf("%d%d", &x, &y), add(x, y);
	tarjan(1);
	for (int i = 1; i <= n; ++i)
		printf("%lld\n", ans[i]);
}

原文:[ BZOJ1123 ] BLO(tarjan点双连通分量)

[Usaco2006 Jan] Redundant Paths 分离的路径

Description

In order to get from one of the F (1 <= F <= 5,000) grazing fields (which are numbered 1..F) to another field, Bessie and the rest of the herd are forced to cross near the Tree of Rotten Apples. The cows are now tired of often being forced to take a particular path and want to build some new paths so that they will always have a choice of at least two separate routes between any pair of fields. They currently have at least one route between each pair of fields and want to have at least two. Of course, they can only travel on Official Paths when they move from one field to another. Given a description of the current set of R (F-1 <= R <= 10,000) paths that each connect exactly two different fields, determine the minimum number of new paths (each of which connects exactly two fields) that must be built so that there are at least two separate routes between any pair of fields. Routes are considered separate if they use none of the same paths, even if they visit the same intermediate field along the way. There might already be more than one paths between the same pair of fields, and you may also build a new path that connects the same fields as some other path.

为了从F(1≤F≤5000)个草场中的一个走到另一个,贝茜和她的同伴们有时不得不路过一些她们讨厌的可怕的树.奶牛们已经厌倦了被迫走某一条路,所以她们想建一些新路,使每一对草场之间都会至少有两条相互分离的路径,这样她们就有多一些选择.
每对草场之间已经有至少一条路径.给出所有R(F-1≤R≤10000)条双向路的描述,每条路连接了两个不同的草场,请计算最少的新建道路的数量, 路径由若干道路首尾相连而成.两条路径相互分离,是指两条路径没有一条重合的道路.但是,两条分离的路径上可以有一些相同的草场. 对于同一对草场之间,可能已经有两条不同的道路,你也可以在它们之间再建一条道路,作为另一条不同的道路.

Input Format

Line 1: Two space-separated integers: F and R
Lines 2..R+1: Each line contains two space-separated integers which are the fields at the endpoints of some path.

第1行输入F和R,接下来R行,每行输入两个整数,表示两个草场,它们之间有一条道路.

Output Format

Line 1: A single integer that is the number of new paths that must be built.

最少的需要新建的道路数.

Sample

样例输入

7 7
1 2
2 3
3 4
2 5
4 5
5 6
5 7

样例输出

2

Hint

Hint 2

思路

首先,我们可以发现题目要求每一个点到其他所有点的路径不只有一条。
这本质上就是要我们把这个图所有的桥都消除掉
要消除掉桥,首先必须要把边双先缩起。
缩边双很简单:和求强连通分量一模一样。
唯一要注意的是我们要多记录一个fa,防止我们求low的时候直接把fa算进来。
求完边双之后,我们会发现原图变成一个树的形式
要把这个树上所有的单边去掉,我们只需要把叶子节点两两连起来即可。
注意:对于直接就是一个环的情况,需要特判,直接输出0即可。

贴代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 5000 + 100;
vector <int> e[N], e2[N];
int n, m, dfn_to, top, cnt;
int dfn[N], low[N], InStack[N], mstack[N], belong[N];
bool vis[N];
ll read() {
	ll x = 0, f = 1;
	char c = getchar();
	while (!isdigit(c)) {
		if (c == '-')
			f = -1;
		c = getchar();
	}
	while (isdigit(c)) {
		x = x * 10 + c - '0';
		c = getchar();
	}
	return x * f;
}
void Tarjan(int now, int father) {
	vis[now] = 1;
	InStack[now] = 1;
	dfn[now] = ++dfn_to;
	low[now] = dfn[now];
	mstack[++top] = now;
	for (int i = 0; i < int(e[now].size()); i++)
		if (vis[e[now][i]] == 0) {
		Tarjan(e[now][i], now);
			low[now] = min(low[now], low[e[now][i]]);
		} else if (e[now][i] != father && InStack[e[now][i]] == 1)
			low[now] = min(low[now], dfn[e[now][i]]);
	if (low[now] == dfn[now]) {
		cnt++;
		while (mstack[top + 1] != now) {
			InStack[mstack[top]] = 0;
			belong[mstack[top--]] = cnt;
		}
	}
	return ;
}
int GetAns(int now, int father) {
	int ans = 0;
	for (int i = 0; i < int(e2[now].size()); i++)
		if (e2[now][i] != father)
			ans += GetAns(e2[now][i], now);
	return max(1, ans);
}
int main() {
	n = read(), m = read();
	for (int i = 1; i <= n; i++)
		e[i].reserve(4);
	for (int i = 1; i <= m; i++) {
		int s = read();
		int t = read();
		e[s].push_back(t);
		e[t].push_back(s);
	}
	Tarjan(1, 0);
	for (int i = 1; i <= n; i++)
		for (int j = 0; j < int(e[i].size()); j++)
			if (belong[i] != belong[e[i][j]])
				e2[belong[i]].push_back(belong[e[i][j]]);
	int ans;
	ans = GetAns(belong[1], belong[1]) + (e2[belong[1]].size() == 1);
	if (cnt == 1) //特判只有一个环
		ans = 0;
	printf("%d", ans / 2 + ans % 2);
	return 0;
}

[USACO15JAN]Grass Cownoisseur G

Description

In an effort to better manage the grazing patterns of his cows, Farmer John has installed one-way cow paths all over his farm. The farm consists of N fields, conveniently numbered 1..N, with each one-way cow path connecting a pair of fields. For example, if a path connects from field X to field Y, then cows are allowed to travel from X to Y but not from Y to X.

Bessie the cow, as we all know, enjoys eating grass from as many fields as possible. She always starts in field 1 at the beginning of the day and visits a sequence of fields, returning to field 1 at the end of the day. She tries to maximize the number of distinct fields along her route, since she gets to eat the grass in each one (if she visits a field multiple times, she only eats the grass there once).

As one might imagine, Bessie is not particularly happy about the one-way restriction on FJ's paths, since this will likely reduce the number of distinct fields she can possibly visit along her daily route. She wonders how much grass she will be able to eat if she breaks the rules and follows up to one path in the wrong direction. Please compute the maximum number of distinct fields she can visit along a route starting and ending at field 1, where she can follow up to one path along the route in the wrong direction. Bessie can only travel backwards at most once in her journey. In particular, she cannot even take the same path backwards twice.

约翰有 n块草场,编号1到n,这些草场由若干条单行道相连。奶牛贝西是美味牧草的鉴赏家,她想到达尽可能多的草场去品尝牧草。
贝西总是从1号草场出发,最后回到1号草场。她想经过尽可能多的草场,贝西在通一个草场只吃一次草,所以一个草场可以经过多次。因为草场是单行道连接,这给贝西的品鉴工作带来了很大的不便,贝西想偷偷逆向行走一次,但最多只能有一次逆行。问,贝西最多能吃到多少个草场的牧草。

INPUT:

The first line of input contains N and M, giving the number of fields and the number of one-way paths (1 <= N, M <= 100,000).

The following M lines each describe a one-way cow path. Each line contains two distinct field numbers X and Y, corresponding to a cow path from X to Y. The same cow path will never appear more than once.

输入:

第一行:草场数n,道路数m。以下m行,每行x和y表明有x到y的单向边,不会有重复的道路出现。

OUTPUT:

A single line indicating the maximum number of distinct fields Bessie

can visit along a route starting and ending at field 1, given that she can

follow at most one path along this route in the wrong direction.

输出:

一个数,逆行一次最多可以走几个草场。

样例

样例输入

7 10 
1 2 
3 1 
2 5 
2 4 
3 7 
3 5 
3 6 
6 5 
7 2 
4 7

样例输出

6

SOLUTION NOTES:

Here is an ASCII drawing of the sample input:

v---3-->6
7   | \ |
^\  v  \|
| \ 1   |
|   |   v
|   v   5
4<--2---^

Bessie can visit pastures 1, 2, 4, 7, 2, 5, 3, 1 by traveling

backwards on the path between 5 and 3. When she arrives at 3 she

cannot reach 6 without following another backwards path.

思路

分层图+tarjan缩点+最长路。
只有一个逆向边->分层图,每个点i在第二层有一个对应的编号为i+n的点。
第二层建变基本还原第一层,对于每一条边x到y还要建一条y到x+n连接一二层。
单向图保证第二层无法下来(逆向边只走一次),缩点时新建的边边权为目的地强联通分量的大小,缩点后为无向图,保证一个边最长路只走一次(点权只取一次)。
答案最后取max(dis[belong[1]],dis[belong[1+n]])(可能在一层终点,也可能是二层终点)
复活吧,我的spfa!(雾

贴代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 100000 + 3;
int head[2 * maxn], dfn[2 * maxn], low[2 * maxn], ssize[2 * maxn];	//开二倍
int stk[2 * maxn], belong[2 * maxn], h[2 * maxn], dis[2 * maxn];
int tot, dfn_clock, scc_cnt, n, m, cnt, top;
bool vis[2 * maxn];
struct edge { 														//原边
	int to, next;
} e[3 * maxn]; 														//开三倍
struct edge2 { 														//缩点之后的边
	int to, next, w;
} ed[3 * maxn]; 													//开三倍
int read() {
	int x = 0, f = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9') {
		if (ch == '-')f = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + ch - '0';
		ch = getchar();
	}
	return x * f;
}
void add(int a, int b) {
	e[++tot].to = b;
	e[tot].next = head[a];
	head[a] = tot;
	return ;
}
void insert(int a, int b, int w) {
	ed[++cnt].to = b;
	ed[cnt].w = w;
	ed[cnt].next = h[a];
	h[a] = cnt;
	return ;
}
void tarjan(int u) { 												//求强联通分量的板子
	dfn[u] = ++dfn_clock;
	low[u] = dfn[u];
	stk[++top] = u;
	for (int i = head[u]; i; i = e[i].next) {
		int v = e[i].to;
		if (!dfn[v]) {
			tarjan(v);
			low[u] = min(low[u], low[v]);
		} else if (!belong[v])
			low[u] = min(low[u], dfn[v]);
	}
	if (low[u] == dfn[u]) {
		scc_cnt++;
		while (1) {
			int tmp = stk[top--];
			belong[tmp] = scc_cnt;
			ssize[scc_cnt]++;
			if (tmp == u)
				break;
		}
	}
	return ;
}
void spfa(int s) { 													//跑最短路
	deque<int> q;
	q.push_back(s);
	while (!q.empty()) {
		int u = q.front();
		q.pop_front();
		vis[u] = 0;
		for (int i = h[u]; i; i = ed[i].next) {
			int v = ed[i].to, w = ed[i].w;
			if (dis[u] + w > dis[v]) {
				dis[v] = dis[u] + w;
				if (!vis[v]) {
					vis[v] = 1;
					if (!q.empty() && dis[v] > dis[q.front()])
						q.push_front(v);
					else
						q.push_back(v);
				}
			}
		}
	}
	return ;
}
int main() {
	n = read();
	m = read();
	for (int i = 1; i <= m; i++) {
		int x, y;
		x = read();
		y = read();
		add(x, y);
		add(x + n, y + n);
		add(y, x + n); 												//分层图核心加边
	}
	for (int i = 1; i <= 2 * n; i++)
		if (!dfn[i])
			tarjan(i);
	for (int i = 1; i <= 2 * n; i++) { 								//缩点
		for (int j = head[i]; j; j = e[j].next) {
			int x = belong[i];
			int y = belong[e[j].to];
			if (x != y)
				insert(x, y, ssize[y]);
		}
	}
	spfa(belong[1]);
	printf("%d\n", max(dis[belong[1]], dis[belong[1 + n]]));
	return 0;
}

「一本通 3.6 例 2」矿场搭建

Description

原题来自:HNOI 2012

煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。

请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。

Input Format

输入文件有若干组数据,每组数据的第一行是一个正整数N,表示工地的隧道数,接下来的N行每行是用空格隔开的两个整数S和T,表示挖煤点S与挖煤点T由隧道直接连接。输入数据以0结尾。

Output Format

输入文件中有多少组数据,输出文件中就有多少行。每行对应一组输入数据的结果。
其中第i行以 Case i: 开始(注意大小写,Case 与 i 之间有空格,i与 : 之间无空格,: 之后有空格),其后是用空格隔开的两个正整数,第一个正整数表示对于第i组输入数据至少需要设置几个救援出口,第二个正整数表示对于第i组输入数据不同最少救援出口的设置方案总数。
输出格式参照以下输入输出样例。

Sample

样例输入

9
1 3 
4 1
3 5
1 2
2 6
1 5
6 3
1 6
3 2
6 
1 2
1 3
2 4
2 5
3 6
3 7
0

样例输出

Case 1: 2 4
Case 2: 4 1

样例解释

Case 1 的四组解分别是 (2,4),(3,4),(4,5),(4,6)(2,4),(3,4),(4,5),(4,6);
Case 2 的一组解为(4,5,6,7)(4,5,6,7)。

Hint

N≤500,输入数据保证答案小于2^64。

思路

先tarjan求割点,把所有割点去掉以后寻找块。
如果有一个块没有割点,即这个子图联通,需要加两个点,方案总数C2,n(即n(n-1)/2)。
如果一个块只有一个割点,则需要在所有不含割点的图里任选一点,方案总数*(n-1)。
如果一个块有多于一个割点,则这个点要么可以通过其中一个割点可以通往救援出口,要么可以通过自己所在的另一个块到达救援出口,不需操作。
注意:一定要最后统计割点数,不开ll见祖宗。

贴代码

#include <bits/stdc++.h>
#define ll long long
#define N 505
#define M 1050
using namespace std;
ll fir[N], nxt[M], to[M];
ll tot, n, m;
void edge(ll u, ll v) {
	to[++tot] = v;
	nxt[tot] = fir[u];
	fir[u] = tot;
}
ll book[N][N];
ll dfn[N], low[N], sta[N], l[N];
ll num, col, top;
bool vis[N];
void tarjan(ll x) {
	dfn[x] = ++num;
	low[x] = ++num;
	sta[++top] = x;
	ll son = 0;
	for (ll k = fir[x]; k != -1; k = nxt[k]) {
		if (!dfn[to[k]]) {
			tarjan(to[k]);
			son++;
			low[x] = min(low[x], low[to[k]]);
			if ((x == 1 && son > 1) || (x != 1 && dfn[x] <= low[to[k]]))
				vis[x] = 1;
			if (dfn[x] <= low[to[k]]) {
				l[++col] = 0;
				book[col][++l[col]] = x;
				while (sta[top] != x) {
					book[col][++l[col]] = sta[top];
					top--;
				}
			}
		} else
			low[x] = min(low[x], dfn[to[k]]);
	}
}
void init() {
	memset(fir, -1, sizeof(fir));
	memset(dfn, 0, sizeof(dfn));
	memset(vis, 0, sizeof(vis));
	tot = -1;
	n = 0;
	col = 0;
	num = 0;
}
ll t = 0;
void slove() {
	init();
	for (ll i = 1; i <= m; i++) {
		ll u, v;
		scanf("%lld%lld", &u, &v);
		edge(u, v);
		edge(v, u);
		n = max(n, max(u, v));
	}
	for (ll i = 1; i <= n; i++) {
		if (!dfn[i]) {
			top = 0;
			tarjan(i);
		}
	}
	ll ans = 1, sum = 0;
	for (ll i = 1; i <= col; i++) {
		ll f = 0;
		for (ll j = 1; j <= l[i]; j++)
			if (vis[book[i][j]])
				f++;
		if (f == 0) {
			sum += 2;
			ans = ans * l[i] * (l[i] - 1) / 2;
		} else {
			if (f == 1) {
				sum++;
				ans = ans * (l[i] - 1);
			}
		}
	}
	t++;
	printf("Case %lld: %lld %lld\n", t, sum, ans);
}
int main() {
	while (scanf("%lld", &m) && m)
		slove();
	return 0;
}